@happyvertical/smrt-education 0.57.43 → 0.57.44

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (82) hide show
  1. package/AGENTS.md +12 -0
  2. package/README.md +5 -0
  3. package/agents/course-authoring-service.md +179 -0
  4. package/agents/course-generation-review.md +79 -0
  5. package/agents/learner-service.md +33 -0
  6. package/agents/question-records.md +100 -0
  7. package/dist/chunks/{workbook-photo-service-C6mbUUp6.js → workbook-photo-service-BxvZDqjM.js} +1440 -309
  8. package/dist/chunks/workbook-photo-service-BxvZDqjM.js.map +1 -0
  9. package/dist/course-append-only.d.ts +15 -0
  10. package/dist/course-append-only.d.ts.map +1 -0
  11. package/dist/course-authoring-contracts.d.ts +464 -0
  12. package/dist/course-authoring-contracts.d.ts.map +1 -0
  13. package/dist/course-authoring-licence.d.ts +15 -0
  14. package/dist/course-authoring-licence.d.ts.map +1 -0
  15. package/dist/course-authoring-models.d.ts +291 -0
  16. package/dist/course-authoring-models.d.ts.map +1 -0
  17. package/dist/course-authoring-service.d.ts +244 -0
  18. package/dist/course-authoring-service.d.ts.map +1 -0
  19. package/dist/course-delegation.d.ts +26 -0
  20. package/dist/course-delegation.d.ts.map +1 -0
  21. package/dist/course-generation-contracts.d.ts +150 -0
  22. package/dist/course-generation-contracts.d.ts.map +1 -0
  23. package/dist/course-generation-models.d.ts +94 -0
  24. package/dist/course-generation-models.d.ts.map +1 -0
  25. package/dist/course-generation-output.d.ts +31 -0
  26. package/dist/course-generation-output.d.ts.map +1 -0
  27. package/dist/course-generation-proposal-generator.d.ts +94 -0
  28. package/dist/course-generation-proposal-generator.d.ts.map +1 -0
  29. package/dist/course-generation-review-service.d.ts +149 -0
  30. package/dist/course-generation-review-service.d.ts.map +1 -0
  31. package/dist/course-release-scope.d.ts +37 -0
  32. package/dist/course-release-scope.d.ts.map +1 -0
  33. package/dist/course-review-contracts.d.ts +127 -0
  34. package/dist/course-review-contracts.d.ts.map +1 -0
  35. package/dist/course-review-digest.d.ts +4 -0
  36. package/dist/course-review-digest.d.ts.map +1 -0
  37. package/dist/course-review-evaluation.d.ts +63 -0
  38. package/dist/course-review-evaluation.d.ts.map +1 -0
  39. package/dist/course-review-gate.d.ts +15 -0
  40. package/dist/course-review-gate.d.ts.map +1 -0
  41. package/dist/course-review-models.d.ts +96 -0
  42. package/dist/course-review-models.d.ts.map +1 -0
  43. package/dist/group-sharing-contracts.d.ts +5 -0
  44. package/dist/group-sharing-contracts.d.ts.map +1 -1
  45. package/dist/group-sharing-policy.d.ts.map +1 -1
  46. package/dist/group-sharing-reader.d.ts.map +1 -1
  47. package/dist/index.d.ts +27 -0
  48. package/dist/index.d.ts.map +1 -1
  49. package/dist/index.js +5658 -116
  50. package/dist/index.js.map +1 -1
  51. package/dist/ingestion.js +1 -1
  52. package/dist/learner-plan-service.d.ts.map +1 -1
  53. package/dist/learner-service.d.ts +81 -7
  54. package/dist/learner-service.d.ts.map +1 -1
  55. package/dist/learners.d.ts +6 -0
  56. package/dist/learners.d.ts.map +1 -1
  57. package/dist/manifest.json +14304 -2177
  58. package/dist/question-authoring-service.d.ts +125 -0
  59. package/dist/question-authoring-service.d.ts.map +1 -0
  60. package/dist/question-content.d.ts +25 -0
  61. package/dist/question-content.d.ts.map +1 -0
  62. package/dist/question-contracts.d.ts +240 -0
  63. package/dist/question-contracts.d.ts.map +1 -0
  64. package/dist/question-models.d.ts +124 -0
  65. package/dist/question-models.d.ts.map +1 -0
  66. package/dist/question-render.d.ts +20 -0
  67. package/dist/question-render.d.ts.map +1 -0
  68. package/dist/question-response-contracts.d.ts +54 -0
  69. package/dist/question-response-contracts.d.ts.map +1 -0
  70. package/dist/question-response-service.d.ts +63 -0
  71. package/dist/question-response-service.d.ts.map +1 -0
  72. package/dist/question-rubric.d.ts +23 -0
  73. package/dist/question-rubric.d.ts.map +1 -0
  74. package/dist/smrt-knowledge.json +5410 -910
  75. package/dist/submission-service.d.ts.map +1 -1
  76. package/dist/submissions.d.ts +5 -0
  77. package/dist/submissions.d.ts.map +1 -1
  78. package/dist/workbook-contracts.d.ts +6 -0
  79. package/dist/workbook-contracts.d.ts.map +1 -1
  80. package/dist/workbook-models.d.ts.map +1 -1
  81. package/package.json +8 -8
  82. package/dist/chunks/workbook-photo-service-C6mbUUp6.js.map +0 -1
@@ -4,8 +4,8 @@ import { AssetCollection, createAssetRuntime, withPrivateAssetAccess } from "@ha
4
4
  import { TenantCollection } from "@happyvertical/smrt-users";
5
5
  //#region \0rolldown/runtime.js
6
6
  var __create = Object.create;
7
- var __defProp$8 = Object.defineProperty;
8
- var __getOwnPropDesc$8 = Object.getOwnPropertyDescriptor;
7
+ var __defProp$9 = Object.defineProperty;
8
+ var __getOwnPropDesc$9 = Object.getOwnPropertyDescriptor;
9
9
  var __getOwnPropNames = Object.getOwnPropertyNames;
10
10
  var __getProtoOf = Object.getPrototypeOf;
11
11
  var __hasOwnProp = Object.prototype.hasOwnProperty;
@@ -13,35 +13,35 @@ var __commonJSMin = (cb, mod) => () => (mod || (cb((mod = { exports: {} }).expor
13
13
  var __copyProps = (to, from, except, desc) => {
14
14
  if (from && typeof from === "object" || typeof from === "function") for (var keys = __getOwnPropNames(from), i = 0, n = keys.length, key; i < n; i++) {
15
15
  key = keys[i];
16
- if (!__hasOwnProp.call(to, key) && key !== except) __defProp$8(to, key, {
16
+ if (!__hasOwnProp.call(to, key) && key !== except) __defProp$9(to, key, {
17
17
  get: ((k) => from[k]).bind(null, key),
18
- enumerable: !(desc = __getOwnPropDesc$8(from, key)) || desc.enumerable
18
+ enumerable: !(desc = __getOwnPropDesc$9(from, key)) || desc.enumerable
19
19
  });
20
20
  }
21
21
  return to;
22
22
  };
23
- var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp$8(target, "default", {
23
+ var __toESM = (mod, isNodeMode, target) => (target = mod != null ? __create(__getProtoOf(mod)) : {}, __copyProps(isNodeMode || !mod || !mod.__esModule ? __defProp$9(target, "default", {
24
24
  value: mod,
25
25
  enumerable: true
26
26
  }) : target, mod));
27
27
  //#endregion
28
28
  //#region src/catalog.ts
29
- var __defProp$7 = Object.defineProperty;
30
- var __getOwnPropDesc$7 = Object.getOwnPropertyDescriptor;
31
- var __defNormalProp$7 = (obj, key, value) => key in obj ? __defProp$7(obj, key, {
29
+ var __defProp$8 = Object.defineProperty;
30
+ var __getOwnPropDesc$8 = Object.getOwnPropertyDescriptor;
31
+ var __defNormalProp$8 = (obj, key, value) => key in obj ? __defProp$8(obj, key, {
32
32
  enumerable: true,
33
33
  configurable: true,
34
34
  writable: true,
35
35
  value
36
36
  }) : obj[key] = value;
37
- var __decorateClass$7 = (decorators, target, key, kind) => {
38
- var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$7(target, key) : target;
37
+ var __decorateClass$8 = (decorators, target, key, kind) => {
38
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$8(target, key) : target;
39
39
  for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
40
- if (kind && result) __defProp$7(target, key, result);
40
+ if (kind && result) __defProp$8(target, key, result);
41
41
  return result;
42
42
  };
43
- var __publicField$7 = (obj, key, value) => __defNormalProp$7(obj, typeof key !== "symbol" ? key + "" : key, value);
44
- var closed$5 = {
43
+ var __publicField$8 = (obj, key, value) => __defNormalProp$8(obj, typeof key !== "symbol" ? key + "" : key, value);
44
+ var closed$6 = {
45
45
  api: false,
46
46
  mcp: false,
47
47
  cli: false
@@ -100,24 +100,24 @@ var Course = class extends SmrtObject {
100
100
  return await super.save(options);
101
101
  }
102
102
  };
103
- __publicField$7(Course, "__smrtPackage__", "@happyvertical/smrt-education");
104
- __decorateClass$7([tenantId({ nullable: true })], Course.prototype, "tenantId", 2);
105
- __decorateClass$7([field({ required: true })], Course.prototype, "title", 2);
106
- Course = __decorateClass$7([TenantScoped({ mode: "optional" }), smrt({
103
+ __publicField$8(Course, "__smrtPackage__", "@happyvertical/smrt-education");
104
+ __decorateClass$8([tenantId({ nullable: true })], Course.prototype, "tenantId", 2);
105
+ __decorateClass$8([field({ required: true })], Course.prototype, "title", 2);
106
+ Course = __decorateClass$8([TenantScoped({ mode: "optional" }), smrt({
107
107
  tableStrategy: "sti",
108
- ...closed$5
108
+ ...closed$6
109
109
  })], Course);
110
110
  var ExternalCourse = class extends Course {};
111
- __publicField$7(ExternalCourse, "__smrtPackage__", "@happyvertical/smrt-education");
112
- ExternalCourse = __decorateClass$7([smrt({
111
+ __publicField$8(ExternalCourse, "__smrtPackage__", "@happyvertical/smrt-education");
112
+ ExternalCourse = __decorateClass$8([smrt({
113
113
  tableName: "courses",
114
- ...closed$5
114
+ ...closed$6
115
115
  })], ExternalCourse);
116
116
  var AuthoredCourse = class extends Course {};
117
- __publicField$7(AuthoredCourse, "__smrtPackage__", "@happyvertical/smrt-education");
118
- AuthoredCourse = __decorateClass$7([smrt({
117
+ __publicField$8(AuthoredCourse, "__smrtPackage__", "@happyvertical/smrt-education");
118
+ AuthoredCourse = __decorateClass$8([smrt({
119
119
  tableName: "courses",
120
- ...closed$5
120
+ ...closed$6
121
121
  })], AuthoredCourse);
122
122
  var CourseSection = class extends SmrtObject {
123
123
  tenantId = null;
@@ -138,11 +138,11 @@ var CourseSection = class extends SmrtObject {
138
138
  return await super.save(options);
139
139
  }
140
140
  };
141
- __publicField$7(CourseSection, "__smrtPackage__", "@happyvertical/smrt-education");
142
- __decorateClass$7([tenantId({ nullable: true })], CourseSection.prototype, "tenantId", 2);
143
- __decorateClass$7([foreignKey("Course", { required: true })], CourseSection.prototype, "courseId", 2);
144
- __decorateClass$7([field({ required: true })], CourseSection.prototype, "title", 2);
145
- CourseSection = __decorateClass$7([TenantScoped({ mode: "optional" }), smrt({ ...closed$5 })], CourseSection);
141
+ __publicField$8(CourseSection, "__smrtPackage__", "@happyvertical/smrt-education");
142
+ __decorateClass$8([tenantId({ nullable: true })], CourseSection.prototype, "tenantId", 2);
143
+ __decorateClass$8([foreignKey("Course", { required: true })], CourseSection.prototype, "courseId", 2);
144
+ __decorateClass$8([field({ required: true })], CourseSection.prototype, "title", 2);
145
+ CourseSection = __decorateClass$8([TenantScoped({ mode: "optional" }), smrt({ ...closed$6 })], CourseSection);
146
146
  var Lesson = class extends SmrtObject {
147
147
  tenantId = null;
148
148
  sectionId = "";
@@ -169,19 +169,19 @@ var Lesson = class extends SmrtObject {
169
169
  return await super.save(options);
170
170
  }
171
171
  };
172
- __publicField$7(Lesson, "__smrtPackage__", "@happyvertical/smrt-education");
173
- __decorateClass$7([tenantId({ nullable: true })], Lesson.prototype, "tenantId", 2);
174
- __decorateClass$7([foreignKey("CourseSection", { required: true })], Lesson.prototype, "sectionId", 2);
175
- __decorateClass$7([field({ required: true })], Lesson.prototype, "title", 2);
176
- Lesson = __decorateClass$7([TenantScoped({ mode: "optional" }), smrt({
172
+ __publicField$8(Lesson, "__smrtPackage__", "@happyvertical/smrt-education");
173
+ __decorateClass$8([tenantId({ nullable: true })], Lesson.prototype, "tenantId", 2);
174
+ __decorateClass$8([foreignKey("CourseSection", { required: true })], Lesson.prototype, "sectionId", 2);
175
+ __decorateClass$8([field({ required: true })], Lesson.prototype, "title", 2);
176
+ Lesson = __decorateClass$8([TenantScoped({ mode: "optional" }), smrt({
177
177
  tableStrategy: "sti",
178
- ...closed$5
178
+ ...closed$6
179
179
  })], Lesson);
180
180
  var Reading = class extends Lesson {};
181
- __publicField$7(Reading, "__smrtPackage__", "@happyvertical/smrt-education");
182
- Reading = __decorateClass$7([smrt({
181
+ __publicField$8(Reading, "__smrtPackage__", "@happyvertical/smrt-education");
182
+ Reading = __decorateClass$8([smrt({
183
183
  tableName: "lessons",
184
- ...closed$5
184
+ ...closed$6
185
185
  })], Reading);
186
186
  var Video = class extends Lesson {
187
187
  videoUrl = "";
@@ -190,10 +190,10 @@ var Video = class extends Lesson {
190
190
  applyModelOptions(this, options);
191
191
  }
192
192
  };
193
- __publicField$7(Video, "__smrtPackage__", "@happyvertical/smrt-education");
194
- Video = __decorateClass$7([smrt({
193
+ __publicField$8(Video, "__smrtPackage__", "@happyvertical/smrt-education");
194
+ Video = __decorateClass$8([smrt({
195
195
  tableName: "lessons",
196
- ...closed$5
196
+ ...closed$6
197
197
  })], Video);
198
198
  var Worksheet = class extends Lesson {
199
199
  printableUrl = "";
@@ -205,16 +205,16 @@ var Worksheet = class extends Lesson {
205
205
  applyModelOptions(this, options);
206
206
  }
207
207
  };
208
- __publicField$7(Worksheet, "__smrtPackage__", "@happyvertical/smrt-education");
209
- Worksheet = __decorateClass$7([smrt({
208
+ __publicField$8(Worksheet, "__smrtPackage__", "@happyvertical/smrt-education");
209
+ Worksheet = __decorateClass$8([smrt({
210
210
  tableName: "lessons",
211
- ...closed$5
211
+ ...closed$6
212
212
  })], Worksheet);
213
213
  var Exercise = class extends Lesson {};
214
- __publicField$7(Exercise, "__smrtPackage__", "@happyvertical/smrt-education");
215
- Exercise = __decorateClass$7([smrt({
214
+ __publicField$8(Exercise, "__smrtPackage__", "@happyvertical/smrt-education");
215
+ Exercise = __decorateClass$8([smrt({
216
216
  tableName: "lessons",
217
- ...closed$5
217
+ ...closed$6
218
218
  })], Exercise);
219
219
  GlobalInterceptors.register({
220
220
  name: "education-catalog-identity",
@@ -246,22 +246,22 @@ GlobalInterceptors.register({
246
246
  });
247
247
  //#endregion
248
248
  //#region src/learners.ts
249
- var __defProp$6 = Object.defineProperty;
250
- var __getOwnPropDesc$6 = Object.getOwnPropertyDescriptor;
251
- var __defNormalProp$6 = (obj, key, value) => key in obj ? __defProp$6(obj, key, {
249
+ var __defProp$7 = Object.defineProperty;
250
+ var __getOwnPropDesc$7 = Object.getOwnPropertyDescriptor;
251
+ var __defNormalProp$7 = (obj, key, value) => key in obj ? __defProp$7(obj, key, {
252
252
  enumerable: true,
253
253
  configurable: true,
254
254
  writable: true,
255
255
  value
256
256
  }) : obj[key] = value;
257
- var __decorateClass$6 = (decorators, target, key, kind) => {
258
- var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$6(target, key) : target;
257
+ var __decorateClass$7 = (decorators, target, key, kind) => {
258
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$7(target, key) : target;
259
259
  for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
260
- if (kind && result) __defProp$6(target, key, result);
260
+ if (kind && result) __defProp$7(target, key, result);
261
261
  return result;
262
262
  };
263
- var __publicField$6 = (obj, key, value) => __defNormalProp$6(obj, typeof key !== "symbol" ? key + "" : key, value);
264
- var closed$4 = {
263
+ var __publicField$7 = (obj, key, value) => __defNormalProp$7(obj, typeof key !== "symbol" ? key + "" : key, value);
264
+ var closed$5 = {
265
265
  api: false,
266
266
  mcp: false,
267
267
  cli: false
@@ -326,11 +326,11 @@ var Learner = class extends SmrtObject {
326
326
  return super.save(options);
327
327
  }
328
328
  };
329
- __publicField$6(Learner, "__smrtPackage__", "@happyvertical/smrt-education");
330
- __decorateClass$6([tenantId()], Learner.prototype, "tenantId", 2);
331
- __decorateClass$6([crossPackageRef("@happyvertical/smrt-users:User", { required: true })], Learner.prototype, "userId", 2);
332
- __decorateClass$6([crossPackageRef("@happyvertical/smrt-profiles:Profile", { required: true })], Learner.prototype, "profileId", 2);
333
- Learner = __decorateClass$6([TenantScoped({ mode: "required" }), smrt({ ...closed$4 })], Learner);
329
+ __publicField$7(Learner, "__smrtPackage__", "@happyvertical/smrt-education");
330
+ __decorateClass$7([tenantId()], Learner.prototype, "tenantId", 2);
331
+ __decorateClass$7([crossPackageRef("@happyvertical/smrt-users:User", { required: true })], Learner.prototype, "userId", 2);
332
+ __decorateClass$7([crossPackageRef("@happyvertical/smrt-profiles:Profile", { required: true })], Learner.prototype, "profileId", 2);
333
+ Learner = __decorateClass$7([TenantScoped({ mode: "required" }), smrt({ ...closed$5 })], Learner);
334
334
  var Curriculum = class extends SmrtObject {
335
335
  tenantId = "";
336
336
  learnerId = "";
@@ -357,12 +357,12 @@ var Curriculum = class extends SmrtObject {
357
357
  return super.save(options);
358
358
  }
359
359
  };
360
- __publicField$6(Curriculum, "__smrtPackage__", "@happyvertical/smrt-education");
361
- __decorateClass$6([tenantId()], Curriculum.prototype, "tenantId", 2);
362
- __decorateClass$6([foreignKey("Learner", { required: true })], Curriculum.prototype, "learnerId", 2);
363
- __decorateClass$6([field({ required: true })], Curriculum.prototype, "title", 2);
364
- Curriculum = __decorateClass$6([TenantScoped({ mode: "required" }), smrt({
365
- ...closed$4,
360
+ __publicField$7(Curriculum, "__smrtPackage__", "@happyvertical/smrt-education");
361
+ __decorateClass$7([tenantId()], Curriculum.prototype, "tenantId", 2);
362
+ __decorateClass$7([foreignKey("Learner", { required: true })], Curriculum.prototype, "learnerId", 2);
363
+ __decorateClass$7([field({ required: true })], Curriculum.prototype, "title", 2);
364
+ Curriculum = __decorateClass$7([TenantScoped({ mode: "required" }), smrt({
365
+ ...closed$5,
366
366
  tableName: "curricula"
367
367
  })], Curriculum);
368
368
  var Enrollment = class extends SmrtObject {
@@ -401,13 +401,13 @@ var Enrollment = class extends SmrtObject {
401
401
  return super.save(options);
402
402
  }
403
403
  };
404
- __publicField$6(Enrollment, "__smrtPackage__", "@happyvertical/smrt-education");
405
- __decorateClass$6([tenantId()], Enrollment.prototype, "tenantId", 2);
406
- __decorateClass$6([foreignKey("Curriculum", { required: true })], Enrollment.prototype, "curriculumId", 2);
407
- __decorateClass$6([foreignKey("Learner", { required: true })], Enrollment.prototype, "learnerId", 2);
408
- __decorateClass$6([foreignKey("Course", { required: true })], Enrollment.prototype, "courseId", 2);
409
- Enrollment = __decorateClass$6([TenantScoped({ mode: "required" }), smrt({
410
- ...closed$4,
404
+ __publicField$7(Enrollment, "__smrtPackage__", "@happyvertical/smrt-education");
405
+ __decorateClass$7([tenantId()], Enrollment.prototype, "tenantId", 2);
406
+ __decorateClass$7([foreignKey("Curriculum", { required: true })], Enrollment.prototype, "curriculumId", 2);
407
+ __decorateClass$7([foreignKey("Learner", { required: true })], Enrollment.prototype, "learnerId", 2);
408
+ __decorateClass$7([foreignKey("Course", { required: true })], Enrollment.prototype, "courseId", 2);
409
+ Enrollment = __decorateClass$7([TenantScoped({ mode: "required" }), smrt({
410
+ ...closed$5,
411
411
  conflictColumns: [
412
412
  "tenant_id",
413
413
  "curriculum_id",
@@ -429,6 +429,23 @@ var LessonProgress = class extends SmrtObject {
429
429
  super(options);
430
430
  applyOptions$1(this, options);
431
431
  }
432
+ /**
433
+ * A pinned enrollment may follow a later release, whose lessons live on that
434
+ * release's own materialized Course row. Hosts that have not migrated the
435
+ * authoring tables keep the strict enrollment-course rule.
436
+ */
437
+ async inPinnedRelease(courseId) {
438
+ if (!await this.db.tableExists("enrollment_release_pins")) return false;
439
+ const pins = await this.db.list("enrollment_release_pins", {
440
+ tenant_id: this.tenantId,
441
+ enrollment_id: this.enrollmentId
442
+ });
443
+ for (const pin of pins) if ((await this.db.get("course_releases", {
444
+ id: pin.release_id,
445
+ tenant_id: this.tenantId
446
+ }))?.release_course_id === courseId) return true;
447
+ return false;
448
+ }
432
449
  async save(options) {
433
450
  await immutableIdentity$1(this, {
434
451
  enrollment_id: this.enrollmentId,
@@ -449,26 +466,24 @@ var LessonProgress = class extends SmrtObject {
449
466
  tenant_id: this.tenantId
450
467
  });
451
468
  const lesson = await this.db.get("lessons", { id: this.lessonId });
452
- if (!enrollment || !lesson || !await this.db.get("course_sections", {
453
- id: lesson.section_id,
454
- course_id: enrollment.course_id
455
- })) throw new Error("Progress requires its enrollment learner and course lesson");
469
+ const section = lesson ? await this.db.get("course_sections", { id: lesson.section_id }) : null;
470
+ if (!enrollment || !section || !(section.course_id === enrollment.course_id || await this.inPinnedRelease(String(section.course_id)))) throw new Error("Progress requires its enrollment learner and course lesson");
456
471
  for (const value of [this.attempts, this.timeSpentSeconds]) if (!Number.isSafeInteger(value) || value < 0) throw new Error("Progress counters must be nonnegative integers");
457
472
  if (this.bestScore !== null && (!Number.isFinite(this.bestScore) || this.bestScore < 0 || this.bestScore > 1)) throw new Error("Score must be null or a number from zero to one");
458
473
  return super.save(options);
459
474
  }
460
475
  };
461
- __publicField$6(LessonProgress, "__smrtPackage__", "@happyvertical/smrt-education");
462
- __decorateClass$6([tenantId()], LessonProgress.prototype, "tenantId", 2);
463
- __decorateClass$6([foreignKey("Enrollment", { required: true })], LessonProgress.prototype, "enrollmentId", 2);
464
- __decorateClass$6([foreignKey("Learner", { required: true })], LessonProgress.prototype, "learnerId", 2);
465
- __decorateClass$6([foreignKey("Lesson", { required: true })], LessonProgress.prototype, "lessonId", 2);
466
- __decorateClass$6([field({
476
+ __publicField$7(LessonProgress, "__smrtPackage__", "@happyvertical/smrt-education");
477
+ __decorateClass$7([tenantId()], LessonProgress.prototype, "tenantId", 2);
478
+ __decorateClass$7([foreignKey("Enrollment", { required: true })], LessonProgress.prototype, "enrollmentId", 2);
479
+ __decorateClass$7([foreignKey("Learner", { required: true })], LessonProgress.prototype, "learnerId", 2);
480
+ __decorateClass$7([foreignKey("Lesson", { required: true })], LessonProgress.prototype, "lessonId", 2);
481
+ __decorateClass$7([field({
467
482
  type: "decimal",
468
483
  nullable: true
469
484
  })], LessonProgress.prototype, "bestScore", 2);
470
- LessonProgress = __decorateClass$6([TenantScoped({ mode: "required" }), smrt({
471
- ...closed$4,
485
+ LessonProgress = __decorateClass$7([TenantScoped({ mode: "required" }), smrt({
486
+ ...closed$5,
472
487
  tableName: "lesson_progress",
473
488
  conflictColumns: [
474
489
  "tenant_id",
@@ -478,21 +493,21 @@ LessonProgress = __decorateClass$6([TenantScoped({ mode: "required" }), smrt({
478
493
  })], LessonProgress);
479
494
  //#endregion
480
495
  //#region src/lesson-assets.ts
481
- var __defProp$5 = Object.defineProperty;
482
- var __getOwnPropDesc$5 = Object.getOwnPropertyDescriptor;
483
- var __defNormalProp$5 = (obj, key, value) => key in obj ? __defProp$5(obj, key, {
496
+ var __defProp$6 = Object.defineProperty;
497
+ var __getOwnPropDesc$6 = Object.getOwnPropertyDescriptor;
498
+ var __defNormalProp$6 = (obj, key, value) => key in obj ? __defProp$6(obj, key, {
484
499
  enumerable: true,
485
500
  configurable: true,
486
501
  writable: true,
487
502
  value
488
503
  }) : obj[key] = value;
489
- var __decorateClass$5 = (decorators, target, key, kind) => {
490
- var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$5(target, key) : target;
504
+ var __decorateClass$6 = (decorators, target, key, kind) => {
505
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$6(target, key) : target;
491
506
  for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
492
- if (kind && result) __defProp$5(target, key, result);
507
+ if (kind && result) __defProp$6(target, key, result);
493
508
  return result;
494
509
  };
495
- var __publicField$5 = (obj, key, value) => __defNormalProp$5(obj, typeof key !== "symbol" ? key + "" : key, value);
510
+ var __publicField$6 = (obj, key, value) => __defNormalProp$6(obj, typeof key !== "symbol" ? key + "" : key, value);
496
511
  var LessonAsset = class extends SmrtObject {
497
512
  tenantId = null;
498
513
  lessonId = "";
@@ -526,12 +541,12 @@ var LessonAsset = class extends SmrtObject {
526
541
  return await super.save(options);
527
542
  }
528
543
  };
529
- __publicField$5(LessonAsset, "__smrtPackage__", "@happyvertical/smrt-education");
530
- __decorateClass$5([tenantId({ nullable: true })], LessonAsset.prototype, "tenantId", 2);
531
- __decorateClass$5([foreignKey("Lesson", { required: true })], LessonAsset.prototype, "lessonId", 2);
532
- __decorateClass$5([crossPackageRef("@happyvertical/smrt-assets:Asset", { required: true })], LessonAsset.prototype, "assetId", 2);
533
- __decorateClass$5([field({ required: true })], LessonAsset.prototype, "relationship", 2);
534
- LessonAsset = __decorateClass$5([TenantScoped({ mode: "optional" }), smrt({
544
+ __publicField$6(LessonAsset, "__smrtPackage__", "@happyvertical/smrt-education");
545
+ __decorateClass$6([tenantId({ nullable: true })], LessonAsset.prototype, "tenantId", 2);
546
+ __decorateClass$6([foreignKey("Lesson", { required: true })], LessonAsset.prototype, "lessonId", 2);
547
+ __decorateClass$6([crossPackageRef("@happyvertical/smrt-assets:Asset", { required: true })], LessonAsset.prototype, "assetId", 2);
548
+ __decorateClass$6([field({ required: true })], LessonAsset.prototype, "relationship", 2);
549
+ LessonAsset = __decorateClass$6([TenantScoped({ mode: "optional" }), smrt({
535
550
  tableName: "lesson_assets",
536
551
  conflictColumns: [
537
552
  "tenant_id",
@@ -544,23 +559,51 @@ LessonAsset = __decorateClass$5([TenantScoped({ mode: "optional" }), smrt({
544
559
  cli: false
545
560
  })], LessonAsset);
546
561
  //#endregion
562
+ //#region src/question-rubric.ts
563
+ var CRITERION_ID = /^[A-Za-z0-9._:-]{1,80}$/;
564
+ function parseQuestionRubric(value) {
565
+ const criteria = value?.criteria;
566
+ if (!Array.isArray(criteria) || !criteria.length || criteria.length > 50) throw new Error("Rubric requires between one and fifty criteria");
567
+ const seen = /* @__PURE__ */ new Set();
568
+ return { criteria: criteria.map((c) => {
569
+ if (!c || typeof c.id !== "string" || !CRITERION_ID.test(c.id) || seen.has(c.id) || typeof c.label !== "string" || !c.label.trim() || c.label.length > 500 || !Number.isFinite(c.maxPoints) || c.maxPoints <= 0) throw new Error("Invalid rubric criterion");
570
+ seen.add(c.id);
571
+ return {
572
+ id: c.id,
573
+ label: c.label,
574
+ maxPoints: c.maxPoints
575
+ };
576
+ }) };
577
+ }
578
+ function validateRubricResults(rubricJson, results) {
579
+ const entries = Object.entries(results);
580
+ if (!entries.length) return;
581
+ if (!rubricJson) throw new Error("Rubric results require a rubric on the question revision");
582
+ const rubric = parseQuestionRubric(JSON.parse(rubricJson));
583
+ const max = new Map(rubric.criteria.map((c) => [c.id, c.maxPoints]));
584
+ for (const [id, raw] of entries) {
585
+ const points = raw && typeof raw === "object" && !Array.isArray(raw) ? raw.points : raw;
586
+ if (!max.has(id) || typeof points !== "number" || !Number.isFinite(points) || points < 0 || points > max.get(id)) throw new Error("Rubric results do not match the question rubric");
587
+ }
588
+ }
589
+ //#endregion
547
590
  //#region src/submissions.ts
548
- var __defProp$4 = Object.defineProperty;
549
- var __getOwnPropDesc$4 = Object.getOwnPropertyDescriptor;
550
- var __defNormalProp$4 = (obj, key, value) => key in obj ? __defProp$4(obj, key, {
591
+ var __defProp$5 = Object.defineProperty;
592
+ var __getOwnPropDesc$5 = Object.getOwnPropertyDescriptor;
593
+ var __defNormalProp$5 = (obj, key, value) => key in obj ? __defProp$5(obj, key, {
551
594
  enumerable: true,
552
595
  configurable: true,
553
596
  writable: true,
554
597
  value
555
598
  }) : obj[key] = value;
556
- var __decorateClass$4 = (decorators, target, key, kind) => {
557
- var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$4(target, key) : target;
599
+ var __decorateClass$5 = (decorators, target, key, kind) => {
600
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$5(target, key) : target;
558
601
  for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
559
- if (kind && result) __defProp$4(target, key, result);
602
+ if (kind && result) __defProp$5(target, key, result);
560
603
  return result;
561
604
  };
562
- var __publicField$4 = (obj, key, value) => __defNormalProp$4(obj, typeof key !== "symbol" ? key + "" : key, value);
563
- var closed$3 = {
605
+ var __publicField$5 = (obj, key, value) => __defNormalProp$5(obj, typeof key !== "symbol" ? key + "" : key, value);
606
+ var closed$4 = {
564
607
  api: false,
565
608
  mcp: false,
566
609
  cli: false
@@ -650,20 +693,20 @@ var Submission = class extends SmrtObject {
650
693
  return super.save(options);
651
694
  }
652
695
  };
653
- __publicField$4(Submission, "__smrtPackage__", "@happyvertical/smrt-education");
654
- __decorateClass$4([tenantId()], Submission.prototype, "tenantId", 2);
655
- __decorateClass$4([foreignKey("Learner", { required: true })], Submission.prototype, "learnerId", 2);
656
- __decorateClass$4([foreignKey("Lesson", { required: true })], Submission.prototype, "lessonId", 2);
657
- __decorateClass$4([foreignKey("LessonProgress", { required: true })], Submission.prototype, "progressId", 2);
658
- __decorateClass$4([field({ required: true })], Submission.prototype, "kind", 2);
659
- __decorateClass$4([foreignKey("Submission", { nullable: true })], Submission.prototype, "previousSubmissionId", 2);
660
- __decorateClass$4([field({
696
+ __publicField$5(Submission, "__smrtPackage__", "@happyvertical/smrt-education");
697
+ __decorateClass$5([tenantId()], Submission.prototype, "tenantId", 2);
698
+ __decorateClass$5([foreignKey("Learner", { required: true })], Submission.prototype, "learnerId", 2);
699
+ __decorateClass$5([foreignKey("Lesson", { required: true })], Submission.prototype, "lessonId", 2);
700
+ __decorateClass$5([foreignKey("LessonProgress", { required: true })], Submission.prototype, "progressId", 2);
701
+ __decorateClass$5([field({ required: true })], Submission.prototype, "kind", 2);
702
+ __decorateClass$5([foreignKey("Submission", { nullable: true })], Submission.prototype, "previousSubmissionId", 2);
703
+ __decorateClass$5([field({
661
704
  type: "text",
662
705
  nullable: true
663
706
  })], Submission.prototype, "intakeItemId", 2);
664
- Submission = __decorateClass$4([TenantScoped({ mode: "required" }), smrt({
707
+ Submission = __decorateClass$5([TenantScoped({ mode: "required" }), smrt({
665
708
  tableStrategy: "sti",
666
- ...closed$3
709
+ ...closed$4
667
710
  })], Submission);
668
711
  var SubmissionIntakeBinding = class extends SmrtObject {
669
712
  tenantId = "";
@@ -684,16 +727,16 @@ var SubmissionIntakeBinding = class extends SmrtObject {
684
727
  return super.save(options);
685
728
  }
686
729
  };
687
- __publicField$4(SubmissionIntakeBinding, "__smrtPackage__", "@happyvertical/smrt-education");
688
- __decorateClass$4([tenantId()], SubmissionIntakeBinding.prototype, "tenantId", 2);
689
- __decorateClass$4([field({
730
+ __publicField$5(SubmissionIntakeBinding, "__smrtPackage__", "@happyvertical/smrt-education");
731
+ __decorateClass$5([tenantId()], SubmissionIntakeBinding.prototype, "tenantId", 2);
732
+ __decorateClass$5([field({
690
733
  required: true,
691
734
  type: "text"
692
735
  })], SubmissionIntakeBinding.prototype, "intakeItemId", 2);
693
- __decorateClass$4([foreignKey("Submission", { required: true })], SubmissionIntakeBinding.prototype, "submissionId", 2);
694
- SubmissionIntakeBinding = __decorateClass$4([TenantScoped({ mode: "required" }), smrt({
736
+ __decorateClass$5([foreignKey("Submission", { required: true })], SubmissionIntakeBinding.prototype, "submissionId", 2);
737
+ SubmissionIntakeBinding = __decorateClass$5([TenantScoped({ mode: "required" }), smrt({
695
738
  tableName: "submission_intake_bindings",
696
- ...closed$3,
739
+ ...closed$4,
697
740
  conflictColumns: ["tenant_id", "intake_item_id"]
698
741
  })], SubmissionIntakeBinding);
699
742
  var ScannedSubmission = class extends Submission {
@@ -715,10 +758,10 @@ var ScannedSubmission = class extends Submission {
715
758
  return super.save(options);
716
759
  }
717
760
  };
718
- __publicField$4(ScannedSubmission, "__smrtPackage__", "@happyvertical/smrt-education");
719
- ScannedSubmission = __decorateClass$4([smrt({
761
+ __publicField$5(ScannedSubmission, "__smrtPackage__", "@happyvertical/smrt-education");
762
+ ScannedSubmission = __decorateClass$5([smrt({
720
763
  tableName: "submissions",
721
- ...closed$3
764
+ ...closed$4
722
765
  })], ScannedSubmission);
723
766
  var ExternalCompletion = class extends Submission {
724
767
  reportedBy = "learner";
@@ -738,17 +781,17 @@ var ExternalCompletion = class extends Submission {
738
781
  return super.save(options);
739
782
  }
740
783
  };
741
- __publicField$4(ExternalCompletion, "__smrtPackage__", "@happyvertical/smrt-education");
742
- __decorateClass$4([field({ type: "decimal" })], ExternalCompletion.prototype, "reportedScore", 2);
743
- ExternalCompletion = __decorateClass$4([smrt({
784
+ __publicField$5(ExternalCompletion, "__smrtPackage__", "@happyvertical/smrt-education");
785
+ __decorateClass$5([field({ type: "decimal" })], ExternalCompletion.prototype, "reportedScore", 2);
786
+ ExternalCompletion = __decorateClass$5([smrt({
744
787
  tableName: "submissions",
745
- ...closed$3
788
+ ...closed$4
746
789
  })], ExternalCompletion);
747
790
  var Attestation = class extends Submission {};
748
- __publicField$4(Attestation, "__smrtPackage__", "@happyvertical/smrt-education");
749
- Attestation = __decorateClass$4([smrt({
791
+ __publicField$5(Attestation, "__smrtPackage__", "@happyvertical/smrt-education");
792
+ Attestation = __decorateClass$5([smrt({
750
793
  tableName: "submissions",
751
- ...closed$3
794
+ ...closed$4
752
795
  })], Attestation);
753
796
  var SubmissionAsset = class extends SmrtObject {
754
797
  tenantId = "";
@@ -785,12 +828,12 @@ var SubmissionAsset = class extends SmrtObject {
785
828
  return super.save(options);
786
829
  }
787
830
  };
788
- __publicField$4(SubmissionAsset, "__smrtPackage__", "@happyvertical/smrt-education");
789
- __decorateClass$4([tenantId()], SubmissionAsset.prototype, "tenantId", 2);
790
- __decorateClass$4([foreignKey("Submission", { required: true })], SubmissionAsset.prototype, "submissionId", 2);
791
- __decorateClass$4([crossPackageRef("@happyvertical/smrt-assets:Asset", { required: true })], SubmissionAsset.prototype, "assetId", 2);
792
- __decorateClass$4([field({ required: true })], SubmissionAsset.prototype, "relationship", 2);
793
- SubmissionAsset = __decorateClass$4([TenantScoped({ mode: "required" }), smrt({
831
+ __publicField$5(SubmissionAsset, "__smrtPackage__", "@happyvertical/smrt-education");
832
+ __decorateClass$5([tenantId()], SubmissionAsset.prototype, "tenantId", 2);
833
+ __decorateClass$5([foreignKey("Submission", { required: true })], SubmissionAsset.prototype, "submissionId", 2);
834
+ __decorateClass$5([crossPackageRef("@happyvertical/smrt-assets:Asset", { required: true })], SubmissionAsset.prototype, "assetId", 2);
835
+ __decorateClass$5([field({ required: true })], SubmissionAsset.prototype, "relationship", 2);
836
+ SubmissionAsset = __decorateClass$5([TenantScoped({ mode: "required" }), smrt({
794
837
  tableName: "submission_assets",
795
838
  conflictColumns: [
796
839
  "tenant_id",
@@ -798,7 +841,7 @@ SubmissionAsset = __decorateClass$4([TenantScoped({ mode: "required" }), smrt({
798
841
  "asset_id",
799
842
  "relationship"
800
843
  ],
801
- ...closed$3
844
+ ...closed$4
802
845
  })], SubmissionAsset);
803
846
  var Grade = class extends SmrtObject {
804
847
  tenantId = "";
@@ -809,6 +852,7 @@ var Grade = class extends SmrtObject {
809
852
  pointsEarned = null;
810
853
  pointsPossible = null;
811
854
  rubricResults = "{}";
855
+ questionRevisionId = null;
812
856
  feedback = "";
813
857
  gradedAt = /* @__PURE__ */ new Date();
814
858
  constructor(options = {}) {
@@ -836,27 +880,39 @@ var Grade = class extends SmrtObject {
836
880
  requireScore(this.score, "Score");
837
881
  for (const [value, label] of [[this.pointsEarned, "Points earned"], [this.pointsPossible, "Points possible"]]) if (value !== null) requireNonnegativeInteger(value, label);
838
882
  if (this.pointsEarned !== null && this.pointsPossible !== null && this.pointsEarned > this.pointsPossible) throw new Error("Points earned cannot exceed points possible");
883
+ if (this.questionRevisionId !== null) {
884
+ const revision = await this.db.get("question_revisions", {
885
+ id: this.questionRevisionId,
886
+ tenant_id: this.tenantId
887
+ });
888
+ if (!revision) throw new Error("Grade requires its tenant question revision");
889
+ validateRubricResults(revision.rubric_json, this.getRubricResults());
890
+ }
839
891
  return super.save(options);
840
892
  }
841
893
  };
842
- __publicField$4(Grade, "__smrtPackage__", "@happyvertical/smrt-education");
843
- __decorateClass$4([tenantId()], Grade.prototype, "tenantId", 2);
844
- __decorateClass$4([foreignKey("Submission", { required: true })], Grade.prototype, "submissionId", 2);
845
- __decorateClass$4([field({ required: true })], Grade.prototype, "graderKind", 2);
846
- __decorateClass$4([crossPackageRef("@happyvertical/smrt-profiles:Profile", { nullable: true })], Grade.prototype, "graderProfileId", 2);
847
- __decorateClass$4([field({
894
+ __publicField$5(Grade, "__smrtPackage__", "@happyvertical/smrt-education");
895
+ __decorateClass$5([tenantId()], Grade.prototype, "tenantId", 2);
896
+ __decorateClass$5([foreignKey("Submission", { required: true })], Grade.prototype, "submissionId", 2);
897
+ __decorateClass$5([field({ required: true })], Grade.prototype, "graderKind", 2);
898
+ __decorateClass$5([crossPackageRef("@happyvertical/smrt-profiles:Profile", { nullable: true })], Grade.prototype, "graderProfileId", 2);
899
+ __decorateClass$5([field({
848
900
  type: "decimal",
849
901
  nullable: true
850
902
  })], Grade.prototype, "score", 2);
851
- __decorateClass$4([field({
903
+ __decorateClass$5([field({
852
904
  type: "integer",
853
905
  nullable: true
854
906
  })], Grade.prototype, "pointsEarned", 2);
855
- __decorateClass$4([field({
907
+ __decorateClass$5([field({
856
908
  type: "integer",
857
909
  nullable: true
858
910
  })], Grade.prototype, "pointsPossible", 2);
859
- Grade = __decorateClass$4([TenantScoped({ mode: "required" }), smrt({ ...closed$3 })], Grade);
911
+ __decorateClass$5([field({
912
+ type: "text",
913
+ nullable: true
914
+ })], Grade.prototype, "questionRevisionId", 2);
915
+ Grade = __decorateClass$5([TenantScoped({ mode: "required" }), smrt({ ...closed$4 })], Grade);
860
916
  //#endregion
861
917
  //#region src/collections.ts
862
918
  var CourseCollection = class extends SmrtCollection {
@@ -915,143 +971,1051 @@ var ScannedSubmissionCollection = class extends SmrtCollection {
915
971
  var ExternalCompletionCollection = class extends SmrtCollection {
916
972
  static _itemClass = ExternalCompletion;
917
973
  };
918
- var AttestationCollection = class extends SmrtCollection {
919
- static _itemClass = Attestation;
974
+ var AttestationCollection = class extends SmrtCollection {
975
+ static _itemClass = Attestation;
976
+ };
977
+ var GradeCollection = class extends SmrtCollection {
978
+ static _itemClass = Grade;
979
+ };
980
+ var SubmissionAssetCollection = class extends SmrtJunction {
981
+ static _itemClass = SubmissionAsset;
982
+ leftField = "submissionId";
983
+ rightField = "assetId";
984
+ };
985
+ //#endregion
986
+ //#region src/catalog-service.ts
987
+ var CatalogAuthorizationError = class extends Error {
988
+ constructor() {
989
+ super("Education catalog access is not authorized.");
990
+ this.name = "CatalogAuthorizationError";
991
+ }
992
+ };
993
+ function projectLesson(lesson, includeAnswerKeys) {
994
+ if (includeAnswerKeys || !("answerUrl" in lesson)) return lesson;
995
+ return Object.assign(Object.create(Object.getPrototypeOf(lesson)), lesson, { answerUrl: "" });
996
+ }
997
+ var CatalogService = class {
998
+ constructor(courses, sections, lessons, assets, tenants, authorize) {
999
+ this.courses = courses;
1000
+ this.sections = sections;
1001
+ this.lessons = lessons;
1002
+ this.assets = assets;
1003
+ this.tenants = tenants;
1004
+ this.authorize = authorize;
1005
+ }
1006
+ courses;
1007
+ sections;
1008
+ lessons;
1009
+ assets;
1010
+ tenants;
1011
+ authorize;
1012
+ executor() {
1013
+ const executor = this.courses.db;
1014
+ if (!executor || [
1015
+ this.sections,
1016
+ this.lessons,
1017
+ this.assets
1018
+ ].some((collection) => collection.db !== executor)) throw new CatalogAuthorizationError();
1019
+ return executor;
1020
+ }
1021
+ authorizeInScope(request) {
1022
+ return this.authorize({
1023
+ ...request,
1024
+ executor: this.executor()
1025
+ });
1026
+ }
1027
+ async findVisible(request) {
1028
+ const { tenantId, actorUserId, includePrivate = false, options = {} } = request;
1029
+ if (!tenantId.trim() || !actorUserId.trim()) throw new CatalogAuthorizationError();
1030
+ const actor = {
1031
+ actorTenantId: tenantId,
1032
+ actorUserId,
1033
+ rootTenantId: tenantId
1034
+ };
1035
+ if (await this.authorizeInScope({
1036
+ ...actor,
1037
+ action: "education.catalog.read"
1038
+ }) !== true) throw new CatalogAuthorizationError();
1039
+ if (includePrivate && await this.authorizeInScope({
1040
+ ...actor,
1041
+ action: "education.catalog.read-private"
1042
+ }) !== true) throw new CatalogAuthorizationError();
1043
+ return this.tenants.withTenantCatalogReadScope({
1044
+ collections: [
1045
+ "@happyvertical/smrt-education:Course",
1046
+ "@happyvertical/smrt-education:CourseSection",
1047
+ "@happyvertical/smrt-education:Lesson",
1048
+ "@happyvertical/smrt-education:LessonAsset"
1049
+ ],
1050
+ authorize: async (scope2) => scope2.mode === "catalog" && scope2.actorTenantId === tenantId && scope2.actorUserId === actorUserId && scope2.rootTenantId === tenantId
1051
+ }, async () => this.courses.list({
1052
+ ...options,
1053
+ where: [
1054
+ [{ "licenseKind !=": "personal-use-copy" }],
1055
+ [{ licenseKind: null }],
1056
+ ...includePrivate ? [[{
1057
+ licenseKind: "personal-use-copy",
1058
+ tenantId
1059
+ }]] : []
1060
+ ]
1061
+ }));
1062
+ }
1063
+ /** Project one already-visible course without exposing answer-key assets by default. */
1064
+ async projectCourse(request) {
1065
+ const course = (await this.findVisible(request)).find((item) => item.id === request.courseId);
1066
+ if (!course?.id) return null;
1067
+ const includeAnswerKeys = request.includeAnswerKeys === true;
1068
+ const actor = {
1069
+ actorTenantId: request.tenantId,
1070
+ actorUserId: request.actorUserId,
1071
+ rootTenantId: request.tenantId
1072
+ };
1073
+ if (includeAnswerKeys && await this.authorizeInScope({
1074
+ ...actor,
1075
+ action: "education.catalog.answer-key.read"
1076
+ }) !== true) throw new CatalogAuthorizationError();
1077
+ return this.tenants.withTenantCatalogReadScope({
1078
+ collections: [
1079
+ "@happyvertical/smrt-education:CourseSection",
1080
+ "@happyvertical/smrt-education:Lesson",
1081
+ "@happyvertical/smrt-education:LessonAsset"
1082
+ ],
1083
+ authorize: async (scope) => scope.mode === "catalog" && scope.actorTenantId === request.tenantId && scope.actorUserId === request.actorUserId
1084
+ }, async () => {
1085
+ const sections = await this.sections.list({
1086
+ where: { courseId: course.id },
1087
+ orderBy: ["sortOrder ASC", "id ASC"]
1088
+ });
1089
+ const result = [];
1090
+ for (const section of sections) {
1091
+ if (section.tenantId !== course.tenantId) continue;
1092
+ const lessons = await this.lessons.list({
1093
+ where: { sectionId: section.id },
1094
+ orderBy: ["sortOrder ASC", "id ASC"]
1095
+ });
1096
+ result.push({
1097
+ section,
1098
+ lessons: await Promise.all(lessons.filter((lesson) => lesson.tenantId === course.tenantId).map(async (lesson) => ({
1099
+ lesson: projectLesson(lesson, includeAnswerKeys),
1100
+ assets: (await this.assets.byLeft(String(lesson.id))).filter((asset) => asset.tenantId === course.tenantId && (includeAnswerKeys || asset.relationship !== "answer-key"))
1101
+ })))
1102
+ });
1103
+ }
1104
+ return {
1105
+ course,
1106
+ sections: result
1107
+ };
1108
+ });
1109
+ }
1110
+ };
1111
+ //#endregion
1112
+ //#region src/course-authoring-contracts.ts
1113
+ var COURSE_AUTHORING_ACTIONS = [
1114
+ "education.course.read-draft",
1115
+ "education.course.author",
1116
+ "education.course.generate",
1117
+ "education.course.review",
1118
+ "education.course.release",
1119
+ "education.course.assign",
1120
+ "education.course.archive"
1121
+ ];
1122
+ var COURSE_PROVENANCES = [
1123
+ "supplied",
1124
+ "generated",
1125
+ "edited",
1126
+ "extracted"
1127
+ ];
1128
+ var MAX_PAGE_SIZE = 100;
1129
+ var DEFAULT_PAGE_SIZE = 50;
1130
+ var CourseAuthoringError = class extends Error {
1131
+ constructor(code, message) {
1132
+ super(message);
1133
+ this.code = code;
1134
+ this.name = new.target.name;
1135
+ }
1136
+ code;
1137
+ };
1138
+ var CourseAuthoringAuthorizationError = class extends CourseAuthoringError {
1139
+ constructor() {
1140
+ super("not-authorized", "Course authoring access is not authorized.");
1141
+ }
1142
+ };
1143
+ var CourseAuthoringValidationError = class extends CourseAuthoringError {
1144
+ constructor(message) {
1145
+ super("invalid-request", message);
1146
+ }
1147
+ };
1148
+ var CourseNotFoundError = class extends CourseAuthoringError {
1149
+ constructor() {
1150
+ super("course-not-found", "Course was not found.");
1151
+ }
1152
+ };
1153
+ var CourseDiscardedError = class extends CourseAuthoringError {
1154
+ constructor() {
1155
+ super("course-discarded", "Course draft was discarded.");
1156
+ }
1157
+ };
1158
+ var CourseArchivedError = class extends CourseAuthoringError {
1159
+ constructor() {
1160
+ super("course-archived", "Course is archived; unarchive it first.");
1161
+ }
1162
+ };
1163
+ var CourseStaleHeadError = class extends CourseAuthoringError {
1164
+ constructor(currentHeadRevisionId) {
1165
+ super("stale-head", "The draft head moved; reload and retry.");
1166
+ this.currentHeadRevisionId = currentHeadRevisionId;
1167
+ }
1168
+ currentHeadRevisionId;
1169
+ };
1170
+ var CoursePolicyBasisMismatchError = class extends CourseAuthoringError {
1171
+ constructor() {
1172
+ super("policy-basis-mismatch", "The supplied policy basis differs from the one the authorizer relied on.");
1173
+ }
1174
+ };
1175
+ var CourseRequestKeyConflictError = class extends CourseAuthoringError {
1176
+ constructor() {
1177
+ super("request-key-conflict", "The request key was already used for a different request.");
1178
+ }
1179
+ };
1180
+ var ReviewGateRequiredError = class extends CourseAuthoringError {
1181
+ constructor() {
1182
+ super("review-gate-required", "Release requires a configured review gate; none is configured.");
1183
+ }
1184
+ };
1185
+ var ReviewNotCurrentError = class extends CourseAuthoringError {
1186
+ constructor(reason) {
1187
+ super("review-not-current", "Release requires a current review of this exact revision.");
1188
+ this.reason = reason;
1189
+ }
1190
+ reason;
1191
+ };
1192
+ var CourseRevisionAlreadyReleasedError = class extends CourseAuthoringError {
1193
+ constructor(releaseId) {
1194
+ super("revision-already-released", "This revision is already released.");
1195
+ this.releaseId = releaseId;
1196
+ }
1197
+ releaseId;
1198
+ };
1199
+ var CourseDiscardRefusedError = class extends CourseAuthoringError {
1200
+ constructor(references) {
1201
+ super("discard-refused", "Only drafts with no release, enrollment, attempt, review or variant can be discarded.");
1202
+ this.references = references;
1203
+ }
1204
+ references;
1205
+ };
1206
+ var EnrollmentPinConflictError = class extends CourseAuthoringError {
1207
+ constructor(message = "Enrollment pin request conflicts with current state.") {
1208
+ super("enrollment-pin-conflict", message);
1209
+ }
1210
+ };
1211
+ var CourseMaterialCopyError = class extends CourseAuthoringError {
1212
+ constructor(message = "Course material could not be copied to an owned asset.") {
1213
+ super("material-copy-failed", message);
1214
+ }
1215
+ };
1216
+ //#endregion
1217
+ //#region src/course-authoring-models.ts
1218
+ var __defProp$4 = Object.defineProperty;
1219
+ var __getOwnPropDesc$4 = Object.getOwnPropertyDescriptor;
1220
+ var __defNormalProp$4 = (obj, key, value) => key in obj ? __defProp$4(obj, key, {
1221
+ enumerable: true,
1222
+ configurable: true,
1223
+ writable: true,
1224
+ value
1225
+ }) : obj[key] = value;
1226
+ var __decorateClass$4 = (decorators, target, key, kind) => {
1227
+ var result = kind > 1 ? void 0 : kind ? __getOwnPropDesc$4(target, key) : target;
1228
+ for (var i = decorators.length - 1, decorator; i >= 0; i--) if (decorator = decorators[i]) result = (kind ? decorator(target, key, result) : decorator(result)) || result;
1229
+ if (kind && result) __defProp$4(target, key, result);
1230
+ return result;
1231
+ };
1232
+ var __publicField$4 = (obj, key, value) => __defNormalProp$4(obj, typeof key !== "symbol" ? key + "" : key, value);
1233
+ var closed$3 = {
1234
+ api: false,
1235
+ mcp: false,
1236
+ cli: false
1237
+ };
1238
+ var LICENSES = [
1239
+ "own-work",
1240
+ "open-license",
1241
+ "personal-use-copy",
1242
+ "licensed-for-sharing"
1243
+ ];
1244
+ var PROVENANCES = [
1245
+ "supplied",
1246
+ "generated",
1247
+ "edited",
1248
+ "extracted"
1249
+ ];
1250
+ function adopt(target, options) {
1251
+ for (const [key, value] of Object.entries(options)) if (Object.hasOwn(target, key)) target[key] = value;
1252
+ }
1253
+ function requireText(value, message, max = 200) {
1254
+ if (typeof value !== "string" || !value.trim() || value.length > max) throw new Error(message);
1255
+ }
1256
+ function requireJson(value, shape, what) {
1257
+ let parsed;
1258
+ try {
1259
+ parsed = JSON.parse(value);
1260
+ } catch {
1261
+ throw new Error(`${what} must be JSON`);
1262
+ }
1263
+ if (shape === "array" ? !Array.isArray(parsed) : !parsed || typeof parsed !== "object" || Array.isArray(parsed)) throw new Error(`${what} must be a JSON ${shape}`);
1264
+ }
1265
+ async function requireInsert(row) {
1266
+ if (!row.tenantId?.trim()) throw new Error("Course authoring requires a tenant");
1267
+ if (row.id && await row.db.get(row.tableName, { id: row.id })) throw new Error("Course authoring history is immutable");
1268
+ }
1269
+ async function requireOwned(row, table, id, extra = {}) {
1270
+ if (!await row.db.get(table, {
1271
+ id,
1272
+ tenant_id: row.tenantId,
1273
+ ...extra
1274
+ })) throw new Error("Course authoring parent ownership mismatch");
1275
+ }
1276
+ function noDelete$1() {
1277
+ throw new Error("Authored course history is immutable; erasure requires an owning retention operation");
1278
+ }
1279
+ function validDate(value) {
1280
+ if (!(value instanceof Date) || !Number.isFinite(value.getTime())) throw new Error("Invalid course authoring timestamp");
1281
+ }
1282
+ var CourseRevision = class extends SmrtObject {
1283
+ tenantId = "";
1284
+ courseId = "";
1285
+ parentRevisionId = null;
1286
+ sequence = 1;
1287
+ authorUserId = "";
1288
+ delegatingGuardianUserId = "";
1289
+ delegationId = "";
1290
+ provenance = "supplied";
1291
+ fieldProvenanceJson = "{}";
1292
+ title = "";
1293
+ summary = "";
1294
+ subject = "";
1295
+ theme = "";
1296
+ levelMin = 0;
1297
+ levelMax = 0;
1298
+ objectivesJson = "[]";
1299
+ sectionsJson = "[]";
1300
+ licenseKind = "own-work";
1301
+ requestKey = "";
1302
+ payloadDigest = "";
1303
+ contentDigest = "";
1304
+ recordedAt = /* @__PURE__ */ new Date();
1305
+ constructor(options = {}) {
1306
+ super(options);
1307
+ adopt(this, options);
1308
+ }
1309
+ async delete() {
1310
+ noDelete$1();
1311
+ }
1312
+ async save(options) {
1313
+ await requireInsert(this);
1314
+ await requireOwned(this, "courses", this.courseId);
1315
+ if (this.parentRevisionId) await requireOwned(this, "course_revisions", this.parentRevisionId, { course_id: this.courseId });
1316
+ requireText(this.authorUserId, "Revision requires an author");
1317
+ requireText(this.title, "Revision requires a title");
1318
+ requireText(this.requestKey, "Revision requires a request key");
1319
+ if (!PROVENANCES.includes(this.provenance) || !LICENSES.includes(this.licenseKind) || !Number.isSafeInteger(this.sequence) || this.sequence < 1 || !Number.isSafeInteger(this.levelMin) || !Number.isSafeInteger(this.levelMax) || this.levelMin < 0 || this.levelMax < this.levelMin) throw new Error("Invalid course revision");
1320
+ requireJson(this.fieldProvenanceJson, "object", "Field provenance");
1321
+ requireJson(this.objectivesJson, "array", "Objectives");
1322
+ requireJson(this.sectionsJson, "array", "Sections");
1323
+ validDate(this.recordedAt);
1324
+ this.requireInsertOnSave();
1325
+ return super.save(options);
1326
+ }
1327
+ };
1328
+ __publicField$4(CourseRevision, "__smrtPackage__", "@happyvertical/smrt-education");
1329
+ __decorateClass$4([tenantId()], CourseRevision.prototype, "tenantId", 2);
1330
+ __decorateClass$4([foreignKey("Course", { required: true })], CourseRevision.prototype, "courseId", 2);
1331
+ __decorateClass$4([foreignKey("CourseRevision", { nullable: true })], CourseRevision.prototype, "parentRevisionId", 2);
1332
+ __decorateClass$4([field({ type: "text" })], CourseRevision.prototype, "fieldProvenanceJson", 2);
1333
+ __decorateClass$4([field({ type: "text" })], CourseRevision.prototype, "summary", 2);
1334
+ __decorateClass$4([field({ type: "text" })], CourseRevision.prototype, "objectivesJson", 2);
1335
+ __decorateClass$4([field({ type: "text" })], CourseRevision.prototype, "sectionsJson", 2);
1336
+ CourseRevision = __decorateClass$4([TenantScoped({ mode: "required" }), smrt({
1337
+ tableName: "course_revisions",
1338
+ conflictColumns: [
1339
+ "tenant_id",
1340
+ "course_id",
1341
+ "request_key"
1342
+ ],
1343
+ ...closed$3
1344
+ })], CourseRevision);
1345
+ var CourseRevisionCollection = class extends SmrtCollection {
1346
+ static _itemClass = CourseRevision;
1347
+ };
1348
+ var LessonRevision = class extends SmrtObject {
1349
+ tenantId = "";
1350
+ courseId = "";
1351
+ lessonKey = "";
1352
+ parentRevisionId = null;
1353
+ title = "";
1354
+ body = "";
1355
+ lessonType = "reading";
1356
+ completionRule = "attest";
1357
+ estimatedMinutes = 0;
1358
+ difficulty = 0;
1359
+ modality = "online";
1360
+ externalRef = "";
1361
+ externalUrl = "";
1362
+ licenseKind = "own-work";
1363
+ /** The lesson's own licence before materials; sticky across revisions of a lessonKey. */
1364
+ ownLicenseKind = "own-work";
1365
+ materialsJson = "[]";
1366
+ questionsJson = "[]";
1367
+ layoutParamsJson = "{}";
1368
+ provenance = "supplied";
1369
+ fieldProvenanceJson = "{}";
1370
+ authorUserId = "";
1371
+ delegatingGuardianUserId = "";
1372
+ delegationId = "";
1373
+ contentDigest = "";
1374
+ recordedAt = /* @__PURE__ */ new Date();
1375
+ constructor(options = {}) {
1376
+ super(options);
1377
+ adopt(this, options);
1378
+ }
1379
+ async delete() {
1380
+ noDelete$1();
1381
+ }
1382
+ async save(options) {
1383
+ await requireInsert(this);
1384
+ await requireOwned(this, "courses", this.courseId);
1385
+ if (this.parentRevisionId) await requireOwned(this, "lesson_revisions", this.parentRevisionId, {
1386
+ course_id: this.courseId,
1387
+ lesson_key: this.lessonKey
1388
+ });
1389
+ requireText(this.lessonKey, "Lesson revision requires a lesson key");
1390
+ requireText(this.title, "Lesson revision requires a title");
1391
+ requireText(this.authorUserId, "Lesson revision requires an author");
1392
+ if (![
1393
+ "reading",
1394
+ "video",
1395
+ "worksheet",
1396
+ "exercise"
1397
+ ].includes(this.lessonType) || ![
1398
+ "attest",
1399
+ "submit",
1400
+ "score",
1401
+ "external"
1402
+ ].includes(this.completionRule) || !PROVENANCES.includes(this.provenance) || !LICENSES.includes(this.licenseKind) || !Number.isSafeInteger(this.estimatedMinutes) || this.estimatedMinutes < 0 || !Number.isFinite(this.difficulty)) throw new Error("Invalid lesson revision");
1403
+ requireJson(this.materialsJson, "array", "Materials");
1404
+ requireJson(this.questionsJson, "array", "Lesson questions");
1405
+ requireJson(this.layoutParamsJson, "object", "Layout params");
1406
+ for (const ref of JSON.parse(this.questionsJson)) {
1407
+ if (typeof ref?.questionRevisionId !== "string") throw new Error("Lesson question reference is invalid");
1408
+ await requireOwned(this, "question_revisions", ref.questionRevisionId);
1409
+ }
1410
+ requireJson(this.fieldProvenanceJson, "object", "Field provenance");
1411
+ validDate(this.recordedAt);
1412
+ this.requireInsertOnSave();
1413
+ return super.save(options);
1414
+ }
1415
+ };
1416
+ __publicField$4(LessonRevision, "__smrtPackage__", "@happyvertical/smrt-education");
1417
+ __decorateClass$4([tenantId()], LessonRevision.prototype, "tenantId", 2);
1418
+ __decorateClass$4([foreignKey("Course", { required: true })], LessonRevision.prototype, "courseId", 2);
1419
+ __decorateClass$4([foreignKey("LessonRevision", { nullable: true })], LessonRevision.prototype, "parentRevisionId", 2);
1420
+ __decorateClass$4([field({ type: "text" })], LessonRevision.prototype, "body", 2);
1421
+ __decorateClass$4([field({ type: "text" })], LessonRevision.prototype, "materialsJson", 2);
1422
+ __decorateClass$4([field({ type: "text" })], LessonRevision.prototype, "questionsJson", 2);
1423
+ __decorateClass$4([field({ type: "text" })], LessonRevision.prototype, "layoutParamsJson", 2);
1424
+ __decorateClass$4([field({ type: "text" })], LessonRevision.prototype, "fieldProvenanceJson", 2);
1425
+ LessonRevision = __decorateClass$4([TenantScoped({ mode: "required" }), smrt({
1426
+ tableName: "lesson_revisions",
1427
+ conflictColumns: [
1428
+ "tenant_id",
1429
+ "course_id",
1430
+ "lesson_key",
1431
+ "content_digest"
1432
+ ],
1433
+ ...closed$3
1434
+ })], LessonRevision);
1435
+ var LessonRevisionCollection = class extends SmrtCollection {
1436
+ static _itemClass = LessonRevision;
1437
+ };
1438
+ var CourseDraftHead = class extends SmrtObject {
1439
+ tenantId = "";
1440
+ courseId = "";
1441
+ headRevisionId = "";
1442
+ version = 1;
1443
+ updatedByUserId = "";
1444
+ updatedAt = /* @__PURE__ */ new Date();
1445
+ constructor(options = {}) {
1446
+ super(options);
1447
+ adopt(this, options);
1448
+ }
1449
+ async delete() {
1450
+ noDelete$1();
1451
+ }
1452
+ async save(options) {
1453
+ if (!this.tenantId?.trim()) throw new Error("Course head requires a tenant");
1454
+ await requireOwned(this, "courses", this.courseId);
1455
+ await requireOwned(this, "course_revisions", this.headRevisionId, { course_id: this.courseId });
1456
+ if (this.id && await this.db.get(this.tableName, { id: this.id }) && !await this.db.get(this.tableName, {
1457
+ id: this.id,
1458
+ tenant_id: this.tenantId,
1459
+ course_id: this.courseId
1460
+ })) throw new Error("Course head identity cannot change");
1461
+ if (!Number.isSafeInteger(this.version) || this.version < 1) throw new Error("Invalid course head version");
1462
+ requireText(this.updatedByUserId, "Course head requires an actor");
1463
+ return super.save(options);
1464
+ }
1465
+ };
1466
+ __publicField$4(CourseDraftHead, "__smrtPackage__", "@happyvertical/smrt-education");
1467
+ __decorateClass$4([tenantId()], CourseDraftHead.prototype, "tenantId", 2);
1468
+ __decorateClass$4([foreignKey("Course", { required: true })], CourseDraftHead.prototype, "courseId", 2);
1469
+ __decorateClass$4([foreignKey("CourseRevision", { required: true })], CourseDraftHead.prototype, "headRevisionId", 2);
1470
+ CourseDraftHead = __decorateClass$4([TenantScoped({ mode: "required" }), smrt({
1471
+ tableName: "course_draft_heads",
1472
+ conflictColumns: ["tenant_id", "course_id"],
1473
+ ...closed$3
1474
+ })], CourseDraftHead);
1475
+ var CourseDraftHeadCollection = class extends SmrtCollection {
1476
+ static _itemClass = CourseDraftHead;
1477
+ };
1478
+ var CourseRelease = class extends SmrtObject {
1479
+ tenantId = "";
1480
+ courseId = "";
1481
+ revisionId = "";
1482
+ releaseCourseId = "";
1483
+ releaseNumber = 1;
1484
+ reviewId = "";
1485
+ actorUserId = "";
1486
+ delegatingGuardianUserId = "";
1487
+ delegationId = "";
1488
+ requestKey = "";
1489
+ recordedAt = /* @__PURE__ */ new Date();
1490
+ constructor(options = {}) {
1491
+ super(options);
1492
+ adopt(this, options);
1493
+ }
1494
+ async delete() {
1495
+ noDelete$1();
1496
+ }
1497
+ async save(options) {
1498
+ await requireInsert(this);
1499
+ await requireOwned(this, "courses", this.courseId);
1500
+ await requireOwned(this, "courses", this.releaseCourseId);
1501
+ await requireOwned(this, "course_revisions", this.revisionId, { course_id: this.courseId });
1502
+ requireText(this.actorUserId, "Release requires an actor");
1503
+ requireText(this.requestKey, "Release requires a request key");
1504
+ if (!Number.isSafeInteger(this.releaseNumber) || this.releaseNumber < 1) throw new Error("Invalid release number");
1505
+ validDate(this.recordedAt);
1506
+ this.requireInsertOnSave();
1507
+ return super.save(options);
1508
+ }
1509
+ };
1510
+ __publicField$4(CourseRelease, "__smrtPackage__", "@happyvertical/smrt-education");
1511
+ __decorateClass$4([tenantId()], CourseRelease.prototype, "tenantId", 2);
1512
+ __decorateClass$4([foreignKey("Course", { required: true })], CourseRelease.prototype, "courseId", 2);
1513
+ __decorateClass$4([foreignKey("CourseRevision", { required: true })], CourseRelease.prototype, "revisionId", 2);
1514
+ __decorateClass$4([foreignKey("Course", { required: true })], CourseRelease.prototype, "releaseCourseId", 2);
1515
+ CourseRelease = __decorateClass$4([TenantScoped({ mode: "required" }), smrt({
1516
+ tableName: "course_releases",
1517
+ conflictColumns: [
1518
+ "tenant_id",
1519
+ "course_id",
1520
+ "request_key"
1521
+ ],
1522
+ ...closed$3
1523
+ })], CourseRelease);
1524
+ var CourseReleaseCollection = class extends SmrtCollection {
1525
+ static _itemClass = CourseRelease;
1526
+ };
1527
+ var ReleasedLesson = class extends SmrtObject {
1528
+ tenantId = "";
1529
+ courseId = "";
1530
+ releaseId = "";
1531
+ lessonKey = "";
1532
+ lessonRevisionId = "";
1533
+ lessonId = "";
1534
+ sectionId = "";
1535
+ constructor(options = {}) {
1536
+ super(options);
1537
+ adopt(this, options);
1538
+ }
1539
+ async delete() {
1540
+ noDelete$1();
1541
+ }
1542
+ async save(options) {
1543
+ await requireInsert(this);
1544
+ await requireOwned(this, "course_releases", this.releaseId, { course_id: this.courseId });
1545
+ await requireOwned(this, "lesson_revisions", this.lessonRevisionId, {
1546
+ course_id: this.courseId,
1547
+ lesson_key: this.lessonKey
1548
+ });
1549
+ await requireOwned(this, "lessons", this.lessonId);
1550
+ requireText(this.lessonKey, "Released lesson requires a lesson key");
1551
+ this.requireInsertOnSave();
1552
+ return super.save(options);
1553
+ }
1554
+ };
1555
+ __publicField$4(ReleasedLesson, "__smrtPackage__", "@happyvertical/smrt-education");
1556
+ __decorateClass$4([tenantId()], ReleasedLesson.prototype, "tenantId", 2);
1557
+ __decorateClass$4([foreignKey("Course", { required: true })], ReleasedLesson.prototype, "courseId", 2);
1558
+ __decorateClass$4([foreignKey("CourseRelease", { required: true })], ReleasedLesson.prototype, "releaseId", 2);
1559
+ __decorateClass$4([foreignKey("LessonRevision", { required: true })], ReleasedLesson.prototype, "lessonRevisionId", 2);
1560
+ __decorateClass$4([foreignKey("Lesson", { required: true })], ReleasedLesson.prototype, "lessonId", 2);
1561
+ __decorateClass$4([foreignKey("CourseSection", { required: true })], ReleasedLesson.prototype, "sectionId", 2);
1562
+ ReleasedLesson = __decorateClass$4([TenantScoped({ mode: "required" }), smrt({
1563
+ tableName: "released_lessons",
1564
+ conflictColumns: [
1565
+ "tenant_id",
1566
+ "release_id",
1567
+ "lesson_key"
1568
+ ],
1569
+ ...closed$3
1570
+ })], ReleasedLesson);
1571
+ var ReleasedLessonCollection = class extends SmrtCollection {
1572
+ static _itemClass = ReleasedLesson;
1573
+ };
1574
+ var CourseArchiveDecision = class extends SmrtObject {
1575
+ tenantId = "";
1576
+ courseId = "";
1577
+ action = "archive";
1578
+ sequence = 1;
1579
+ actorUserId = "";
1580
+ delegatingGuardianUserId = "";
1581
+ delegationId = "";
1582
+ requestKey = "";
1583
+ recordedAt = /* @__PURE__ */ new Date();
1584
+ constructor(options = {}) {
1585
+ super(options);
1586
+ adopt(this, options);
1587
+ }
1588
+ async delete() {
1589
+ noDelete$1();
1590
+ }
1591
+ async save(options) {
1592
+ await requireInsert(this);
1593
+ await requireOwned(this, "courses", this.courseId);
1594
+ requireText(this.actorUserId, "Archive decision requires an actor");
1595
+ requireText(this.requestKey, "Archive decision requires a request key");
1596
+ if (!["archive", "unarchive"].includes(this.action) || !Number.isSafeInteger(this.sequence) || this.sequence < 1) throw new Error("Invalid archive decision");
1597
+ validDate(this.recordedAt);
1598
+ this.requireInsertOnSave();
1599
+ return super.save(options);
1600
+ }
1601
+ };
1602
+ __publicField$4(CourseArchiveDecision, "__smrtPackage__", "@happyvertical/smrt-education");
1603
+ __decorateClass$4([tenantId()], CourseArchiveDecision.prototype, "tenantId", 2);
1604
+ __decorateClass$4([foreignKey("Course", { required: true })], CourseArchiveDecision.prototype, "courseId", 2);
1605
+ CourseArchiveDecision = __decorateClass$4([TenantScoped({ mode: "required" }), smrt({
1606
+ tableName: "course_archive_decisions",
1607
+ conflictColumns: [
1608
+ "tenant_id",
1609
+ "course_id",
1610
+ "request_key"
1611
+ ],
1612
+ ...closed$3
1613
+ })], CourseArchiveDecision);
1614
+ var CourseArchiveDecisionCollection = class extends SmrtCollection {
1615
+ static _itemClass = CourseArchiveDecision;
1616
+ };
1617
+ var CourseDiscardTombstone = class extends SmrtObject {
1618
+ tenantId = "";
1619
+ courseId = "";
1620
+ title = "";
1621
+ headRevisionId = "";
1622
+ revisionCount = 0;
1623
+ reason = "";
1624
+ actorUserId = "";
1625
+ delegatingGuardianUserId = "";
1626
+ delegationId = "";
1627
+ requestKey = "";
1628
+ recordedAt = /* @__PURE__ */ new Date();
1629
+ constructor(options = {}) {
1630
+ super(options);
1631
+ adopt(this, options);
1632
+ }
1633
+ async delete() {
1634
+ noDelete$1();
1635
+ }
1636
+ async save(options) {
1637
+ await requireInsert(this);
1638
+ await requireOwned(this, "courses", this.courseId);
1639
+ requireText(this.actorUserId, "Discard requires an actor");
1640
+ requireText(this.requestKey, "Discard requires a request key");
1641
+ validDate(this.recordedAt);
1642
+ this.requireInsertOnSave();
1643
+ return super.save(options);
1644
+ }
1645
+ };
1646
+ __publicField$4(CourseDiscardTombstone, "__smrtPackage__", "@happyvertical/smrt-education");
1647
+ __decorateClass$4([tenantId()], CourseDiscardTombstone.prototype, "tenantId", 2);
1648
+ __decorateClass$4([foreignKey("Course", { required: true })], CourseDiscardTombstone.prototype, "courseId", 2);
1649
+ CourseDiscardTombstone = __decorateClass$4([TenantScoped({ mode: "required" }), smrt({
1650
+ tableName: "course_discard_tombstones",
1651
+ conflictColumns: ["tenant_id", "course_id"],
1652
+ ...closed$3
1653
+ })], CourseDiscardTombstone);
1654
+ var CourseDiscardTombstoneCollection = class extends SmrtCollection {
1655
+ static _itemClass = CourseDiscardTombstone;
1656
+ };
1657
+ var EnrollmentReleasePin = class extends SmrtObject {
1658
+ tenantId = "";
1659
+ enrollmentId = "";
1660
+ learnerId = "";
1661
+ courseId = "";
1662
+ releaseId = "";
1663
+ previousReleaseId = null;
1664
+ kind = "initial";
1665
+ sequence = 1;
1666
+ /** Empty for automatic moves, which have no actor. */
1667
+ actorUserId = "";
1668
+ delegatingGuardianUserId = "";
1669
+ delegationId = "";
1670
+ /** Automatic moves: the delegation id recorded on the opt-in followed; empty otherwise. */
1671
+ optInDelegationId = "";
1672
+ /** Host assignment-setting value relied on; automatic moves copy the opt-in's. */
1673
+ policyBasis = "";
1674
+ /** Scope of the opt-in an automatic move followed ('course' | 'enrollment'); empty otherwise. */
1675
+ optInScope = "";
1676
+ requestKey = "";
1677
+ recordedAt = /* @__PURE__ */ new Date();
1678
+ constructor(options = {}) {
1679
+ super(options);
1680
+ adopt(this, options);
1681
+ }
1682
+ async delete() {
1683
+ noDelete$1();
1684
+ }
1685
+ async save(options) {
1686
+ await requireInsert(this);
1687
+ await requireOwned(this, "enrollments", this.enrollmentId, { learner_id: this.learnerId });
1688
+ await requireOwned(this, "course_releases", this.releaseId, { course_id: this.courseId });
1689
+ if (this.previousReleaseId) await requireOwned(this, "course_releases", this.previousReleaseId, { course_id: this.courseId });
1690
+ requireText(this.requestKey, "Enrollment pin requires a request key", 300);
1691
+ requireText(this.policyBasis, "Enrollment pin requires a policy basis");
1692
+ if (![
1693
+ "initial",
1694
+ "explicit",
1695
+ "auto"
1696
+ ].includes(this.kind) || this.kind !== "auto" && !this.actorUserId || !Number.isSafeInteger(this.sequence) || this.sequence < 1) throw new Error("Invalid enrollment pin");
1697
+ if (this.kind === "auto" ? !["course", "enrollment"].includes(this.optInScope) : this.optInScope !== "") throw new Error("Invalid enrollment pin opt-in scope");
1698
+ validDate(this.recordedAt);
1699
+ this.requireInsertOnSave();
1700
+ return super.save(options);
1701
+ }
1702
+ };
1703
+ __publicField$4(EnrollmentReleasePin, "__smrtPackage__", "@happyvertical/smrt-education");
1704
+ __decorateClass$4([tenantId()], EnrollmentReleasePin.prototype, "tenantId", 2);
1705
+ __decorateClass$4([foreignKey("Enrollment", { required: true })], EnrollmentReleasePin.prototype, "enrollmentId", 2);
1706
+ __decorateClass$4([foreignKey("Learner", { required: true })], EnrollmentReleasePin.prototype, "learnerId", 2);
1707
+ __decorateClass$4([foreignKey("Course", { required: true })], EnrollmentReleasePin.prototype, "courseId", 2);
1708
+ __decorateClass$4([foreignKey("CourseRelease", { required: true })], EnrollmentReleasePin.prototype, "releaseId", 2);
1709
+ __decorateClass$4([foreignKey("CourseRelease", { nullable: true })], EnrollmentReleasePin.prototype, "previousReleaseId", 2);
1710
+ EnrollmentReleasePin = __decorateClass$4([TenantScoped({ mode: "required" }), smrt({
1711
+ tableName: "enrollment_release_pins",
1712
+ conflictColumns: [
1713
+ "tenant_id",
1714
+ "enrollment_id",
1715
+ "request_key"
1716
+ ],
1717
+ ...closed$3
1718
+ })], EnrollmentReleasePin);
1719
+ var EnrollmentReleasePinCollection = class extends SmrtCollection {
1720
+ static _itemClass = EnrollmentReleasePin;
1721
+ };
1722
+ var CourseUpgradeChoice = class extends SmrtObject {
1723
+ tenantId = "";
1724
+ courseId = "";
1725
+ /** Empty for a course-level choice. */
1726
+ enrollmentId = "";
1727
+ enabled = false;
1728
+ sequence = 1;
1729
+ actorUserId = "";
1730
+ delegatingGuardianUserId = "";
1731
+ delegationId = "";
1732
+ /** Host assignment-setting value relied on for this choice. */
1733
+ policyBasis = "";
1734
+ requestKey = "";
1735
+ recordedAt = /* @__PURE__ */ new Date();
1736
+ constructor(options = {}) {
1737
+ super(options);
1738
+ adopt(this, options);
1739
+ }
1740
+ async delete() {
1741
+ noDelete$1();
1742
+ }
1743
+ async save(options) {
1744
+ await requireInsert(this);
1745
+ await requireOwned(this, "courses", this.courseId);
1746
+ if (this.enrollmentId) await requireOwned(this, "enrollments", this.enrollmentId);
1747
+ requireText(this.actorUserId, "Upgrade choice requires an actor");
1748
+ requireText(this.requestKey, "Upgrade choice requires a request key");
1749
+ requireText(this.policyBasis, "Upgrade choice requires a policy basis");
1750
+ if (typeof this.enabled !== "boolean" || !Number.isSafeInteger(this.sequence) || this.sequence < 1) throw new Error("Invalid upgrade choice");
1751
+ validDate(this.recordedAt);
1752
+ this.requireInsertOnSave();
1753
+ return super.save(options);
1754
+ }
1755
+ };
1756
+ __publicField$4(CourseUpgradeChoice, "__smrtPackage__", "@happyvertical/smrt-education");
1757
+ __decorateClass$4([tenantId()], CourseUpgradeChoice.prototype, "tenantId", 2);
1758
+ __decorateClass$4([foreignKey("Course", { required: true })], CourseUpgradeChoice.prototype, "courseId", 2);
1759
+ CourseUpgradeChoice = __decorateClass$4([TenantScoped({ mode: "required" }), smrt({
1760
+ tableName: "course_upgrade_choices",
1761
+ conflictColumns: [
1762
+ "tenant_id",
1763
+ "course_id",
1764
+ "request_key"
1765
+ ],
1766
+ ...closed$3
1767
+ })], CourseUpgradeChoice);
1768
+ var CourseUpgradeChoiceCollection = class extends SmrtCollection {
1769
+ static _itemClass = CourseUpgradeChoice;
1770
+ };
1771
+ var EnrollmentAutoUpgradeSkip = class extends SmrtObject {
1772
+ tenantId = "";
1773
+ enrollmentId = "";
1774
+ learnerId = "";
1775
+ courseId = "";
1776
+ releaseId = "";
1777
+ /** 'course' or 'enrollment': the scope of the opt-in that was followed. */
1778
+ optInScope = "";
1779
+ optInActorUserId = "";
1780
+ optInDelegatingGuardianUserId = "";
1781
+ optInDelegationId = "";
1782
+ policyBasis = "";
1783
+ /** The authorizer's bounded reason, else 'not-authorized'. Never a thrown message. */
1784
+ reason = "not-authorized";
1785
+ /** The authorizer's short code (`[a-z0-9._-]`, up to 64); empty when none. */
1786
+ code = "";
1787
+ recordedAt = /* @__PURE__ */ new Date();
1788
+ constructor(options = {}) {
1789
+ super(options);
1790
+ adopt(this, options);
1791
+ }
1792
+ async delete() {
1793
+ noDelete$1();
1794
+ }
1795
+ async save(options) {
1796
+ await requireInsert(this);
1797
+ await requireOwned(this, "enrollments", this.enrollmentId, { learner_id: this.learnerId });
1798
+ await requireOwned(this, "course_releases", this.releaseId, { course_id: this.courseId });
1799
+ requireText(this.optInActorUserId, "Auto-upgrade skip requires the opt-in actor");
1800
+ requireText(this.policyBasis, "Auto-upgrade skip requires a policy basis");
1801
+ requireText(this.reason, "Auto-upgrade skip requires a reason", 200);
1802
+ if (!["course", "enrollment"].includes(this.optInScope) || /[\u0000-\u001f\u007f]/.test(this.reason) || !/^[a-z0-9._-]{0,64}$/.test(this.code)) throw new Error("Invalid auto-upgrade skip");
1803
+ validDate(this.recordedAt);
1804
+ this.requireInsertOnSave();
1805
+ return super.save(options);
1806
+ }
1807
+ };
1808
+ __publicField$4(EnrollmentAutoUpgradeSkip, "__smrtPackage__", "@happyvertical/smrt-education");
1809
+ __decorateClass$4([tenantId()], EnrollmentAutoUpgradeSkip.prototype, "tenantId", 2);
1810
+ __decorateClass$4([foreignKey("Enrollment", { required: true })], EnrollmentAutoUpgradeSkip.prototype, "enrollmentId", 2);
1811
+ __decorateClass$4([foreignKey("Learner", { required: true })], EnrollmentAutoUpgradeSkip.prototype, "learnerId", 2);
1812
+ __decorateClass$4([foreignKey("Course", { required: true })], EnrollmentAutoUpgradeSkip.prototype, "courseId", 2);
1813
+ __decorateClass$4([foreignKey("CourseRelease", { required: true })], EnrollmentAutoUpgradeSkip.prototype, "releaseId", 2);
1814
+ EnrollmentAutoUpgradeSkip = __decorateClass$4([TenantScoped({ mode: "required" }), smrt({
1815
+ tableName: "enrollment_auto_upgrade_skips",
1816
+ conflictColumns: [
1817
+ "tenant_id",
1818
+ "enrollment_id",
1819
+ "release_id"
1820
+ ],
1821
+ ...closed$3
1822
+ })], EnrollmentAutoUpgradeSkip);
1823
+ var EnrollmentAutoUpgradeSkipCollection = class extends SmrtCollection {
1824
+ static _itemClass = EnrollmentAutoUpgradeSkip;
920
1825
  };
921
- var GradeCollection = class extends SmrtCollection {
922
- static _itemClass = Grade;
1826
+ var EnrollmentAssignmentRecord = class extends SmrtObject {
1827
+ tenantId = "";
1828
+ enrollmentId = "";
1829
+ learnerId = "";
1830
+ actorUserId = "";
1831
+ delegatingGuardianUserId = "";
1832
+ delegationId = "";
1833
+ recordedAt = /* @__PURE__ */ new Date();
1834
+ constructor(options = {}) {
1835
+ super(options);
1836
+ adopt(this, options);
1837
+ }
1838
+ async delete() {
1839
+ noDelete$1();
1840
+ }
1841
+ async save(options) {
1842
+ await requireInsert(this);
1843
+ await requireOwned(this, "enrollments", this.enrollmentId, { learner_id: this.learnerId });
1844
+ requireText(this.actorUserId, "Assignment record requires the agent");
1845
+ requireText(this.delegatingGuardianUserId, "Assignment record requires the delegating guardian", 200);
1846
+ requireText(this.delegationId, "Assignment record requires a delegation id", 200);
1847
+ validDate(this.recordedAt);
1848
+ this.requireInsertOnSave();
1849
+ return super.save(options);
1850
+ }
923
1851
  };
924
- var SubmissionAssetCollection = class extends SmrtJunction {
925
- static _itemClass = SubmissionAsset;
926
- leftField = "submissionId";
927
- rightField = "assetId";
1852
+ __publicField$4(EnrollmentAssignmentRecord, "__smrtPackage__", "@happyvertical/smrt-education");
1853
+ __decorateClass$4([tenantId()], EnrollmentAssignmentRecord.prototype, "tenantId", 2);
1854
+ __decorateClass$4([foreignKey("Enrollment", { required: true })], EnrollmentAssignmentRecord.prototype, "enrollmentId", 2);
1855
+ __decorateClass$4([foreignKey("Learner", { required: true })], EnrollmentAssignmentRecord.prototype, "learnerId", 2);
1856
+ EnrollmentAssignmentRecord = __decorateClass$4([TenantScoped({ mode: "required" }), smrt({
1857
+ tableName: "enrollment_assignment_records",
1858
+ conflictColumns: ["tenant_id", "enrollment_id"],
1859
+ ...closed$3
1860
+ })], EnrollmentAssignmentRecord);
1861
+ var EnrollmentAssignmentRecordCollection = class extends SmrtCollection {
1862
+ static _itemClass = EnrollmentAssignmentRecord;
928
1863
  };
929
- //#endregion
930
- //#region src/catalog-service.ts
931
- var CatalogAuthorizationError = class extends Error {
932
- constructor() {
933
- super("Education catalog access is not authorized.");
934
- this.name = "CatalogAuthorizationError";
1864
+ var CurriculumCreationRecord = class extends SmrtObject {
1865
+ tenantId = "";
1866
+ curriculumId = "";
1867
+ learnerId = "";
1868
+ actorUserId = "";
1869
+ delegatingGuardianUserId = "";
1870
+ delegationId = "";
1871
+ recordedAt = /* @__PURE__ */ new Date();
1872
+ constructor(options = {}) {
1873
+ super(options);
1874
+ adopt(this, options);
935
1875
  }
936
- };
937
- function projectLesson(lesson, includeAnswerKeys) {
938
- if (includeAnswerKeys || !("answerUrl" in lesson)) return lesson;
939
- return Object.assign(Object.create(Object.getPrototypeOf(lesson)), lesson, { answerUrl: "" });
940
- }
941
- var CatalogService = class {
942
- constructor(courses, sections, lessons, assets, tenants, authorize) {
943
- this.courses = courses;
944
- this.sections = sections;
945
- this.lessons = lessons;
946
- this.assets = assets;
947
- this.tenants = tenants;
948
- this.authorize = authorize;
1876
+ async delete() {
1877
+ noDelete$1();
949
1878
  }
950
- courses;
951
- sections;
952
- lessons;
953
- assets;
954
- tenants;
955
- authorize;
956
- executor() {
957
- const executor = this.courses.db;
958
- if (!executor || [
959
- this.sections,
960
- this.lessons,
961
- this.assets
962
- ].some((collection) => collection.db !== executor)) throw new CatalogAuthorizationError();
963
- return executor;
1879
+ async save(options) {
1880
+ await requireInsert(this);
1881
+ await requireOwned(this, "curricula", this.curriculumId, { learner_id: this.learnerId });
1882
+ requireText(this.actorUserId, "Creation record requires the agent");
1883
+ requireText(this.delegatingGuardianUserId, "Creation record requires the delegating guardian", 200);
1884
+ requireText(this.delegationId, "Creation record requires a delegation id", 200);
1885
+ validDate(this.recordedAt);
1886
+ this.requireInsertOnSave();
1887
+ return super.save(options);
964
1888
  }
965
- authorizeInScope(request) {
966
- return this.authorize({
967
- ...request,
968
- executor: this.executor()
1889
+ };
1890
+ __publicField$4(CurriculumCreationRecord, "__smrtPackage__", "@happyvertical/smrt-education");
1891
+ __decorateClass$4([tenantId()], CurriculumCreationRecord.prototype, "tenantId", 2);
1892
+ __decorateClass$4([foreignKey("Curriculum", { required: true })], CurriculumCreationRecord.prototype, "curriculumId", 2);
1893
+ __decorateClass$4([foreignKey("Learner", { required: true })], CurriculumCreationRecord.prototype, "learnerId", 2);
1894
+ CurriculumCreationRecord = __decorateClass$4([TenantScoped({ mode: "required" }), smrt({
1895
+ tableName: "curriculum_creation_records",
1896
+ conflictColumns: ["tenant_id", "curriculum_id"],
1897
+ ...closed$3
1898
+ })], CurriculumCreationRecord);
1899
+ var CurriculumCreationRecordCollection = class extends SmrtCollection {
1900
+ static _itemClass = CurriculumCreationRecord;
1901
+ };
1902
+ var present = (value) => value !== void 0 && value !== null && value !== "";
1903
+ function bounded(value, label) {
1904
+ if (typeof value !== "string" || value.length < 1 || value.length > 200 || /[\u0000-\u001f\u007f]/.test(value)) throw new CourseAuthoringValidationError(`Delegation ${label} must be 1-200 printable characters`);
1905
+ return value;
1906
+ }
1907
+ function readDelegation(claim) {
1908
+ const hasGuardian = present(claim.delegatingGuardianUserId);
1909
+ const hasId = present(claim.delegationId);
1910
+ if (!hasGuardian && !hasId) return null;
1911
+ if (hasGuardian !== hasId) throw new CourseAuthoringValidationError("A delegation id requires a delegating guardian, and the reverse");
1912
+ const delegatingGuardianUserId = bounded(claim.delegatingGuardianUserId, "guardian");
1913
+ const delegationId = bounded(claim.delegationId, "id");
1914
+ if (delegatingGuardianUserId === claim.actorUserId) throw new CourseAuthoringAuthorizationError();
1915
+ return {
1916
+ delegatingGuardianUserId,
1917
+ delegationId
1918
+ };
1919
+ }
1920
+ function delegationActorFields(claim) {
1921
+ return readDelegation(claim) ?? {};
1922
+ }
1923
+ function delegationRecordFields(claim) {
1924
+ return readDelegation(claim) ?? {
1925
+ delegatingGuardianUserId: "",
1926
+ delegationId: ""
1927
+ };
1928
+ }
1929
+ //#endregion
1930
+ //#region src/course-release-scope.ts
1931
+ async function resolveEnrollmentLessons(db, tenantId, enrollmentId) {
1932
+ const pins = (await (await EnrollmentReleasePinCollection.create({ db })).list({
1933
+ where: {
1934
+ tenantId,
1935
+ enrollmentId
1936
+ },
1937
+ limit: 1e5
1938
+ })).sort((a, b) => a.sequence - b.sequence);
1939
+ const current = pins.at(-1);
1940
+ if (!current) return null;
1941
+ const currentReleaseId = current.releaseId;
1942
+ const lineage = [...new Set(pins.map((p) => p.releaseId))];
1943
+ const releasedLessons = await ReleasedLessonCollection.create({ db });
1944
+ const keysByRelease = /* @__PURE__ */ new Map();
1945
+ for (const releaseId of lineage) {
1946
+ const rows = await releasedLessons.list({
1947
+ where: {
1948
+ tenantId,
1949
+ releaseId
1950
+ },
1951
+ limit: 1e5
969
1952
  });
1953
+ keysByRelease.set(releaseId, new Map(rows.map((row) => [row.lessonKey, row.lessonId])));
970
1954
  }
971
- async findVisible(request) {
972
- const { tenantId, actorUserId, includePrivate = false, options = {} } = request;
973
- if (!tenantId.trim() || !actorUserId.trim()) throw new CatalogAuthorizationError();
974
- const actor = {
975
- actorTenantId: tenantId,
976
- actorUserId,
977
- rootTenantId: tenantId
978
- };
979
- if (await this.authorizeInScope({
980
- ...actor,
981
- action: "education.catalog.read"
982
- }) !== true) throw new CatalogAuthorizationError();
983
- if (includePrivate && await this.authorizeInScope({
984
- ...actor,
985
- action: "education.catalog.read-private"
986
- }) !== true) throw new CatalogAuthorizationError();
987
- return this.tenants.withTenantCatalogReadScope({
988
- collections: [
989
- "@happyvertical/smrt-education:Course",
990
- "@happyvertical/smrt-education:CourseSection",
991
- "@happyvertical/smrt-education:Lesson",
992
- "@happyvertical/smrt-education:LessonAsset"
993
- ],
994
- authorize: async (scope2) => scope2.mode === "catalog" && scope2.actorTenantId === tenantId && scope2.actorUserId === actorUserId && scope2.rootTenantId === tenantId
995
- }, async () => this.courses.list({
996
- ...options,
997
- where: [
998
- [{ "licenseKind !=": "personal-use-copy" }],
999
- [{ licenseKind: null }],
1000
- ...includePrivate ? [[{
1001
- licenseKind: "personal-use-copy",
1002
- tenantId
1003
- }]] : []
1004
- ]
1005
- }));
1006
- }
1007
- /** Project one already-visible course without exposing answer-key assets by default. */
1008
- async projectCourse(request) {
1009
- const course = (await this.findVisible(request)).find((item) => item.id === request.courseId);
1010
- if (!course?.id) return null;
1011
- const includeAnswerKeys = request.includeAnswerKeys === true;
1012
- const actor = {
1013
- actorTenantId: request.tenantId,
1014
- actorUserId: request.actorUserId,
1015
- rootTenantId: request.tenantId
1016
- };
1017
- if (includeAnswerKeys && await this.authorizeInScope({
1018
- ...actor,
1019
- action: "education.catalog.answer-key.read"
1020
- }) !== true) throw new CatalogAuthorizationError();
1021
- return this.tenants.withTenantCatalogReadScope({
1022
- collections: [
1023
- "@happyvertical/smrt-education:CourseSection",
1024
- "@happyvertical/smrt-education:Lesson",
1025
- "@happyvertical/smrt-education:LessonAsset"
1026
- ],
1027
- authorize: async (scope) => scope.mode === "catalog" && scope.actorTenantId === request.tenantId && scope.actorUserId === request.actorUserId
1028
- }, async () => {
1029
- const sections = await this.sections.list({
1030
- where: { courseId: course.id },
1031
- orderBy: ["sortOrder ASC", "id ASC"]
1032
- });
1033
- const result = [];
1034
- for (const section of sections) {
1035
- if (section.tenantId !== course.tenantId) continue;
1036
- const lessons = await this.lessons.list({
1037
- where: { sectionId: section.id },
1038
- orderBy: ["sortOrder ASC", "id ASC"]
1039
- });
1040
- result.push({
1041
- section,
1042
- lessons: await Promise.all(lessons.filter((lesson) => lesson.tenantId === course.tenantId).map(async (lesson) => ({
1043
- lesson: projectLesson(lesson, includeAnswerKeys),
1044
- assets: (await this.assets.byLeft(String(lesson.id))).filter((asset) => asset.tenantId === course.tenantId && (includeAnswerKeys || asset.relationship !== "answer-key"))
1045
- })))
1046
- });
1047
- }
1048
- return {
1049
- course,
1050
- sections: result
1051
- };
1955
+ const progress = await (await LessonProgressCollection.create({ db })).list({
1956
+ where: {
1957
+ tenantId,
1958
+ enrollmentId
1959
+ },
1960
+ limit: 1e6
1961
+ });
1962
+ const startedIds = new Set(progress.filter((row) => row.status !== "not_started" || Number(row.attempts) > 0).map((row) => row.lessonId));
1963
+ const result = /* @__PURE__ */ new Map();
1964
+ for (const [lessonKey, lessonId] of keysByRelease.get(currentReleaseId) ?? []) result.set(lessonKey, {
1965
+ lessonKey,
1966
+ lessonId,
1967
+ releaseId: currentReleaseId,
1968
+ pinnedToOlderRelease: false
1969
+ });
1970
+ for (const releaseId of lineage.filter((id) => id !== currentReleaseId)) for (const [lessonKey, lessonId] of keysByRelease.get(releaseId) ?? []) if (startedIds.has(lessonId)) result.set(lessonKey, {
1971
+ lessonKey,
1972
+ lessonId,
1973
+ releaseId,
1974
+ pinnedToOlderRelease: true
1975
+ });
1976
+ const lessons = [...result.values()].sort((a, b) => a.lessonKey.localeCompare(b.lessonKey));
1977
+ const releaseCourseIds = /* @__PURE__ */ new Map();
1978
+ for (const releaseId of /* @__PURE__ */ new Set([currentReleaseId, ...lessons.map((l) => l.releaseId)])) {
1979
+ const release = await db.get("course_releases", {
1980
+ id: releaseId,
1981
+ tenant_id: tenantId
1052
1982
  });
1983
+ if (release) releaseCourseIds.set(releaseId, String(release.release_course_id));
1053
1984
  }
1054
- };
1985
+ return {
1986
+ currentReleaseId,
1987
+ lineage,
1988
+ lessons,
1989
+ releaseCourseIds,
1990
+ keysByRelease,
1991
+ pinLearnerIds: new Set(pins.map((p) => p.learnerId))
1992
+ };
1993
+ }
1994
+ function orderEffectiveLessons(selection, orderOf) {
1995
+ const currentKeys = selection.keysByRelease.get(selection.currentReleaseId);
1996
+ const rank = (lesson) => {
1997
+ const position = (releaseId, lessonId) => {
1998
+ const at = orderOf(releaseId).indexOf(lessonId);
1999
+ return at < 0 ? Number.MAX_SAFE_INTEGER : at;
2000
+ };
2001
+ const currentId = currentKeys?.get(lesson.lessonKey);
2002
+ if (currentId) return [
2003
+ 0,
2004
+ position(selection.currentReleaseId, currentId),
2005
+ 0
2006
+ ];
2007
+ return [
2008
+ 1,
2009
+ selection.lineage.indexOf(lesson.releaseId),
2010
+ position(lesson.releaseId, lesson.lessonId)
2011
+ ];
2012
+ };
2013
+ return [...selection.lessons].sort((a, b) => {
2014
+ const [a0, a1, a2] = rank(a);
2015
+ const [b0, b1, b2] = rank(b);
2016
+ return a0 - b0 || a1 - b1 || a2 - b2 || a.lessonKey.localeCompare(b.lessonKey);
2017
+ });
2018
+ }
1055
2019
  //#endregion
1056
2020
  //#region src/family-intake-contracts.ts
1057
2021
  function familyIntakeScopeId(tenantId) {
@@ -1292,6 +2256,7 @@ function evaluateGroupSharingPolicy(policy, facts) {
1292
2256
  if (!Number.isFinite(captured) || !Number.isFinite(Date.parse(policy.historyFrom)) || captured < Date.parse(policy.historyFrom)) reasons.push("history");
1293
2257
  if (policy.courseIds.mode === "include" && (!facts.courseId || !policy.courseIds.values.includes(facts.courseId))) reasons.push("course");
1294
2258
  if (policy.subjects.mode === "include" && (!facts.subject || !policy.subjects.values.includes(facts.subject))) reasons.push("subject");
2259
+ if (facts.courseShareable === false) reasons.push("course-private");
1295
2260
  if (policy.parentReviewedOnly && !facts.parentReviewed) reasons.push("parent-review");
1296
2261
  const eligible = reasons.length === 0;
1297
2262
  return {
@@ -1461,6 +2426,7 @@ function validateWorkbookAssessment(value) {
1461
2426
  "unknown"
1462
2427
  ].includes(q.mappingProvenance)) throw new WorkbookAssessmentValidationError("Invalid mapping provenance");
1463
2428
  if (q.evidenceLimitations !== void 0 && (!Array.isArray(q.evidenceLimitations) || q.evidenceLimitations.length > 20 || q.evidenceLimitations.some((v) => typeof v !== "string" || v.length > 500))) throw new WorkbookAssessmentValidationError("Invalid evidence limitations");
2429
+ for (const [value2, label] of [[q.questionRevisionId, "question revision id"], [q.lessonRevisionId, "lesson revision id"]]) if (value2 !== void 0 && (typeof value2 !== "string" || !value2.trim() || value2.length > 200)) throw new WorkbookAssessmentValidationError(`Invalid ${label}`);
1464
2430
  text(q.id, "question id", 200);
1465
2431
  text(q.prompt, "prompt");
1466
2432
  text(q.answer, "answer");
@@ -1859,6 +2825,19 @@ var WorkbookAssessmentRevision = class extends SmrtObject {
1859
2825
  learner_id: this.learnerId,
1860
2826
  attempt_id: this.attemptId
1861
2827
  })) throw new Error("Workbook reviewed source mismatch");
2828
+ const assessment = JSON.parse(this.assessmentJson);
2829
+ for (const [column, table, ids] of [[
2830
+ "question revision",
2831
+ "question_revisions",
2832
+ assessment.questions.map((q) => q.questionRevisionId)
2833
+ ], [
2834
+ "lesson revision",
2835
+ "lesson_revisions",
2836
+ assessment.questions.map((q) => q.lessonRevisionId)
2837
+ ]]) for (const id of new Set(ids.filter((v) => !!v))) if (!await this.db.get(table, {
2838
+ id,
2839
+ tenant_id: this.tenantId
2840
+ })) throw new Error(`Workbook ${column} reference mismatch`);
1862
2841
  if (!["ai", "guardian"].includes(this.kind)) throw new Error("Invalid revision kind");
1863
2842
  return super.save(options);
1864
2843
  }
@@ -2192,15 +3171,17 @@ function progressView(row) {
2192
3171
  };
2193
3172
  }
2194
3173
  var LearnerService = class LearnerService {
2195
- constructor(db, authorize, authorizeCatalog) {
3174
+ constructor(db, authorize, authorizeCatalog, learnerOptions = {}) {
2196
3175
  this.db = db;
2197
3176
  this.authorize = authorize;
2198
3177
  this.authorizeCatalog = authorizeCatalog;
3178
+ this.learnerOptions = learnerOptions;
2199
3179
  this.postgres = isPostgresDatabase(db);
2200
3180
  }
2201
3181
  db;
2202
3182
  authorize;
2203
3183
  authorizeCatalog;
3184
+ learnerOptions;
2204
3185
  transactionState;
2205
3186
  writeFailure;
2206
3187
  writeTail = Promise.resolve();
@@ -2264,7 +3245,7 @@ var LearnerService = class LearnerService {
2264
3245
  }
2265
3246
  /** Internal subclass hook: preserve the concrete owning service in a transaction. */
2266
3247
  createTransactionService(db) {
2267
- return new LearnerService(db, this.authorize, this.authorizeCatalog);
3248
+ return new LearnerService(db, this.authorize, this.authorizeCatalog, this.learnerOptions);
2268
3249
  }
2269
3250
  /** Internal compensation hook, invoked after in-flight work drains on abort. */
2270
3251
  async rollbackResources(_cause, _rootDb) {}
@@ -2312,7 +3293,7 @@ var LearnerService = class LearnerService {
2312
3293
  assertServiceOpen() {
2313
3294
  if (this.transactionState === "aborting" || this.transactionState === "closed") throw new Error("Learner transaction scope is closed");
2314
3295
  }
2315
- async requireActor(request, action) {
3296
+ async requireActor(request, action, delegation = null) {
2316
3297
  this.assertServiceOpen();
2317
3298
  const context = getCurrentTenant();
2318
3299
  if (!context || !request.tenantId || !request.actorUserId || !request.learnerId || context.tenantId !== request.tenantId || context.userId !== request.actorUserId || context.superAdminBypass) throw new LearnerAuthorizationError();
@@ -2335,10 +3316,41 @@ var LearnerService = class LearnerService {
2335
3316
  id: learner.profileId,
2336
3317
  _meta_type: "@happyvertical/smrt-profiles:Person"
2337
3318
  })) throw new LearnerAuthorizationError();
2338
- const own = learner.userId === request.actorUserId;
3319
+ const holder = delegation?.delegatingGuardianUserId ?? request.actorUserId;
3320
+ if (delegation && (learner.userId === request.actorUserId || !await this.db.get("users", {
3321
+ id: request.actorUserId,
3322
+ status: "active"
3323
+ }) || !await this.db.get("memberships", {
3324
+ tenant_id: request.tenantId,
3325
+ user_id: holder,
3326
+ status: "active"
3327
+ }))) throw new LearnerAuthorizationError();
3328
+ if (delegation) {
3329
+ const verify = this.learnerOptions.verifyDelegation;
3330
+ let verified = false;
3331
+ if (typeof verify === "function") try {
3332
+ const decision = await verify(this.db, {
3333
+ tenantId: request.tenantId,
3334
+ actorUserId: request.actorUserId,
3335
+ delegatingGuardianUserId: delegation.delegatingGuardianUserId,
3336
+ delegationId: delegation.delegationId,
3337
+ learnerId: request.learnerId,
3338
+ ...delegation.courseId ? { courseId: delegation.courseId } : {},
3339
+ ...delegation.operation ? { operation: delegation.operation } : {}
3340
+ });
3341
+ verified = typeof decision === "object" && decision !== null ? decision.allowed === true : decision === true;
3342
+ } catch {
3343
+ verified = false;
3344
+ }
3345
+ if (!verified) throw new LearnerAuthorizationError();
3346
+ }
3347
+ const own = learner.userId === holder;
2339
3348
  let relationshipTypeId;
2340
3349
  if (!own) {
2341
- const user = await this.db.get("users", { id: request.actorUserId });
3350
+ const user = await this.db.get("users", {
3351
+ id: holder,
3352
+ ...delegation ? { status: "active" } : {}
3353
+ });
2342
3354
  const relationship = user && request.guardianRelationshipId ? await this.db.get("profile_relationships", {
2343
3355
  id: request.guardianRelationshipId,
2344
3356
  tenant_id: request.tenantId,
@@ -2354,14 +3366,29 @@ var LearnerService = class LearnerService {
2354
3366
  "education.learner.packet",
2355
3367
  "education.learner.notes.read",
2356
3368
  "education.learner.notes.add",
2357
- "education.learner.complete"
3369
+ "education.learner.complete",
3370
+ "education.learner.review-response"
2358
3371
  ].includes(action)) throw new LearnerAuthorizationError();
3372
+ const { delegatingGuardianUserId: _guardian, delegationId: _id, relationshipTypeId: _type, delegated: _delegated, delegatingRelationshipTypeId: _delegatingType, own: _own, ...base } = request;
3373
+ const authority = delegation ? {
3374
+ delegated: true,
3375
+ relationshipTypeId: void 0,
3376
+ delegatingGuardianUserId: delegation.delegatingGuardianUserId,
3377
+ delegationId: delegation.delegationId,
3378
+ delegatingRelationshipTypeId: relationshipTypeId
3379
+ } : {
3380
+ delegated: void 0,
3381
+ relationshipTypeId,
3382
+ delegatingGuardianUserId: void 0,
3383
+ delegationId: void 0,
3384
+ delegatingRelationshipTypeId: void 0
3385
+ };
2359
3386
  if (await this.authorize({
2360
- ...request,
3387
+ ...base,
3388
+ ...authority,
2361
3389
  executor: this.db,
2362
3390
  action,
2363
- own,
2364
- relationshipTypeId
3391
+ own
2365
3392
  }) !== true) throw new LearnerAuthorizationError();
2366
3393
  return learner;
2367
3394
  }
@@ -2509,15 +3536,40 @@ var LearnerService = class LearnerService {
2509
3536
  };
2510
3537
  });
2511
3538
  }
3539
+ /**
3540
+ * Creates a planned curriculum. A delegated agent passes
3541
+ * `delegatingGuardianUserId` + `delegationId` (as for `enroll`); the new
3542
+ * curriculum then also appends one `CurriculumCreationRecord`.
3543
+ */
2512
3544
  async createCurriculum(request) {
3545
+ let delegation;
3546
+ try {
3547
+ delegation = readDelegation(request);
3548
+ } catch (error) {
3549
+ if (error instanceof CourseAuthoringAuthorizationError) throw new LearnerAuthorizationError();
3550
+ throw error;
3551
+ }
2513
3552
  return this.mutate(async (service) => {
2514
- const learner = await service.requireActor(request, "education.learner.plan");
3553
+ const learner = await service.requireActor(request, "education.learner.plan", delegation ? {
3554
+ ...delegation,
3555
+ operation: "create-curriculum"
3556
+ } : null);
2515
3557
  if (typeof request.title !== "string" || !request.title.trim()) throw new Error("Curriculum title is required");
2516
- return curriculumView(await (await CurriculumCollection.create({ db: service.db })).create({
3558
+ const created = await (await CurriculumCollection.create({ db: service.db })).create({
2517
3559
  tenantId: request.tenantId,
2518
3560
  learnerId: String(learner.id),
2519
3561
  title: request.title
2520
- }));
3562
+ });
3563
+ if (delegation) await (await CurriculumCreationRecordCollection.create({ db: service.db })).create({
3564
+ _insertOnly: true,
3565
+ tenantId: request.tenantId,
3566
+ curriculumId: String(created.id),
3567
+ learnerId: String(learner.id),
3568
+ actorUserId: request.actorUserId,
3569
+ ...delegation,
3570
+ recordedAt: /* @__PURE__ */ new Date()
3571
+ });
3572
+ return curriculumView(created);
2521
3573
  });
2522
3574
  }
2523
3575
  async catalog(request, courseId) {
@@ -2531,12 +3583,81 @@ var LearnerService = class LearnerService {
2531
3583
  if (!projection) throw new LearnerAuthorizationError();
2532
3584
  return projection;
2533
3585
  }
3586
+ /**
3587
+ * The lessons that apply to an authorized enrollment. An unpinned enrollment
3588
+ * reads its own course, exactly as before. A pinned one reads the effective
3589
+ * set shared with `CourseAuthoringService.getEnrollmentRelease`: started
3590
+ * lessons stay on their original immutable rows, the rest follow the current
3591
+ * release. Every course row is still authorized through the catalog.
3592
+ */
3593
+ async lessonScope(request, enrollment) {
3594
+ const selection = await this.db.tableExists("enrollment_release_pins") ? await resolveEnrollmentLessons(this.db, request.tenantId, String(enrollment.id)) : null;
3595
+ if (!selection) {
3596
+ const catalog = await this.catalog(request, enrollment.courseId);
3597
+ return {
3598
+ course: catalog.course,
3599
+ entries: catalog.sections.flatMap((section) => section.lessons.map(({ lesson }) => ({
3600
+ lesson,
3601
+ course: catalog.course
3602
+ })))
3603
+ };
3604
+ }
3605
+ if ([...selection.pinLearnerIds].some((id) => id !== enrollment.learnerId)) throw new LearnerAuthorizationError();
3606
+ const releases = /* @__PURE__ */ new Set([selection.currentReleaseId, ...selection.lessons.map((lesson) => lesson.releaseId)]);
3607
+ const catalogs = /* @__PURE__ */ new Map();
3608
+ for (const releaseId of releases) {
3609
+ const courseId = selection.releaseCourseIds.get(releaseId);
3610
+ if (!courseId) throw new LearnerAuthorizationError();
3611
+ catalogs.set(releaseId, await this.catalog(request, courseId));
3612
+ }
3613
+ const rows = /* @__PURE__ */ new Map();
3614
+ const order = /* @__PURE__ */ new Map();
3615
+ for (const [releaseId, catalog] of catalogs) {
3616
+ const ids = [];
3617
+ for (const section of catalog.sections) for (const { lesson } of section.lessons) {
3618
+ ids.push(String(lesson.id));
3619
+ rows.set(String(lesson.id), {
3620
+ lesson,
3621
+ course: catalog.course
3622
+ });
3623
+ }
3624
+ order.set(releaseId, ids);
3625
+ }
3626
+ const entries = [];
3627
+ for (const effective of orderEffectiveLessons(selection, (releaseId) => order.get(releaseId) ?? [])) {
3628
+ const entry = rows.get(effective.lessonId);
3629
+ if (entry && order.get(effective.releaseId)?.includes(effective.lessonId)) entries.push(entry);
3630
+ }
3631
+ return {
3632
+ course: catalogs.get(selection.currentReleaseId).course,
3633
+ entries
3634
+ };
3635
+ }
3636
+ async lessonInScope(request, enrollment, lessonId) {
3637
+ return (await this.lessonScope(request, enrollment)).entries.some(({ lesson }) => String(lesson.id) === lessonId);
3638
+ }
2534
3639
  async lock(table, id, tenant) {
2535
3640
  if (this.postgres) await this.db.query(`SELECT id FROM ${table} WHERE id = ? AND tenant_id = ? FOR UPDATE`, [id, tenant]);
2536
3641
  }
3642
+ /**
3643
+ * Creates (or returns) the enrollment that assigns a course to a learner. A
3644
+ * delegated agent passes `delegatingGuardianUserId` + `delegationId`; a new
3645
+ * delegated enrollment also appends an `EnrollmentAssignmentRecord`.
3646
+ */
2537
3647
  async enroll(request) {
3648
+ let delegation;
3649
+ try {
3650
+ delegation = readDelegation(request);
3651
+ } catch (error) {
3652
+ if (error instanceof CourseAuthoringAuthorizationError) throw new LearnerAuthorizationError();
3653
+ throw error;
3654
+ }
2538
3655
  return this.mutate(async (service) => {
2539
- const learner = await service.requireActor(request, "education.learner.plan");
3656
+ const learner = await service.requireActor(request, "education.learner.plan", delegation ? {
3657
+ ...delegation,
3658
+ courseId: request.courseId,
3659
+ operation: "enroll"
3660
+ } : null);
2540
3661
  await service.lock("curricula", request.curriculumId, request.tenantId);
2541
3662
  const curriculum = await (await CurriculumCollection.create({ db: service.db })).get({
2542
3663
  id: request.curriculumId,
@@ -2555,13 +3676,23 @@ var LearnerService = class LearnerService {
2555
3676
  if (existing.learnerId !== learner.id) throw new LearnerAuthorizationError();
2556
3677
  return enrollmentView(existing);
2557
3678
  }
2558
- return enrollmentView(await enrollments.create({
3679
+ const created = await enrollments.create({
2559
3680
  tenantId: request.tenantId,
2560
3681
  curriculumId: String(curriculum.id),
2561
3682
  learnerId: String(learner.id),
2562
3683
  courseId: request.courseId,
2563
3684
  _insertOnly: true
2564
- }));
3685
+ });
3686
+ if (delegation) await (await EnrollmentAssignmentRecordCollection.create({ db: service.db })).create({
3687
+ _insertOnly: true,
3688
+ tenantId: request.tenantId,
3689
+ enrollmentId: String(created.id),
3690
+ learnerId: String(learner.id),
3691
+ actorUserId: request.actorUserId,
3692
+ ...delegation,
3693
+ recordedAt: /* @__PURE__ */ new Date()
3694
+ });
3695
+ return enrollmentView(created);
2565
3696
  });
2566
3697
  }
2567
3698
  async requireEnrollment(request, action) {
@@ -2613,8 +3744,8 @@ var LearnerService = class LearnerService {
2613
3744
  }
2614
3745
  async listProgress(request) {
2615
3746
  const enrollment = await this.requireEnrollment(request, "education.learner.read");
2616
- const catalog = await this.catalog(request, enrollment.courseId);
2617
- const lessons = new Set(catalog.sections.flatMap((section) => section.lessons.map(({ lesson }) => lesson.id)));
3747
+ const scope = await this.lessonScope(request, enrollment);
3748
+ const lessons = new Set(scope.entries.map(({ lesson }) => String(lesson.id)));
2618
3749
  const rows = await (await LessonProgressCollection.create({ db: this.db })).list({ where: {
2619
3750
  enrollmentId: String(enrollment.id),
2620
3751
  learnerId: request.learnerId,
@@ -2648,18 +3779,19 @@ var LearnerService = class LearnerService {
2648
3779
  await this.requireActor(request, "education.learner.read");
2649
3780
  const result = [];
2650
3781
  for (const enrollment of enrollments) {
2651
- const catalog = await this.catalog(request, enrollment.courseId);
3782
+ const scope = await this.lessonScope(request, enrollment);
2652
3783
  const progresses = await (await LessonProgressCollection.create({ db: this.db })).list({ where: {
2653
3784
  tenantId: request.tenantId,
2654
3785
  enrollmentId: String(enrollment.id),
2655
3786
  learnerId: request.learnerId
2656
3787
  } });
2657
3788
  const byLesson = new Map(await Promise.all(progresses.map(async (row) => [row.lessonId, await this.projectProgress(request, row)])));
2658
- const lesson = catalog.sections.flatMap((section) => section.lessons.map(({ lesson: lesson2 }) => lesson2)).find((candidate) => {
3789
+ const next = scope.entries.find(({ lesson: candidate }) => {
2659
3790
  const progress2 = byLesson.get(String(candidate.id));
2660
3791
  return !progress2 || !["complete", "skipped"].includes(progress2.status);
2661
3792
  });
2662
- if (!lesson) continue;
3793
+ if (!next) continue;
3794
+ const { lesson, course } = next;
2663
3795
  const progress = byLesson.get(String(lesson.id));
2664
3796
  result.push({
2665
3797
  enrollment: enrollmentView(enrollment),
@@ -2668,7 +3800,7 @@ var LearnerService = class LearnerService {
2668
3800
  id: String(lesson.id),
2669
3801
  title: lesson.title,
2670
3802
  kind: lesson.constructor.name,
2671
- requiresOnlineResource: catalog.course.modality === "online" || lesson.constructor.name === "Video"
3803
+ requiresOnlineResource: course.modality === "online" || lesson.constructor.name === "Video"
2672
3804
  }
2673
3805
  });
2674
3806
  }
@@ -2677,30 +3809,30 @@ var LearnerService = class LearnerService {
2677
3809
  /** All lesson summaries for one authorized assignment/enrollment. */
2678
3810
  async assignmentLessons(request) {
2679
3811
  const enrollment = await this.requireEnrollment(request, "education.learner.read");
2680
- const catalog = await this.catalog(request, enrollment.courseId);
3812
+ const scope = await this.lessonScope(request, enrollment);
2681
3813
  const progresses = await (await LessonProgressCollection.create({ db: this.db })).list({ where: {
2682
3814
  tenantId: request.tenantId,
2683
3815
  enrollmentId: String(enrollment.id),
2684
3816
  learnerId: request.learnerId
2685
3817
  } });
2686
3818
  const byLesson = new Map(progresses.map((row) => [row.lessonId, row]));
2687
- return Promise.all(catalog.sections.flatMap((section) => section.lessons.map(async ({ lesson }) => ({
3819
+ return Promise.all(scope.entries.map(async ({ lesson, course }) => ({
2688
3820
  enrollment: enrollmentView(enrollment),
2689
3821
  progress: byLesson.get(String(lesson.id)) ? await this.projectProgress(request, byLesson.get(String(lesson.id))) : null,
2690
3822
  lesson: {
2691
3823
  id: String(lesson.id),
2692
3824
  title: lesson.title,
2693
3825
  kind: lesson.constructor.name,
2694
- requiresOnlineResource: catalog.course.modality === "online" || lesson.constructor.name === "Video"
3826
+ requiresOnlineResource: course.modality === "online" || lesson.constructor.name === "Video"
2695
3827
  }
2696
- }))));
3828
+ })));
2697
3829
  }
2698
3830
  /** Start once. Caller-supplied learner/status/score/counter patches are never applied. */
2699
3831
  async startLesson(request) {
2700
3832
  return this.mutate(async (service) => {
2701
3833
  const enrollment = await service.requireEnrollment(request, "education.learner.start");
2702
3834
  await service.lock("enrollments", request.enrollmentId, request.tenantId);
2703
- if (!(await service.catalog(request, enrollment.courseId)).sections.some((section) => section.lessons.some(({ lesson }) => lesson.id === request.lessonId))) throw new LearnerAuthorizationError();
3835
+ if (!await service.lessonInScope(request, enrollment, request.lessonId)) throw new LearnerAuthorizationError();
2704
3836
  const progress = await LessonProgressCollection.create({ db: service.db });
2705
3837
  const existing = await progress.get({
2706
3838
  enrollmentId: String(enrollment.id),
@@ -25459,7 +26591,7 @@ var SubmissionService = class SubmissionService extends LearnerService {
25459
26591
  async progress(request, action) {
25460
26592
  const enrollment = await this.requireEnrollment(request, action);
25461
26593
  await this.lockProgressOwner(request);
25462
- if (!(await this.catalog(request, enrollment.courseId)).sections.some((section) => section.lessons.some(({ lesson }) => lesson.id === request.lessonId))) throw new LearnerAuthorizationError();
26594
+ if (!await this.lessonInScope(request, enrollment, request.lessonId)) throw new LearnerAuthorizationError();
25463
26595
  const collection = await LessonProgressCollection.create({ db: this.db });
25464
26596
  let progress = await collection.get({
25465
26597
  enrollmentId: request.enrollmentId,
@@ -25493,8 +26625,7 @@ var SubmissionService = class SubmissionService extends LearnerService {
25493
26625
  lessonId: row.lessonId,
25494
26626
  tenantId: request.tenantId
25495
26627
  }, { cache: false });
25496
- const catalog = await this.catalog(request, enrollment.courseId);
25497
- if (!progress || !catalog.sections.some((section) => section.lessons.some(({ lesson }) => lesson.id === row.lessonId))) throw new LearnerAuthorizationError();
26628
+ if (!progress || !await this.lessonInScope(request, enrollment, row.lessonId)) throw new LearnerAuthorizationError();
25498
26629
  return {
25499
26630
  row,
25500
26631
  progress
@@ -25622,7 +26753,7 @@ var SubmissionService = class SubmissionService extends LearnerService {
25622
26753
  */
25623
26754
  async assertLessonAssignment(request) {
25624
26755
  const enrollment = await this.requireEnrollment(request, "education.learner.submit");
25625
- if (!(await this.catalog(request, enrollment.courseId)).sections.some((section) => section.lessons.some(({ lesson }) => lesson.id === request.lessonId))) throw new LearnerAuthorizationError();
26756
+ if (!await this.lessonInScope(request, enrollment, request.lessonId)) throw new LearnerAuthorizationError();
25626
26757
  }
25627
26758
  /** Reject legacy upload/review entry points using authoritative owned scope. */
25628
26759
  async assertNonWorkbookSubmissionScope(request) {
@@ -25798,7 +26929,7 @@ var SubmissionService = class SubmissionService extends LearnerService {
25798
26929
  /** Latest retained completed-work evidence; lesson completion is independent. */
25799
26930
  async getLatestLessonSubmission(request) {
25800
26931
  const enrollment = await this.requireEnrollment(request, "education.learner.read");
25801
- if (!(await this.catalog(request, enrollment.courseId)).sections.some((section) => section.lessons.some(({ lesson }) => lesson.id === request.lessonId))) throw new LearnerAuthorizationError();
26932
+ if (!await this.lessonInScope(request, enrollment, request.lessonId)) throw new LearnerAuthorizationError();
25802
26933
  const progress = await (await LessonProgressCollection.create({ db: this.db })).get({
25803
26934
  enrollmentId: request.enrollmentId,
25804
26935
  learnerId: request.learnerId,
@@ -25962,7 +27093,7 @@ var SubmissionService = class SubmissionService extends LearnerService {
25962
27093
  /** Sign only an exact authorized printable join; answer keys never qualify. */
25963
27094
  async signLessonPrintable(request) {
25964
27095
  const enrollment = await this.requireEnrollment(request, "education.learner.read");
25965
- if (!(await this.catalog(request, enrollment.courseId)).sections.some((section) => section.lessons.some(({ lesson }) => String(lesson.id) === request.lessonId))) throw new LearnerAuthorizationError();
27096
+ if (!await this.lessonInScope(request, enrollment, request.lessonId)) throw new LearnerAuthorizationError();
25966
27097
  if (!await (await LessonAssetCollection.create({ db: this.db })).get({
25967
27098
  tenantId: request.tenantId,
25968
27099
  lessonId: request.lessonId,
@@ -28041,6 +29172,6 @@ var WorkbookPhotoService = class WorkbookPhotoService extends WorkbookService {
28041
29172
  }
28042
29173
  };
28043
29174
  //#endregion
28044
- export { FamilyIntakeDecision as $, WorkbookFeedbackDelivery as A, SubmissionAsset as At, validateWorkbookAssessment as B, Exercise as Bt, WorkbookActivity as C, VideoCollection as Ct, WorkbookAttempt as D, Grade as Dt, WorkbookAssessmentRevisionCollection as E, ExternalCompletion as Et, WorkbookWorkFilingCollection as F, Learner as Ft, GroupSharingConflictError as G, Worksheet as Gt, evaluateGroupSharingPolicy as H, Lesson as Ht, WORKBOOK_MATH_VOCABULARY as I, LessonProgress as It, validateGroupSharingPolicy as J, GroupSharingPolicyValidationError as K, WorkbookAssessmentValidationError as L, AuthoredCourse as Lt, WorkbookFeedbackRelease as M, LessonAsset as Mt, WorkbookFeedbackReleaseCollection as N, Curriculum as Nt, WorkbookAttemptCollection as O, ScannedSubmission as Ot, WorkbookWorkFiling as P, Enrollment as Pt, FamilyIntakeCollection as Q, WorkbookCompletionConflictError as R, Course as Rt, Workbook as S, SubmissionIntakeBindingCollection as St, WorkbookAssessmentRevision as T, Attestation as Tt, summarizeWorkbookEvidence as U, Reading as Ut, workbookAggregateFeedback as V, ExternalCourse as Vt, GroupSharingAuthorizationError as W, Video as Wt, FamilyIntakeAsset as X, FamilyIntake as Y, FamilyIntakeAssetCollection as Z, WorkbookWorkflowRequiredError as _, LessonProgressCollection as _t, WorkbookContextRevisionCollection as a, AuthoredCourseCollection as at, WorkbookCompletionDecision as b, SubmissionAssetCollection as bt, WorkbookPhotoDecision as c, CurriculumCollection as ct, WorkbookPhotoIntakeCollection as d, ExternalCompletionCollection as dt, FamilyIntakeDecisionCollection as et, validateWorkbookPhotoIdentification as f, ExternalCourseCollection as ft, SubmissionStorageCleanupError as g, LessonCollection as gt, SubmissionService as h, LessonAssetCollection as ht, WorkbookContextRevision as i, AttestationCollection as it, WorkbookFeedbackDeliveryCollection as j, SubmissionIntakeBinding as jt, WorkbookCollection as k, Submission as kt, WorkbookPhotoDecisionCollection as l, EnrollmentCollection as lt, normalizeWorkbookAnswerRegions as m, LearnerCollection as mt, preservePhotoProposal as n, CatalogAuthorizationError as nt, WorkbookPhotoAsset as o, CourseCollection as ot, normalizeWorkbookObservedLayout as p, GradeCollection as pt, canonicalGroupSharingPolicy as q, WorkbookService as r, CatalogService as rt, WorkbookPhotoAssetCollection as s, CourseSectionCollection as st, WorkbookPhotoService as t, familyIntakeScopeId as tt, WorkbookPhotoIntake as u, ExerciseCollection as ut, LearnerAuthorizationError as v, ReadingCollection as vt, WorkbookActivityCollection as w, WorksheetCollection as wt, WorkbookCompletionDecisionCollection as x, SubmissionCollection as xt, LearnerService as y, ScannedSubmissionCollection as yt, aggregateWorkbookQuestions as z, CourseSection as zt };
29175
+ export { FamilyIntakeDecision as $, CourseCollection as $t, WorkbookFeedbackDelivery as A, Learner as An, COURSE_AUTHORING_ACTIONS as At, validateWorkbookAssessment as B, Worksheet as Bn, CoursePolicyBasisMismatchError as Bt, WorkbookActivity as C, SubmissionAsset as Cn, EnrollmentAutoUpgradeSkipCollection as Ct, WorkbookAttempt as D, LessonAsset as Dn, LessonRevisionCollection as Dt, WorkbookAssessmentRevisionCollection as E, validateRubricResults as En, LessonRevision as Et, WorkbookWorkFilingCollection as F, Exercise as Fn, CourseAuthoringValidationError as Ft, GroupSharingConflictError as G, EnrollmentPinConflictError as Gt, evaluateGroupSharingPolicy as H, CourseRevisionAlreadyReleasedError as Ht, WORKBOOK_MATH_VOCABULARY as I, ExternalCourse as In, CourseDiscardRefusedError as It, validateGroupSharingPolicy as J, ReviewNotCurrentError as Jt, GroupSharingPolicyValidationError as K, MAX_PAGE_SIZE as Kt, WorkbookAssessmentValidationError as L, Lesson as Ln, CourseDiscardedError as Lt, WorkbookFeedbackRelease as M, AuthoredCourse as Mn, CourseArchivedError as Mt, WorkbookFeedbackReleaseCollection as N, Course as Nn, CourseAuthoringAuthorizationError as Nt, WorkbookAttemptCollection as O, Curriculum as On, ReleasedLesson as Ot, WorkbookWorkFiling as P, CourseSection as Pn, CourseAuthoringError as Pt, FamilyIntakeCollection as Q, AuthoredCourseCollection as Qt, WorkbookCompletionConflictError as R, Reading as Rn, CourseMaterialCopyError as Rt, Workbook as S, Submission as Sn, EnrollmentAutoUpgradeSkip as St, WorkbookAssessmentRevision as T, parseQuestionRubric as Tn, EnrollmentReleasePinCollection as Tt, summarizeWorkbookEvidence as U, CourseStaleHeadError as Ut, workbookAggregateFeedback as V, CourseRequestKeyConflictError as Vt, GroupSharingAuthorizationError as W, DEFAULT_PAGE_SIZE as Wt, FamilyIntakeAsset as X, CatalogService as Xt, FamilyIntake as Y, CatalogAuthorizationError as Yt, FamilyIntakeAssetCollection as Z, AttestationCollection as Zt, WorkbookWorkflowRequiredError as _, WorksheetCollection as _n, CourseUpgradeChoiceCollection as _t, WorkbookContextRevisionCollection as a, ExternalCourseCollection as an, readDelegation as at, WorkbookCompletionDecision as b, Grade as bn, EnrollmentAssignmentRecord as bt, WorkbookPhotoDecision as c, LessonAssetCollection as cn, CourseDiscardTombstone as ct, WorkbookPhotoIntakeCollection as d, ReadingCollection as dn, CourseDraftHeadCollection as dt, CourseSectionCollection as en, FamilyIntakeDecisionCollection as et, validateWorkbookPhotoIdentification as f, ScannedSubmissionCollection as fn, CourseRelease as ft, SubmissionStorageCleanupError as g, VideoCollection as gn, CourseUpgradeChoice as gt, SubmissionService as h, SubmissionIntakeBindingCollection as hn, CourseRevisionCollection as ht, WorkbookContextRevision as i, ExternalCompletionCollection as in, delegationRecordFields as it, WorkbookFeedbackDeliveryCollection as j, LessonProgress as jn, COURSE_PROVENANCES as jt, WorkbookCollection as k, Enrollment as kn, ReleasedLessonCollection as kt, WorkbookPhotoDecisionCollection as l, LessonCollection as ln, CourseDiscardTombstoneCollection as lt, normalizeWorkbookAnswerRegions as m, SubmissionCollection as mn, CourseRevision as mt, preservePhotoProposal as n, EnrollmentCollection as nn, resolveEnrollmentLessons as nt, WorkbookPhotoAsset as o, GradeCollection as on, CourseArchiveDecision as ot, normalizeWorkbookObservedLayout as p, SubmissionAssetCollection as pn, CourseReleaseCollection as pt, canonicalGroupSharingPolicy as q, ReviewGateRequiredError as qt, WorkbookService as r, ExerciseCollection as rn, delegationActorFields as rt, WorkbookPhotoAssetCollection as s, LearnerCollection as sn, CourseArchiveDecisionCollection as st, WorkbookPhotoService as t, CurriculumCollection as tn, familyIntakeScopeId as tt, WorkbookPhotoIntake as u, LessonProgressCollection as un, CourseDraftHead as ut, LearnerAuthorizationError as v, Attestation as vn, CurriculumCreationRecord as vt, WorkbookActivityCollection as w, SubmissionIntakeBinding as wn, EnrollmentReleasePin as wt, WorkbookCompletionDecisionCollection as x, ScannedSubmission as xn, EnrollmentAssignmentRecordCollection as xt, LearnerService as y, ExternalCompletion as yn, CurriculumCreationRecordCollection as yt, aggregateWorkbookQuestions as z, Video as zn, CourseNotFoundError as zt };
28045
29176
 
28046
- //# sourceMappingURL=workbook-photo-service-C6mbUUp6.js.map
29177
+ //# sourceMappingURL=workbook-photo-service-BxvZDqjM.js.map