@happyvertical/smrt-education 0.57.49 → 0.57.51

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 (45) hide show
  1. package/README.md +18 -1
  2. package/agents/course-authoring-service.md +39 -0
  3. package/agents/course-generation-review.md +53 -0
  4. package/agents/learner-service.md +20 -0
  5. package/agents/tutor-context.md +2 -1
  6. package/dist/chunks/{workbook-photo-service-WwDDC5GF.js → workbook-photo-service-BtMQsbhq.js} +1036 -402
  7. package/dist/chunks/workbook-photo-service-BtMQsbhq.js.map +1 -0
  8. package/dist/course-authoring-contracts.d.ts +23 -1
  9. package/dist/course-authoring-contracts.d.ts.map +1 -1
  10. package/dist/course-authoring-models.d.ts +2 -0
  11. package/dist/course-authoring-models.d.ts.map +1 -1
  12. package/dist/course-authoring-service.d.ts +56 -2
  13. package/dist/course-authoring-service.d.ts.map +1 -1
  14. package/dist/course-generation-contracts.d.ts +91 -0
  15. package/dist/course-generation-contracts.d.ts.map +1 -1
  16. package/dist/course-generation-models.d.ts +29 -0
  17. package/dist/course-generation-models.d.ts.map +1 -1
  18. package/dist/course-generation-output.d.ts +9 -3
  19. package/dist/course-generation-output.d.ts.map +1 -1
  20. package/dist/course-generation-proposal-generator.d.ts +171 -1
  21. package/dist/course-generation-proposal-generator.d.ts.map +1 -1
  22. package/dist/course-generation-review-service.d.ts +15 -1
  23. package/dist/course-generation-review-service.d.ts.map +1 -1
  24. package/dist/index.d.ts +6 -4
  25. package/dist/index.d.ts.map +1 -1
  26. package/dist/index.js +364 -327
  27. package/dist/index.js.map +1 -1
  28. package/dist/ingestion.js +1 -1
  29. package/dist/learner-history.d.ts +23 -0
  30. package/dist/learner-history.d.ts.map +1 -1
  31. package/dist/learner-interest-theme.d.ts +25 -0
  32. package/dist/learner-interest-theme.d.ts.map +1 -0
  33. package/dist/learner-service.d.ts +31 -2
  34. package/dist/learner-service.d.ts.map +1 -1
  35. package/dist/learners.d.ts +4 -0
  36. package/dist/learners.d.ts.map +1 -1
  37. package/dist/manifest.json +3699 -2802
  38. package/dist/question-authoring-service.d.ts +12 -0
  39. package/dist/question-authoring-service.d.ts.map +1 -1
  40. package/dist/smrt-knowledge.json +363 -13
  41. package/dist/tutor-context-service.d.ts.map +1 -1
  42. package/dist/tutor-contracts.d.ts +8 -0
  43. package/dist/tutor-contracts.d.ts.map +1 -1
  44. package/package.json +8 -8
  45. package/dist/chunks/workbook-photo-service-WwDDC5GF.js.map +0 -1
@@ -41,7 +41,7 @@ var __decorateClass$11 = (decorators, target, key, kind) => {
41
41
  return result;
42
42
  };
43
43
  var __publicField$11 = (obj, key, value) => __defNormalProp$11(obj, typeof key !== "symbol" ? key + "" : key, value);
44
- var closed$9 = {
44
+ var closed$10 = {
45
45
  api: false,
46
46
  mcp: false,
47
47
  cli: false
@@ -105,13 +105,13 @@ __decorateClass$11([tenantId({ nullable: true })], Course.prototype, "tenantId",
105
105
  __decorateClass$11([field({ required: true })], Course.prototype, "title", 2);
106
106
  Course = __decorateClass$11([TenantScoped({ mode: "optional" }), smrt({
107
107
  tableStrategy: "sti",
108
- ...closed$9
108
+ ...closed$10
109
109
  })], Course);
110
110
  var ExternalCourse = class extends Course {};
111
111
  __publicField$11(ExternalCourse, "__smrtPackage__", "@happyvertical/smrt-education");
112
112
  ExternalCourse = __decorateClass$11([smrt({
113
113
  tableName: "courses",
114
- ...closed$9
114
+ ...closed$10
115
115
  })], ExternalCourse);
116
116
  var COURSE_VISIBILITIES = [
117
117
  "family",
@@ -138,7 +138,7 @@ var AuthoredCourse = class extends Course {
138
138
  __publicField$11(AuthoredCourse, "__smrtPackage__", "@happyvertical/smrt-education");
139
139
  AuthoredCourse = __decorateClass$11([smrt({
140
140
  tableName: "courses",
141
- ...closed$9
141
+ ...closed$10
142
142
  })], AuthoredCourse);
143
143
  var CourseSection = class extends SmrtObject {
144
144
  tenantId = null;
@@ -163,7 +163,7 @@ __publicField$11(CourseSection, "__smrtPackage__", "@happyvertical/smrt-educatio
163
163
  __decorateClass$11([tenantId({ nullable: true })], CourseSection.prototype, "tenantId", 2);
164
164
  __decorateClass$11([foreignKey("Course", { required: true })], CourseSection.prototype, "courseId", 2);
165
165
  __decorateClass$11([field({ required: true })], CourseSection.prototype, "title", 2);
166
- CourseSection = __decorateClass$11([TenantScoped({ mode: "optional" }), smrt({ ...closed$9 })], CourseSection);
166
+ CourseSection = __decorateClass$11([TenantScoped({ mode: "optional" }), smrt({ ...closed$10 })], CourseSection);
167
167
  var Lesson = class extends SmrtObject {
168
168
  tenantId = null;
169
169
  sectionId = "";
@@ -196,13 +196,13 @@ __decorateClass$11([foreignKey("CourseSection", { required: true })], Lesson.pro
196
196
  __decorateClass$11([field({ required: true })], Lesson.prototype, "title", 2);
197
197
  Lesson = __decorateClass$11([TenantScoped({ mode: "optional" }), smrt({
198
198
  tableStrategy: "sti",
199
- ...closed$9
199
+ ...closed$10
200
200
  })], Lesson);
201
201
  var Reading = class extends Lesson {};
202
202
  __publicField$11(Reading, "__smrtPackage__", "@happyvertical/smrt-education");
203
203
  Reading = __decorateClass$11([smrt({
204
204
  tableName: "lessons",
205
- ...closed$9
205
+ ...closed$10
206
206
  })], Reading);
207
207
  var Video = class extends Lesson {
208
208
  videoUrl = "";
@@ -214,7 +214,7 @@ var Video = class extends Lesson {
214
214
  __publicField$11(Video, "__smrtPackage__", "@happyvertical/smrt-education");
215
215
  Video = __decorateClass$11([smrt({
216
216
  tableName: "lessons",
217
- ...closed$9
217
+ ...closed$10
218
218
  })], Video);
219
219
  var Worksheet = class extends Lesson {
220
220
  printableUrl = "";
@@ -229,13 +229,13 @@ var Worksheet = class extends Lesson {
229
229
  __publicField$11(Worksheet, "__smrtPackage__", "@happyvertical/smrt-education");
230
230
  Worksheet = __decorateClass$11([smrt({
231
231
  tableName: "lessons",
232
- ...closed$9
232
+ ...closed$10
233
233
  })], Worksheet);
234
234
  var Exercise = class extends Lesson {};
235
235
  __publicField$11(Exercise, "__smrtPackage__", "@happyvertical/smrt-education");
236
236
  Exercise = __decorateClass$11([smrt({
237
237
  tableName: "lessons",
238
- ...closed$9
238
+ ...closed$10
239
239
  })], Exercise);
240
240
  GlobalInterceptors.register({
241
241
  name: "education-catalog-identity",
@@ -266,6 +266,35 @@ GlobalInterceptors.register({
266
266
  }
267
267
  });
268
268
  //#endregion
269
+ //#region src/learner-interest-theme.ts
270
+ var MAX_INTEREST_THEME_LABEL_CHARS = 60;
271
+ var MAX_INTEREST_THEME_KEY_CHARS = 40;
272
+ var KEY$3 = /^[a-z0-9][a-z0-9-]*$/;
273
+ var CONTROL = /[\u0000-\u001f\u007f]/;
274
+ var CONTACT = /@|:\/\/|www\./i;
275
+ function parseInterestTheme(raw) {
276
+ if (!raw || typeof raw !== "object" || Array.isArray(raw)) return null;
277
+ const { label, key } = raw;
278
+ if (typeof label !== "string" || CONTROL.test(label)) return null;
279
+ const text = label.trim().replace(/\s+/g, " ");
280
+ if (!text || text.length > 60 || CONTACT.test(text)) return null;
281
+ if (key === void 0 || key === null || key === "") return {
282
+ label: text,
283
+ key: ""
284
+ };
285
+ if (typeof key !== "string" || key.length > 40 || !KEY$3.test(key)) return null;
286
+ return {
287
+ label: text,
288
+ key
289
+ };
290
+ }
291
+ function interestThemeFromColumns(label, key) {
292
+ return typeof label === "string" && label ? {
293
+ label,
294
+ key: typeof key === "string" ? key : ""
295
+ } : null;
296
+ }
297
+ //#endregion
269
298
  //#region src/learners.ts
270
299
  var __defProp$10 = Object.defineProperty;
271
300
  var __getOwnPropDesc$10 = Object.getOwnPropertyDescriptor;
@@ -282,7 +311,7 @@ var __decorateClass$10 = (decorators, target, key, kind) => {
282
311
  return result;
283
312
  };
284
313
  var __publicField$10 = (obj, key, value) => __defNormalProp$10(obj, typeof key !== "symbol" ? key + "" : key, value);
285
- var closed$8 = {
314
+ var closed$9 = {
286
315
  api: false,
287
316
  mcp: false,
288
317
  cli: false
@@ -311,6 +340,10 @@ var Learner = class extends SmrtObject {
311
340
  birthDate = null;
312
341
  level = 0;
313
342
  settings = "{}";
343
+ /** Interest theme label (`''` = none); see `learner-interest-theme.ts`. */
344
+ interestThemeLabel = "";
345
+ /** Optional stable host key for the theme; only with a label. */
346
+ interestThemeKey = "";
314
347
  constructor(options = {}) {
315
348
  super(options);
316
349
  applyOptions$1(this, options);
@@ -344,6 +377,13 @@ var Learner = class extends SmrtObject {
344
377
  status: "active"
345
378
  })) throw new Error("Learner requires a linked user/profile and active tenant membership");
346
379
  if (!Number.isSafeInteger(this.level) || this.level < 0) throw new Error("Learner level must be a nonnegative integer");
380
+ if (this.interestThemeLabel || this.interestThemeKey) {
381
+ const theme = parseInterestTheme({
382
+ label: this.interestThemeLabel,
383
+ key: this.interestThemeKey
384
+ });
385
+ if (!theme || theme.label !== this.interestThemeLabel || theme.key !== this.interestThemeKey) throw new Error("Invalid learner interest theme");
386
+ }
347
387
  return super.save(options);
348
388
  }
349
389
  };
@@ -351,7 +391,7 @@ __publicField$10(Learner, "__smrtPackage__", "@happyvertical/smrt-education");
351
391
  __decorateClass$10([tenantId()], Learner.prototype, "tenantId", 2);
352
392
  __decorateClass$10([crossPackageRef("@happyvertical/smrt-users:User", { required: true })], Learner.prototype, "userId", 2);
353
393
  __decorateClass$10([crossPackageRef("@happyvertical/smrt-profiles:Profile", { required: true })], Learner.prototype, "profileId", 2);
354
- Learner = __decorateClass$10([TenantScoped({ mode: "required" }), smrt({ ...closed$8 })], Learner);
394
+ Learner = __decorateClass$10([TenantScoped({ mode: "required" }), smrt({ ...closed$9 })], Learner);
355
395
  var Curriculum = class extends SmrtObject {
356
396
  tenantId = "";
357
397
  learnerId = "";
@@ -383,7 +423,7 @@ __decorateClass$10([tenantId()], Curriculum.prototype, "tenantId", 2);
383
423
  __decorateClass$10([foreignKey("Learner", { required: true })], Curriculum.prototype, "learnerId", 2);
384
424
  __decorateClass$10([field({ required: true })], Curriculum.prototype, "title", 2);
385
425
  Curriculum = __decorateClass$10([TenantScoped({ mode: "required" }), smrt({
386
- ...closed$8,
426
+ ...closed$9,
387
427
  tableName: "curricula"
388
428
  })], Curriculum);
389
429
  var Enrollment = class extends SmrtObject {
@@ -431,7 +471,7 @@ __decorateClass$10([foreignKey("Curriculum", { required: true })], Enrollment.pr
431
471
  __decorateClass$10([foreignKey("Learner", { required: true })], Enrollment.prototype, "learnerId", 2);
432
472
  __decorateClass$10([foreignKey("Course", { required: true })], Enrollment.prototype, "courseId", 2);
433
473
  Enrollment = __decorateClass$10([TenantScoped({ mode: "required" }), smrt({
434
- ...closed$8,
474
+ ...closed$9,
435
475
  conflictColumns: [
436
476
  "tenant_id",
437
477
  "curriculum_id",
@@ -507,7 +547,7 @@ __decorateClass$10([field({
507
547
  nullable: true
508
548
  })], LessonProgress.prototype, "bestScore", 2);
509
549
  LessonProgress = __decorateClass$10([TenantScoped({ mode: "required" }), smrt({
510
- ...closed$8,
550
+ ...closed$9,
511
551
  tableName: "lesson_progress",
512
552
  conflictColumns: [
513
553
  "tenant_id",
@@ -627,7 +667,7 @@ var __decorateClass$8 = (decorators, target, key, kind) => {
627
667
  return result;
628
668
  };
629
669
  var __publicField$8 = (obj, key, value) => __defNormalProp$8(obj, typeof key !== "symbol" ? key + "" : key, value);
630
- var closed$7 = {
670
+ var closed$8 = {
631
671
  api: false,
632
672
  mcp: false,
633
673
  cli: false
@@ -730,7 +770,7 @@ __decorateClass$8([field({
730
770
  })], Submission.prototype, "intakeItemId", 2);
731
771
  Submission = __decorateClass$8([TenantScoped({ mode: "required" }), smrt({
732
772
  tableStrategy: "sti",
733
- ...closed$7
773
+ ...closed$8
734
774
  })], Submission);
735
775
  var SubmissionIntakeBinding = class extends SmrtObject {
736
776
  tenantId = "";
@@ -760,7 +800,7 @@ __decorateClass$8([field({
760
800
  __decorateClass$8([foreignKey("Submission", { required: true })], SubmissionIntakeBinding.prototype, "submissionId", 2);
761
801
  SubmissionIntakeBinding = __decorateClass$8([TenantScoped({ mode: "required" }), smrt({
762
802
  tableName: "submission_intake_bindings",
763
- ...closed$7,
803
+ ...closed$8,
764
804
  conflictColumns: ["tenant_id", "intake_item_id"]
765
805
  })], SubmissionIntakeBinding);
766
806
  var ScannedSubmission = class extends Submission {
@@ -785,7 +825,7 @@ var ScannedSubmission = class extends Submission {
785
825
  __publicField$8(ScannedSubmission, "__smrtPackage__", "@happyvertical/smrt-education");
786
826
  ScannedSubmission = __decorateClass$8([smrt({
787
827
  tableName: "submissions",
788
- ...closed$7
828
+ ...closed$8
789
829
  })], ScannedSubmission);
790
830
  var ExternalCompletion = class extends Submission {
791
831
  reportedBy = "learner";
@@ -809,13 +849,13 @@ __publicField$8(ExternalCompletion, "__smrtPackage__", "@happyvertical/smrt-educ
809
849
  __decorateClass$8([field({ type: "decimal" })], ExternalCompletion.prototype, "reportedScore", 2);
810
850
  ExternalCompletion = __decorateClass$8([smrt({
811
851
  tableName: "submissions",
812
- ...closed$7
852
+ ...closed$8
813
853
  })], ExternalCompletion);
814
854
  var Attestation = class extends Submission {};
815
855
  __publicField$8(Attestation, "__smrtPackage__", "@happyvertical/smrt-education");
816
856
  Attestation = __decorateClass$8([smrt({
817
857
  tableName: "submissions",
818
- ...closed$7
858
+ ...closed$8
819
859
  })], Attestation);
820
860
  var LessonSelfAttestation = class extends SmrtObject {
821
861
  tenantId = "";
@@ -864,7 +904,7 @@ __decorateClass$8([foreignKey("Lesson", { required: true })], LessonSelfAttestat
864
904
  LessonSelfAttestation = __decorateClass$8([TenantScoped({ mode: "required" }), smrt({
865
905
  tableName: "lesson_self_attestations",
866
906
  conflictColumns: ["tenant_id", "submission_id"],
867
- ...closed$7
907
+ ...closed$8
868
908
  })], LessonSelfAttestation);
869
909
  var SubmissionAsset = class extends SmrtObject {
870
910
  tenantId = "";
@@ -914,7 +954,7 @@ SubmissionAsset = __decorateClass$8([TenantScoped({ mode: "required" }), smrt({
914
954
  "asset_id",
915
955
  "relationship"
916
956
  ],
917
- ...closed$7
957
+ ...closed$8
918
958
  })], SubmissionAsset);
919
959
  var Grade = class extends SmrtObject {
920
960
  tenantId = "";
@@ -985,7 +1025,7 @@ __decorateClass$8([field({
985
1025
  type: "text",
986
1026
  nullable: true
987
1027
  })], Grade.prototype, "questionRevisionId", 2);
988
- Grade = __decorateClass$8([TenantScoped({ mode: "required" }), smrt({ ...closed$7 })], Grade);
1028
+ Grade = __decorateClass$8([TenantScoped({ mode: "required" }), smrt({ ...closed$8 })], Grade);
989
1029
  //#endregion
990
1030
  //#region src/collections.ts
991
1031
  var CourseCollection = class extends SmrtCollection {
@@ -1238,6 +1278,13 @@ var CourseArchivedError = class extends CourseAuthoringError {
1238
1278
  super("course-archived", "Course is archived; unarchive it first.");
1239
1279
  }
1240
1280
  };
1281
+ var CourseAppendRefusedError = class extends CourseAuthoringError {
1282
+ constructor(reason) {
1283
+ super("append-refused", `Lesson append refused: ${reason}.`);
1284
+ this.reason = reason;
1285
+ }
1286
+ reason;
1287
+ };
1241
1288
  var CourseStaleHeadError = class extends CourseAuthoringError {
1242
1289
  constructor(currentHeadRevisionId) {
1243
1290
  super("stale-head", "The draft head moved; reload and retry.");
@@ -1346,7 +1393,7 @@ var __decorateClass$7 = (decorators, target, key, kind) => {
1346
1393
  return result;
1347
1394
  };
1348
1395
  var __publicField$7 = (obj, key, value) => __defNormalProp$7(obj, typeof key !== "symbol" ? key + "" : key, value);
1349
- var closed$6 = {
1396
+ var closed$7 = {
1350
1397
  api: false,
1351
1398
  mcp: false,
1352
1399
  cli: false
@@ -1456,7 +1503,7 @@ CourseRevision = __decorateClass$7([TenantScoped({ mode: "required" }), smrt({
1456
1503
  "course_id",
1457
1504
  "request_key"
1458
1505
  ],
1459
- ...closed$6
1506
+ ...closed$7
1460
1507
  })], CourseRevision);
1461
1508
  var CourseRevisionCollection = class extends SmrtCollection {
1462
1509
  static _itemClass = CourseRevision;
@@ -1546,7 +1593,7 @@ LessonRevision = __decorateClass$7([TenantScoped({ mode: "required" }), smrt({
1546
1593
  "lesson_key",
1547
1594
  "content_digest"
1548
1595
  ],
1549
- ...closed$6
1596
+ ...closed$7
1550
1597
  })], LessonRevision);
1551
1598
  var LessonRevisionCollection = class extends SmrtCollection {
1552
1599
  static _itemClass = LessonRevision;
@@ -1586,7 +1633,7 @@ __decorateClass$7([foreignKey("CourseRevision", { required: true })], CourseDraf
1586
1633
  CourseDraftHead = __decorateClass$7([TenantScoped({ mode: "required" }), smrt({
1587
1634
  tableName: "course_draft_heads",
1588
1635
  conflictColumns: ["tenant_id", "course_id"],
1589
- ...closed$6
1636
+ ...closed$7
1590
1637
  })], CourseDraftHead);
1591
1638
  var CourseDraftHeadCollection = class extends SmrtCollection {
1592
1639
  static _itemClass = CourseDraftHead;
@@ -1601,6 +1648,8 @@ var CourseRelease = class extends SmrtObject {
1601
1648
  actorUserId = "";
1602
1649
  delegatingGuardianUserId = "";
1603
1650
  delegationId = "";
1651
+ /** Host setting relied on for this release (`''` = none); see `release`. */
1652
+ policyBasis = "";
1604
1653
  requestKey = "";
1605
1654
  recordedAt = /* @__PURE__ */ new Date();
1606
1655
  constructor(options = {}) {
@@ -1614,6 +1663,7 @@ var CourseRelease = class extends SmrtObject {
1614
1663
  await requireInsert$3(this);
1615
1664
  await requireOwned$2(this, "courses", this.courseId);
1616
1665
  await requireOwned$2(this, "courses", this.releaseCourseId);
1666
+ if (this.policyBasis) requireText$3(this.policyBasis, "Invalid policy basis");
1617
1667
  await requireOwned$2(this, "course_revisions", this.revisionId, { course_id: this.courseId });
1618
1668
  requireText$3(this.actorUserId, "Release requires an actor");
1619
1669
  requireText$3(this.requestKey, "Release requires a request key");
@@ -1635,7 +1685,7 @@ CourseRelease = __decorateClass$7([TenantScoped({ mode: "required" }), smrt({
1635
1685
  "course_id",
1636
1686
  "request_key"
1637
1687
  ],
1638
- ...closed$6
1688
+ ...closed$7
1639
1689
  })], CourseRelease);
1640
1690
  var CourseReleaseCollection = class extends SmrtCollection {
1641
1691
  static _itemClass = CourseRelease;
@@ -1682,7 +1732,7 @@ ReleasedLesson = __decorateClass$7([TenantScoped({ mode: "required" }), smrt({
1682
1732
  "release_id",
1683
1733
  "lesson_key"
1684
1734
  ],
1685
- ...closed$6
1735
+ ...closed$7
1686
1736
  })], ReleasedLesson);
1687
1737
  var ReleasedLessonCollection = class extends SmrtCollection {
1688
1738
  static _itemClass = ReleasedLesson;
@@ -1725,7 +1775,7 @@ CourseArchiveDecision = __decorateClass$7([TenantScoped({ mode: "required" }), s
1725
1775
  "course_id",
1726
1776
  "request_key"
1727
1777
  ],
1728
- ...closed$6
1778
+ ...closed$7
1729
1779
  })], CourseArchiveDecision);
1730
1780
  var CourseArchiveDecisionCollection = class extends SmrtCollection {
1731
1781
  static _itemClass = CourseArchiveDecision;
@@ -1771,7 +1821,7 @@ CourseVisibilityChange = __decorateClass$7([TenantScoped({ mode: "required" }),
1771
1821
  "course_id",
1772
1822
  "request_key"
1773
1823
  ],
1774
- ...closed$6
1824
+ ...closed$7
1775
1825
  })], CourseVisibilityChange);
1776
1826
  var CourseVisibilityChangeCollection = class extends SmrtCollection {
1777
1827
  static _itemClass = CourseVisibilityChange;
@@ -1815,7 +1865,7 @@ CourseCoverChange = __decorateClass$7([TenantScoped({ mode: "required" }), smrt(
1815
1865
  "course_id",
1816
1866
  "request_key"
1817
1867
  ],
1818
- ...closed$6
1868
+ ...closed$7
1819
1869
  })], CourseCoverChange);
1820
1870
  var CourseCoverChangeCollection = class extends SmrtCollection {
1821
1871
  static _itemClass = CourseCoverChange;
@@ -1855,7 +1905,7 @@ __decorateClass$7([foreignKey("Course", { required: true })], CourseDiscardTombs
1855
1905
  CourseDiscardTombstone = __decorateClass$7([TenantScoped({ mode: "required" }), smrt({
1856
1906
  tableName: "course_discard_tombstones",
1857
1907
  conflictColumns: ["tenant_id", "course_id"],
1858
- ...closed$6
1908
+ ...closed$7
1859
1909
  })], CourseDiscardTombstone);
1860
1910
  var CourseDiscardTombstoneCollection = class extends SmrtCollection {
1861
1911
  static _itemClass = CourseDiscardTombstone;
@@ -1920,7 +1970,7 @@ EnrollmentReleasePin = __decorateClass$7([TenantScoped({ mode: "required" }), sm
1920
1970
  "enrollment_id",
1921
1971
  "request_key"
1922
1972
  ],
1923
- ...closed$6
1973
+ ...closed$7
1924
1974
  })], EnrollmentReleasePin);
1925
1975
  var EnrollmentReleasePinCollection = class extends SmrtCollection {
1926
1976
  static _itemClass = EnrollmentReleasePin;
@@ -1969,7 +2019,7 @@ CourseUpgradeChoice = __decorateClass$7([TenantScoped({ mode: "required" }), smr
1969
2019
  "course_id",
1970
2020
  "request_key"
1971
2021
  ],
1972
- ...closed$6
2022
+ ...closed$7
1973
2023
  })], CourseUpgradeChoice);
1974
2024
  var CourseUpgradeChoiceCollection = class extends SmrtCollection {
1975
2025
  static _itemClass = CourseUpgradeChoice;
@@ -2024,7 +2074,7 @@ EnrollmentAutoUpgradeSkip = __decorateClass$7([TenantScoped({ mode: "required" }
2024
2074
  "enrollment_id",
2025
2075
  "release_id"
2026
2076
  ],
2027
- ...closed$6
2077
+ ...closed$7
2028
2078
  })], EnrollmentAutoUpgradeSkip);
2029
2079
  var EnrollmentAutoUpgradeSkipCollection = class extends SmrtCollection {
2030
2080
  static _itemClass = EnrollmentAutoUpgradeSkip;
@@ -2062,7 +2112,7 @@ __decorateClass$7([foreignKey("Learner", { required: true })], EnrollmentAssignm
2062
2112
  EnrollmentAssignmentRecord = __decorateClass$7([TenantScoped({ mode: "required" }), smrt({
2063
2113
  tableName: "enrollment_assignment_records",
2064
2114
  conflictColumns: ["tenant_id", "enrollment_id"],
2065
- ...closed$6
2115
+ ...closed$7
2066
2116
  })], EnrollmentAssignmentRecord);
2067
2117
  var EnrollmentAssignmentRecordCollection = class extends SmrtCollection {
2068
2118
  static _itemClass = EnrollmentAssignmentRecord;
@@ -2100,7 +2150,7 @@ __decorateClass$7([foreignKey("Learner", { required: true })], CurriculumCreatio
2100
2150
  CurriculumCreationRecord = __decorateClass$7([TenantScoped({ mode: "required" }), smrt({
2101
2151
  tableName: "curriculum_creation_records",
2102
2152
  conflictColumns: ["tenant_id", "curriculum_id"],
2103
- ...closed$6
2153
+ ...closed$7
2104
2154
  })], CurriculumCreationRecord);
2105
2155
  var CurriculumCreationRecordCollection = class extends SmrtCollection {
2106
2156
  static _itemClass = CurriculumCreationRecord;
@@ -2133,6 +2183,646 @@ function delegationRecordFields(claim) {
2133
2183
  };
2134
2184
  }
2135
2185
  //#endregion
2186
+ //#region src/course-generation-contracts.ts
2187
+ var COURSE_GENERATION_STATUSES = [
2188
+ "queued",
2189
+ "running",
2190
+ "succeeded",
2191
+ "failed",
2192
+ "cancelled",
2193
+ "refused"
2194
+ ];
2195
+ var COURSE_GENERATION_TERMINAL = [
2196
+ "succeeded",
2197
+ "failed",
2198
+ "cancelled",
2199
+ "refused"
2200
+ ];
2201
+ var MAX_REFUSAL_CATEGORY_LENGTH = 100;
2202
+ var MAX_REFUSAL_EXPLANATION_LENGTH = 2e3;
2203
+ var CourseGenerationRefusal = class extends Error {
2204
+ category;
2205
+ explanation;
2206
+ usage;
2207
+ responseModel;
2208
+ constructor(detail = {}) {
2209
+ super("The provider refused to generate the requested material.");
2210
+ this.name = "CourseGenerationRefusal";
2211
+ this.category = detail.category;
2212
+ this.explanation = detail.explanation;
2213
+ this.usage = detail.usage;
2214
+ this.responseModel = detail.responseModel;
2215
+ }
2216
+ };
2217
+ var COURSE_GENERATION_TARGETS = ["course", "lesson"];
2218
+ var MAX_GENERATION_BRIEF_FIELD_CHARS = 200;
2219
+ var MAX_GENERATION_OBJECTIVE_CHARS = 1e3;
2220
+ var MAX_GENERATION_GRADE = 12;
2221
+ var CourseGeneratorNotConfiguredError = class extends CourseAuthoringError {
2222
+ constructor() {
2223
+ super("generator-not-configured", "No course material generator is configured.");
2224
+ }
2225
+ };
2226
+ var CourseGenerationNotFoundError = class extends CourseAuthoringError {
2227
+ constructor() {
2228
+ super("generation-not-found", "Generation job was not found.");
2229
+ }
2230
+ };
2231
+ var CourseGenerationInProgressError = class extends CourseAuthoringError {
2232
+ constructor() {
2233
+ super("generation-in-progress", "Generation is already running.");
2234
+ }
2235
+ };
2236
+ var CourseGenerationOutputError = class extends CourseAuthoringError {
2237
+ constructor(path) {
2238
+ super("invalid-generation-output", "Generated output did not match the closed proposal schema.");
2239
+ this.path = path;
2240
+ }
2241
+ path;
2242
+ };
2243
+ var CourseProposalNotFoundError = class extends CourseAuthoringError {
2244
+ constructor() {
2245
+ super("proposal-not-found", "Proposal was not found.");
2246
+ }
2247
+ };
2248
+ var CourseProposalFieldError = class extends CourseAuthoringError {
2249
+ constructor(fields) {
2250
+ super("proposal-field-not-decidable", "Only undecided conflicting proposal fields can be decided.");
2251
+ this.fields = fields;
2252
+ }
2253
+ fields;
2254
+ };
2255
+ var CourseGenerationBriefError = class extends CourseAuthoringError {
2256
+ constructor(field) {
2257
+ super("invalid-generation-brief", "The generation brief is not valid.");
2258
+ this.field = field;
2259
+ }
2260
+ field;
2261
+ };
2262
+ //#endregion
2263
+ //#region src/group-sharing-hash.ts
2264
+ function groupSharingDigest(value) {
2265
+ const crypto = typeof process !== "undefined" && process.getBuiltinModule?.("node:crypto");
2266
+ if (!crypto) throw new Error("Group sharing operations require a Node server");
2267
+ return crypto.createHash("sha256").update(value).digest("hex");
2268
+ }
2269
+ //#endregion
2270
+ //#region src/question-contracts.ts
2271
+ var QUESTION_PROVENANCES = [
2272
+ "supplied",
2273
+ "inferred",
2274
+ "generated",
2275
+ "edited",
2276
+ "extracted"
2277
+ ];
2278
+ var QUESTION_RESPONSE_TYPES = [
2279
+ "short-answer",
2280
+ "numeric",
2281
+ "multiple-choice",
2282
+ "multi-select",
2283
+ "true-false",
2284
+ "extended",
2285
+ "multi-part"
2286
+ ];
2287
+ var QUESTION_COMPLETION_MODES = [
2288
+ "printable",
2289
+ "online",
2290
+ "interactive"
2291
+ ];
2292
+ var QUESTION_FIELDS = [
2293
+ "prompt",
2294
+ "responseType",
2295
+ "choices",
2296
+ "subparts",
2297
+ "media",
2298
+ "objectives",
2299
+ "concepts",
2300
+ "answerKey",
2301
+ "rubric",
2302
+ "pointsPossible"
2303
+ ];
2304
+ var MAX_RESOLVE_QUESTION_REVISIONS = 200;
2305
+ var QUESTION_PROMPT_SUMMARY_LENGTH = 200;
2306
+ var QuestionNotFoundError = class extends CourseAuthoringError {
2307
+ constructor() {
2308
+ super("question-not-found", "Question was not found.");
2309
+ }
2310
+ };
2311
+ var LearningObjectiveNotFoundError = class extends CourseAuthoringError {
2312
+ constructor() {
2313
+ super("objective-not-found", "Learning objective was not found.");
2314
+ }
2315
+ };
2316
+ var LearningObjectiveKeyConflictError = class extends CourseAuthoringError {
2317
+ constructor() {
2318
+ super("objective-key-conflict", "The objective key already names a different learning objective.");
2319
+ }
2320
+ };
2321
+ var QuestionReferenceError = class extends CourseAuthoringError {
2322
+ constructor(references) {
2323
+ super("question-reference-invalid", "Lesson questions must reference existing immutable question revisions in this tenant.");
2324
+ this.references = references;
2325
+ }
2326
+ references;
2327
+ };
2328
+ var GENERATED_PRINTABLE_AUDIENCES = ["learner", "guardian"];
2329
+ var GENERATED_PRINTABLE_RELATIONSHIPS = {
2330
+ learner: "printable",
2331
+ guardian: "answer-key"
2332
+ };
2333
+ var MAX_PRINTABLE_ITEMS = 200;
2334
+ var MAX_PRINTABLE_TEXT_LENGTH = 1e4;
2335
+ var MAX_PRINTABLE_TOTAL_TEXT = 4e5;
2336
+ var MAX_PRINTABLE_PAGES = 150;
2337
+ var WorksheetVersionMismatchError = class extends CourseAuthoringError {
2338
+ constructor() {
2339
+ super("worksheet-version-mismatch", "The worksheet version is unknown or stale for this lesson.");
2340
+ }
2341
+ };
2342
+ var PrintableStorageNotConfiguredError = class extends CourseAuthoringError {
2343
+ constructor() {
2344
+ super("printable-storage-not-configured", "Generated printables require a configured private asset storage.");
2345
+ }
2346
+ };
2347
+ var PrintableOwnerError = class extends CourseAuthoringError {
2348
+ constructor() {
2349
+ super("printable-owner-unavailable", "No owning profile is available for the generated printable.");
2350
+ }
2351
+ };
2352
+ var PrintableStorageCleanupError = class extends CourseAuthoringError {
2353
+ constructor(assetIds, cause) {
2354
+ super("printable-storage-cleanup", `Printable rollback could not remove fresh assets: ${assetIds.join(", ")}`);
2355
+ this.assetIds = assetIds;
2356
+ this.cause = cause;
2357
+ }
2358
+ assetIds;
2359
+ };
2360
+ //#endregion
2361
+ //#region src/question-content.ts
2362
+ var ID = /^[A-Za-z0-9._:-]{1,80}$/;
2363
+ var CONCEPT = /^[A-Za-z0-9._:/-]{1,200}$/;
2364
+ var bad$1 = (message) => {
2365
+ throw new CourseAuthoringValidationError(message);
2366
+ };
2367
+ var isObject$1 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
2368
+ function onlyKeys(value, allowed, what) {
2369
+ for (const key of Object.keys(value)) if (!allowed.includes(key)) bad$1(`Unknown ${what} field`);
2370
+ }
2371
+ function strings(value, what, max, itemMax) {
2372
+ if (!Array.isArray(value) || value.length > max || value.some((v) => typeof v !== "string" || !v.trim() || v.length > itemMax)) return bad$1(`Invalid ${what}`);
2373
+ return value;
2374
+ }
2375
+ function unique(values, what) {
2376
+ if (new Set(values).size !== values.length) bad$1(`Duplicate ${what}`);
2377
+ return values;
2378
+ }
2379
+ function atom(value, choiceIds, what) {
2380
+ if (!isObject$1(value)) return bad$1(`Invalid ${what}`);
2381
+ onlyKeys(value, [
2382
+ "correctChoiceIds",
2383
+ "acceptedAnswers",
2384
+ "numeric"
2385
+ ], what);
2386
+ const result = {};
2387
+ if (value.correctChoiceIds !== void 0) {
2388
+ const ids = unique(strings(value.correctChoiceIds, `${what} choices`, 50, 80), "correct choice");
2389
+ if (!ids.length || !choiceIds || ids.some((id) => !choiceIds.has(id))) bad$1(`${what} names an unknown choice`);
2390
+ result.correctChoiceIds = ids;
2391
+ }
2392
+ if (value.acceptedAnswers !== void 0) {
2393
+ const answers = strings(value.acceptedAnswers, `${what} answers`, 50, 1e3);
2394
+ if (!answers.length) bad$1(`Invalid ${what} answers`);
2395
+ result.acceptedAnswers = answers;
2396
+ }
2397
+ if (value.numeric !== void 0) {
2398
+ const n = value.numeric;
2399
+ if (!isObject$1(n) || typeof n.value !== "number" || !Number.isFinite(n.value) || n.tolerance !== void 0 && (typeof n.tolerance !== "number" || !Number.isFinite(n.tolerance) || n.tolerance < 0)) return bad$1(`Invalid ${what} number`);
2400
+ onlyKeys(n, ["value", "tolerance"], what);
2401
+ result.numeric = {
2402
+ value: n.value,
2403
+ tolerance: n.tolerance ?? 0
2404
+ };
2405
+ }
2406
+ if (!Object.keys(result).length) bad$1(`${what} cannot be empty`);
2407
+ return result;
2408
+ }
2409
+ function normalizeQuestionContent(input, options = {}) {
2410
+ if (!isObject$1(input)) return bad$1("Invalid question");
2411
+ const prompt = input.prompt;
2412
+ if (typeof prompt !== "string" || !prompt.trim() || prompt.length > 2e4) bad$1("Invalid prompt");
2413
+ const responseType = input.responseType;
2414
+ if (!QUESTION_RESPONSE_TYPES.includes(responseType)) bad$1("Invalid response type");
2415
+ if (!QUESTION_PROVENANCES.includes(input.provenance)) bad$1("Invalid provenance");
2416
+ const hasChoices = ["multiple-choice", "multi-select"].includes(responseType);
2417
+ const choices = input.choices ?? [];
2418
+ if (!Array.isArray(choices) || choices.length > 50) bad$1("Invalid choices");
2419
+ if (hasChoices ? choices.length < 2 : choices.length) bad$1(hasChoices ? "Choice questions need at least two choices" : "Choices are only for choice questions");
2420
+ const normalizedChoices = choices.map((c) => {
2421
+ if (!isObject$1(c)) return bad$1("Invalid choice");
2422
+ onlyKeys(c, ["id", "label"], "choice");
2423
+ if (typeof c.id !== "string" || !ID.test(c.id) || typeof c.label !== "string" || !c.label.trim() || c.label.length > 2e3) bad$1("Invalid choice");
2424
+ return {
2425
+ id: c.id,
2426
+ label: c.label
2427
+ };
2428
+ });
2429
+ unique(normalizedChoices.map((c) => c.id), "choice id");
2430
+ const parts = input.subparts ?? [];
2431
+ if (!Array.isArray(parts) || parts.length > 50) bad$1("Invalid subparts");
2432
+ if (responseType === "multi-part" ? !parts.length : parts.length) bad$1("Subparts are required for, and only for, multi-part questions");
2433
+ const subparts = parts.map((p) => {
2434
+ if (!isObject$1(p)) return bad$1("Invalid subpart");
2435
+ onlyKeys(p, [
2436
+ "partKey",
2437
+ "prompt",
2438
+ "responseType",
2439
+ "pointsPossible"
2440
+ ], "subpart");
2441
+ if (typeof p.partKey !== "string" || !ID.test(p.partKey) || typeof p.prompt !== "string" || !p.prompt.trim() || p.prompt.length > 2e4 || p.responseType !== void 0 && (!QUESTION_RESPONSE_TYPES.includes(p.responseType) || [
2442
+ "multi-part",
2443
+ "multiple-choice",
2444
+ "multi-select"
2445
+ ].includes(p.responseType)) || p.pointsPossible !== void 0 && (typeof p.pointsPossible !== "number" || !Number.isFinite(p.pointsPossible) || p.pointsPossible <= 0)) bad$1("Invalid subpart");
2446
+ return {
2447
+ partKey: p.partKey,
2448
+ prompt: p.prompt,
2449
+ ...p.responseType ? { responseType: p.responseType } : {},
2450
+ ...p.pointsPossible !== void 0 ? { pointsPossible: p.pointsPossible } : {}
2451
+ };
2452
+ });
2453
+ unique(subparts.map((p) => p.partKey), "subpart key");
2454
+ const mediaIn = input.media ?? [];
2455
+ if (!Array.isArray(mediaIn) || mediaIn.length > 20) bad$1("Invalid media");
2456
+ const media = mediaIn.map((m) => {
2457
+ if (!isObject$1(m)) return bad$1("Invalid media");
2458
+ onlyKeys(m, [
2459
+ "assetId",
2460
+ "role",
2461
+ "alt"
2462
+ ], "media");
2463
+ if (typeof m.assetId !== "string" || !m.assetId || m.assetId.length > 200 || m.role !== void 0 && ![
2464
+ "figure",
2465
+ "passage",
2466
+ "audio",
2467
+ "other"
2468
+ ].includes(m.role) || m.alt !== void 0 && (typeof m.alt !== "string" || m.alt.length > 500)) bad$1("Invalid media");
2469
+ return {
2470
+ assetId: m.assetId,
2471
+ role: m.role ?? "other",
2472
+ alt: m.alt ?? ""
2473
+ };
2474
+ });
2475
+ const objectiveIds = unique(strings(input.objectiveIds ?? [], "objectives", 50, 200), "objective");
2476
+ const conceptIds = unique(strings(input.conceptIds ?? [], "concepts", 50, 200), "concept");
2477
+ for (const id of conceptIds) if (!CONCEPT.test(id) || options.conceptVocabulary && !options.conceptVocabulary.has(id)) bad$1("Unknown concept");
2478
+ let answerKey = null;
2479
+ if (input.answerKey !== void 0 && input.answerKey !== null) {
2480
+ const key = input.answerKey;
2481
+ if (!isObject$1(key)) return bad$1("Invalid answer key");
2482
+ onlyKeys(key, [
2483
+ "correctChoiceIds",
2484
+ "acceptedAnswers",
2485
+ "numeric",
2486
+ "parts",
2487
+ "explanation"
2488
+ ], "answer key");
2489
+ const choiceIds = hasChoices ? new Set(normalizedChoices.map((c) => c.id)) : null;
2490
+ const { parts: keyParts, explanation, ...rest } = key;
2491
+ const normalized = Object.keys(rest).length ? atom(rest, choiceIds, "answer key") : {};
2492
+ if (keyParts !== void 0) {
2493
+ if (!isObject$1(keyParts) || !Object.keys(keyParts).length) return bad$1("Invalid answer key parts");
2494
+ const known = new Set(subparts.map((p) => p.partKey));
2495
+ normalized.parts = Object.fromEntries(Object.entries(keyParts).map(([partKey, value]) => {
2496
+ if (!known.has(partKey)) bad$1("Answer key names an unknown subpart");
2497
+ return [partKey, atom(value, null, "answer key part")];
2498
+ }));
2499
+ }
2500
+ if (explanation !== void 0) {
2501
+ if (typeof explanation !== "string" || !explanation.trim() || explanation.length > 5e3) bad$1("Invalid answer explanation");
2502
+ normalized.explanation = explanation;
2503
+ }
2504
+ if (!normalized.correctChoiceIds && !normalized.acceptedAnswers && !normalized.numeric && !normalized.parts) bad$1("An answer key cannot be empty; omit it when unknown");
2505
+ if (responseType === "multiple-choice" && normalized.correctChoiceIds && normalized.correctChoiceIds.length !== 1) bad$1("A multiple-choice key names exactly one choice");
2506
+ answerKey = normalized;
2507
+ }
2508
+ let rubric = null;
2509
+ if (input.rubric !== void 0 && input.rubric !== null) try {
2510
+ rubric = parseQuestionRubric(input.rubric);
2511
+ } catch (error) {
2512
+ bad$1(error.message);
2513
+ }
2514
+ let pointsPossible = null;
2515
+ if (input.pointsPossible !== void 0 && input.pointsPossible !== null) {
2516
+ if (typeof input.pointsPossible !== "number" || !Number.isFinite(input.pointsPossible) || input.pointsPossible <= 0) bad$1("Invalid points possible");
2517
+ pointsPossible = input.pointsPossible;
2518
+ }
2519
+ if (rubric && pointsPossible !== null && rubric.criteria.reduce((s, c) => s + c.maxPoints, 0) !== pointsPossible) bad$1("Points possible must equal the rubric total");
2520
+ const present = {
2521
+ prompt: true,
2522
+ responseType: true,
2523
+ choices: normalizedChoices.length > 0,
2524
+ subparts: subparts.length > 0,
2525
+ media: media.length > 0,
2526
+ objectives: objectiveIds.length > 0,
2527
+ concepts: conceptIds.length > 0,
2528
+ answerKey: answerKey !== null,
2529
+ rubric: rubric !== null,
2530
+ pointsPossible: pointsPossible !== null
2531
+ };
2532
+ const explicit = input.fieldProvenance ?? {};
2533
+ if (!isObject$1(explicit)) return bad$1("Invalid field provenance");
2534
+ const fieldProvenance = {};
2535
+ for (const [field, value] of Object.entries(explicit)) if (!QUESTION_FIELDS.includes(field) || !QUESTION_PROVENANCES.includes(value) || !present[field]) bad$1("Provenance may only describe a present field");
2536
+ for (const field of QUESTION_FIELDS) if (present[field]) fieldProvenance[field] = explicit[field] ?? input.provenance;
2537
+ return {
2538
+ prompt,
2539
+ responseType,
2540
+ choices: normalizedChoices,
2541
+ subparts,
2542
+ media,
2543
+ objectiveIds,
2544
+ conceptIds,
2545
+ answerKey,
2546
+ rubric,
2547
+ pointsPossible,
2548
+ provenance: input.provenance,
2549
+ fieldProvenance
2550
+ };
2551
+ }
2552
+ //#endregion
2553
+ //#region src/course-generation-output.ts
2554
+ var KEY$2 = /^[A-Za-z0-9._:-]{1,120}$/;
2555
+ var hasControl = (value) => {
2556
+ for (let i = 0; i < value.length; i++) {
2557
+ const code = value.charCodeAt(i);
2558
+ if (code < 32 && code !== 9 && code !== 10 && code !== 13 || code === 127) return true;
2559
+ }
2560
+ return false;
2561
+ };
2562
+ function closed$6(value, allowed, path) {
2563
+ if (!value || typeof value !== "object" || Array.isArray(value)) throw new CourseGenerationOutputError(path);
2564
+ for (const key of Object.keys(value)) if (!allowed.includes(key)) throw new CourseGenerationOutputError(`${path}.${key}`);
2565
+ return value;
2566
+ }
2567
+ function str(value, path, max) {
2568
+ if (typeof value !== "string" || !value.trim() || value.length > max) throw new CourseGenerationOutputError(path);
2569
+ if (hasControl(value)) throw new CourseGenerationOutputError(path);
2570
+ return value;
2571
+ }
2572
+ function int(value, path, max) {
2573
+ if (!Number.isSafeInteger(value) || value < 1 || value > max) throw new CourseGenerationOutputError(path);
2574
+ return value;
2575
+ }
2576
+ var MAX_GENERATED_QUESTIONS = 50;
2577
+ var QUESTION_KEYS = [
2578
+ "prompt",
2579
+ "responseType",
2580
+ "choices",
2581
+ "subparts",
2582
+ "answerKey",
2583
+ "rubric",
2584
+ "pointsPossible"
2585
+ ];
2586
+ var sameText = (a, b) => a.trim().replace(/\s+/g, " ") === b.trim().replace(/\s+/g, " ");
2587
+ function question(raw, path) {
2588
+ const q = closed$6(raw, QUESTION_KEYS, path);
2589
+ if (q.answerKey === void 0 || q.answerKey === null) throw new CourseGenerationOutputError(`${path}.answerKey`);
2590
+ try {
2591
+ normalizeQuestionContent({
2592
+ ...q,
2593
+ provenance: "generated"
2594
+ });
2595
+ } catch {
2596
+ throw new CourseGenerationOutputError(path);
2597
+ }
2598
+ return q;
2599
+ }
2600
+ function validateCourseGenerationOutput(output, existingLessonKeys, brief) {
2601
+ const lessonTarget = brief?.target === "lesson";
2602
+ const root = closed$6(output, [
2603
+ "course",
2604
+ "lessons",
2605
+ "rationale"
2606
+ ], "output");
2607
+ const courseRaw = root.course === void 0 ? {} : closed$6(root.course, [
2608
+ "summary",
2609
+ "subject",
2610
+ "objectives"
2611
+ ], "course");
2612
+ const course = {};
2613
+ if (courseRaw.summary !== void 0) course.summary = str(courseRaw.summary, "course.summary", 2e4);
2614
+ if (courseRaw.subject !== void 0) course.subject = str(courseRaw.subject, "course.subject", 200);
2615
+ if (courseRaw.objectives !== void 0) {
2616
+ if (!Array.isArray(courseRaw.objectives) || courseRaw.objectives.length < 1 || courseRaw.objectives.length > 50) throw new CourseGenerationOutputError("course.objectives");
2617
+ course.objectives = courseRaw.objectives.map((o, i) => str(o, `course.objectives.${i}`, 1e3));
2618
+ }
2619
+ const lessonsRaw = root.lessons ?? [];
2620
+ if (!Array.isArray(lessonsRaw) || lessonsRaw.length > 50) throw new CourseGenerationOutputError("lessons");
2621
+ if (lessonTarget) {
2622
+ if (Object.keys(course).length) throw new CourseGenerationOutputError("course");
2623
+ if (lessonsRaw.length !== 1) throw new CourseGenerationOutputError("lessons");
2624
+ }
2625
+ const seen = /* @__PURE__ */ new Set();
2626
+ return {
2627
+ course,
2628
+ lessons: lessonsRaw.map((raw, i) => {
2629
+ const path = `lessons.${i}`;
2630
+ const l = closed$6(raw, [
2631
+ "lessonKey",
2632
+ "sectionKey",
2633
+ "sectionTitle",
2634
+ "title",
2635
+ "body",
2636
+ "estimatedMinutes",
2637
+ "difficulty",
2638
+ "objective",
2639
+ "questions"
2640
+ ], path);
2641
+ if (!lessonTarget && (l.objective !== void 0 || l.questions !== void 0)) throw new CourseGenerationOutputError(`${path}.${l.objective !== void 0 ? "objective" : "questions"}`);
2642
+ const lesson = {};
2643
+ if (l.lessonKey !== void 0) {
2644
+ if (typeof l.lessonKey !== "string" || !KEY$2.test(l.lessonKey) || !existingLessonKeys.has(l.lessonKey) || seen.has(l.lessonKey)) throw new CourseGenerationOutputError(`${path}.lessonKey`);
2645
+ seen.add(l.lessonKey);
2646
+ lesson.lessonKey = l.lessonKey;
2647
+ if (l.sectionKey !== void 0 || l.sectionTitle !== void 0) throw new CourseGenerationOutputError(`${path}.sectionKey`);
2648
+ } else {
2649
+ if (typeof l.sectionKey !== "string" || !KEY$2.test(l.sectionKey)) throw new CourseGenerationOutputError(`${path}.sectionKey`);
2650
+ lesson.sectionKey = l.sectionKey;
2651
+ if (l.sectionTitle !== void 0) lesson.sectionTitle = str(l.sectionTitle, `${path}.sectionTitle`, 300);
2652
+ if (l.title === void 0) throw new CourseGenerationOutputError(`${path}.title`);
2653
+ }
2654
+ if (l.title !== void 0) lesson.title = str(l.title, `${path}.title`, 300);
2655
+ if (l.body !== void 0) lesson.body = str(l.body, `${path}.body`, 1e5);
2656
+ if (l.estimatedMinutes !== void 0) lesson.estimatedMinutes = int(l.estimatedMinutes, `${path}.estimatedMinutes`, 1440);
2657
+ if (l.difficulty !== void 0) lesson.difficulty = int(l.difficulty, `${path}.difficulty`, 10);
2658
+ if (lessonTarget) {
2659
+ if (lesson.lessonKey || lesson.body === void 0) throw new CourseGenerationOutputError(`${path}.${lesson.lessonKey ? "lessonKey" : "body"}`);
2660
+ if (l.objective !== void 0) {
2661
+ if (!sameText(str(l.objective, `${path}.objective`, 1e3), brief?.objective ?? "")) throw new CourseGenerationOutputError(`${path}.objective`);
2662
+ }
2663
+ lesson.objective = brief?.objective ?? "";
2664
+ if (!Array.isArray(l.questions) || l.questions.length < 1 || l.questions.length > MAX_GENERATED_QUESTIONS) throw new CourseGenerationOutputError(`${path}.questions`);
2665
+ lesson.questions = l.questions.map((q, n) => question(q, `${path}.questions.${n}`));
2666
+ }
2667
+ if (lesson.lessonKey && Object.keys(lesson).length === 1) throw new CourseGenerationOutputError(path);
2668
+ return lesson;
2669
+ }),
2670
+ rationale: root.rationale === void 0 ? "" : str(root.rationale, "rationale", 4e3)
2671
+ };
2672
+ }
2673
+ var isEmpty = (value) => value === void 0 || value === null || value === "" || value === 0 || Array.isArray(value) && value.length === 0;
2674
+ var same = (a, b) => JSON.stringify(a) === JSON.stringify(b);
2675
+ function classify(supplied, proposed) {
2676
+ if (same(supplied, proposed)) return "unchanged";
2677
+ return isEmpty(supplied) ? "filled" : "conflict";
2678
+ }
2679
+ function planProposal(base, proposal) {
2680
+ const plan = [];
2681
+ for (const key of [
2682
+ "summary",
2683
+ "subject",
2684
+ "objectives"
2685
+ ]) {
2686
+ const proposed = proposal.course[key];
2687
+ if (proposed === void 0) continue;
2688
+ const supplied = base[key];
2689
+ plan.push({
2690
+ field: `course.${key}`,
2691
+ status: classify(supplied, proposed),
2692
+ supplied: isEmpty(supplied) ? null : supplied,
2693
+ proposed
2694
+ });
2695
+ }
2696
+ const lessons = new Map(base.sections.flatMap((s) => s.lessons).map((l) => [l.lessonKey, l]));
2697
+ let added = 0;
2698
+ for (const lesson of proposal.lessons) if (lesson.lessonKey) {
2699
+ const current = lessons.get(lesson.lessonKey);
2700
+ if (!current) continue;
2701
+ for (const key of [
2702
+ "title",
2703
+ "body",
2704
+ "estimatedMinutes",
2705
+ "difficulty"
2706
+ ]) {
2707
+ const proposed = lesson[key];
2708
+ if (proposed === void 0) continue;
2709
+ const supplied = current[key];
2710
+ plan.push({
2711
+ field: `lesson:${lesson.lessonKey}.${key}`,
2712
+ status: classify(supplied, proposed),
2713
+ supplied: isEmpty(supplied) ? null : supplied,
2714
+ proposed
2715
+ });
2716
+ }
2717
+ } else plan.push({
2718
+ field: `new-lesson:${added++}`,
2719
+ status: "added",
2720
+ supplied: null,
2721
+ proposed: lesson
2722
+ });
2723
+ return plan;
2724
+ }
2725
+ var generatedLessonKey = (jobId, index) => `g-${groupSharingDigest(JSON.stringify([jobId, index])).slice(0, 32)}`;
2726
+ function draftFromRevision(view) {
2727
+ return {
2728
+ title: view.title,
2729
+ summary: view.summary,
2730
+ subject: view.subject,
2731
+ theme: view.theme,
2732
+ levelMin: view.levelMin,
2733
+ levelMax: view.levelMax,
2734
+ objectives: [...view.objectives],
2735
+ provenance: view.provenance,
2736
+ fieldProvenance: { ...view.fieldProvenance },
2737
+ sections: view.sections.map((section) => ({
2738
+ sectionKey: section.sectionKey,
2739
+ title: section.title,
2740
+ lessons: section.lessons.map((l) => ({
2741
+ lessonKey: l.lessonKey,
2742
+ title: l.title,
2743
+ body: l.body,
2744
+ lessonType: l.lessonType,
2745
+ completionRule: l.completionRule,
2746
+ estimatedMinutes: l.estimatedMinutes,
2747
+ difficulty: l.difficulty,
2748
+ modality: l.modality,
2749
+ externalRef: l.externalRef,
2750
+ externalUrl: l.externalUrl,
2751
+ provenance: l.provenance,
2752
+ fieldProvenance: { ...l.fieldProvenance },
2753
+ materials: l.materials.map((m) => ({
2754
+ assetId: m.assetId,
2755
+ relationship: m.relationship,
2756
+ licenseKind: m.licenseKind
2757
+ })),
2758
+ questions: l.questions,
2759
+ layoutParams: l.layoutParams
2760
+ }))
2761
+ }))
2762
+ };
2763
+ }
2764
+ function applyProposalFields(draft, entries, options) {
2765
+ const missing = [];
2766
+ for (const entry of entries) {
2767
+ const [head, rest] = [entry.field.split(".", 1)[0], entry.field];
2768
+ if (head === "course") {
2769
+ const key = entry.field.slice(7);
2770
+ draft[key] = entry.proposed;
2771
+ draft.fieldProvenance = {
2772
+ ...draft.fieldProvenance,
2773
+ [key]: "generated"
2774
+ };
2775
+ } else if (rest.startsWith("lesson:")) {
2776
+ const dot = rest.lastIndexOf(".");
2777
+ const lessonKey = rest.slice(7, dot);
2778
+ const key = rest.slice(dot + 1);
2779
+ const lesson = draft.sections.flatMap((s) => s.lessons).find((l) => l.lessonKey === lessonKey);
2780
+ if (!lesson) {
2781
+ missing.push(entry.field);
2782
+ continue;
2783
+ }
2784
+ lesson[key] = entry.proposed;
2785
+ lesson.provenance = "edited";
2786
+ lesson.fieldProvenance = {
2787
+ ...lesson.fieldProvenance,
2788
+ [key]: "generated"
2789
+ };
2790
+ } else if (rest.startsWith("new-lesson:")) {
2791
+ const index = Number(rest.slice(11));
2792
+ const proposed = entry.proposed;
2793
+ const sectionKey = proposed.sectionKey;
2794
+ let section = draft.sections.find((s) => s.sectionKey === sectionKey);
2795
+ if (!section) {
2796
+ section = {
2797
+ sectionKey,
2798
+ title: proposed.sectionTitle ?? sectionKey,
2799
+ lessons: []
2800
+ };
2801
+ draft.sections.push(section);
2802
+ }
2803
+ const generated = {
2804
+ title: "generated",
2805
+ body: "generated",
2806
+ estimatedMinutes: "generated",
2807
+ difficulty: "generated"
2808
+ };
2809
+ const refs = options.questionRefs?.get(index);
2810
+ section.lessons.push({
2811
+ lessonKey: generatedLessonKey(options.jobId, index),
2812
+ ...refs?.length ? { lessonType: "worksheet" } : {},
2813
+ ...refs?.length ? { questions: refs } : {},
2814
+ title: proposed.title,
2815
+ ...proposed.body === void 0 ? {} : { body: proposed.body },
2816
+ ...proposed.estimatedMinutes === void 0 ? {} : { estimatedMinutes: proposed.estimatedMinutes },
2817
+ ...proposed.difficulty === void 0 ? {} : { difficulty: proposed.difficulty },
2818
+ provenance: "generated",
2819
+ fieldProvenance: Object.fromEntries(Object.keys(generated).filter((k) => k === "title" || proposed[k] !== void 0).map((k) => [k, "generated"]))
2820
+ });
2821
+ }
2822
+ }
2823
+ return { missing };
2824
+ }
2825
+ //#endregion
2136
2826
  //#region src/course-release-scope.ts
2137
2827
  async function resolveEnrollmentLessons(db, tenantId, enrollmentId) {
2138
2828
  const pins = (await (await EnrollmentReleasePinCollection.create({ db })).list({
@@ -2243,104 +2933,6 @@ async function authoredVisibilityPermitsGroups(db, tenantId, courseId) {
2243
2933
  return stored === void 0 || stored === null || stored === "" || stored === "groups" || stored === "public";
2244
2934
  }
2245
2935
  //#endregion
2246
- //#region src/group-sharing-hash.ts
2247
- function groupSharingDigest(value) {
2248
- const crypto = typeof process !== "undefined" && process.getBuiltinModule?.("node:crypto");
2249
- if (!crypto) throw new Error("Group sharing operations require a Node server");
2250
- return crypto.createHash("sha256").update(value).digest("hex");
2251
- }
2252
- //#endregion
2253
- //#region src/question-contracts.ts
2254
- var QUESTION_PROVENANCES = [
2255
- "supplied",
2256
- "inferred",
2257
- "generated",
2258
- "edited",
2259
- "extracted"
2260
- ];
2261
- var QUESTION_RESPONSE_TYPES = [
2262
- "short-answer",
2263
- "numeric",
2264
- "multiple-choice",
2265
- "multi-select",
2266
- "true-false",
2267
- "extended",
2268
- "multi-part"
2269
- ];
2270
- var QUESTION_COMPLETION_MODES = [
2271
- "printable",
2272
- "online",
2273
- "interactive"
2274
- ];
2275
- var QUESTION_FIELDS = [
2276
- "prompt",
2277
- "responseType",
2278
- "choices",
2279
- "subparts",
2280
- "media",
2281
- "objectives",
2282
- "concepts",
2283
- "answerKey",
2284
- "rubric",
2285
- "pointsPossible"
2286
- ];
2287
- var MAX_RESOLVE_QUESTION_REVISIONS = 200;
2288
- var QUESTION_PROMPT_SUMMARY_LENGTH = 200;
2289
- var QuestionNotFoundError = class extends CourseAuthoringError {
2290
- constructor() {
2291
- super("question-not-found", "Question was not found.");
2292
- }
2293
- };
2294
- var LearningObjectiveNotFoundError = class extends CourseAuthoringError {
2295
- constructor() {
2296
- super("objective-not-found", "Learning objective was not found.");
2297
- }
2298
- };
2299
- var LearningObjectiveKeyConflictError = class extends CourseAuthoringError {
2300
- constructor() {
2301
- super("objective-key-conflict", "The objective key already names a different learning objective.");
2302
- }
2303
- };
2304
- var QuestionReferenceError = class extends CourseAuthoringError {
2305
- constructor(references) {
2306
- super("question-reference-invalid", "Lesson questions must reference existing immutable question revisions in this tenant.");
2307
- this.references = references;
2308
- }
2309
- references;
2310
- };
2311
- var GENERATED_PRINTABLE_AUDIENCES = ["learner", "guardian"];
2312
- var GENERATED_PRINTABLE_RELATIONSHIPS = {
2313
- learner: "printable",
2314
- guardian: "answer-key"
2315
- };
2316
- var MAX_PRINTABLE_ITEMS = 200;
2317
- var MAX_PRINTABLE_TEXT_LENGTH = 1e4;
2318
- var MAX_PRINTABLE_TOTAL_TEXT = 4e5;
2319
- var MAX_PRINTABLE_PAGES = 150;
2320
- var WorksheetVersionMismatchError = class extends CourseAuthoringError {
2321
- constructor() {
2322
- super("worksheet-version-mismatch", "The worksheet version is unknown or stale for this lesson.");
2323
- }
2324
- };
2325
- var PrintableStorageNotConfiguredError = class extends CourseAuthoringError {
2326
- constructor() {
2327
- super("printable-storage-not-configured", "Generated printables require a configured private asset storage.");
2328
- }
2329
- };
2330
- var PrintableOwnerError = class extends CourseAuthoringError {
2331
- constructor() {
2332
- super("printable-owner-unavailable", "No owning profile is available for the generated printable.");
2333
- }
2334
- };
2335
- var PrintableStorageCleanupError = class extends CourseAuthoringError {
2336
- constructor(assetIds, cause) {
2337
- super("printable-storage-cleanup", `Printable rollback could not remove fresh assets: ${assetIds.join(", ")}`);
2338
- this.assetIds = assetIds;
2339
- this.cause = cause;
2340
- }
2341
- assetIds;
2342
- };
2343
- //#endregion
2344
2936
  //#region src/course-authoring-service.ts
2345
2937
  var NAMESPACE$1 = "smrt.education.course-authoring.v1";
2346
2938
  var KEY_PATTERN$1 = /^[A-Za-z0-9._:@/-]{1,200}$/;
@@ -2436,6 +3028,12 @@ function resolvePolicyBasis(decided, supplied) {
2436
3028
  if (basis === null) throw new CourseAuthoringValidationError("policy basis is required");
2437
3029
  return basis;
2438
3030
  }
3031
+ function resolveOptionalPolicyBasis(decided, supplied) {
3032
+ const fromHost = basisValue(decided);
3033
+ const fromCaller = basisValue(supplied);
3034
+ if (fromHost !== null && fromCaller !== null && fromHost !== fromCaller) throw new CoursePolicyBasisMismatchError();
3035
+ return fromHost ?? fromCaller ?? "";
3036
+ }
2439
3037
  function pageLimit(value) {
2440
3038
  if (value === void 0) return 50;
2441
3039
  if (!Number.isSafeInteger(value) || value < 1) throw new CourseAuthoringValidationError("Invalid page limit");
@@ -3419,14 +4017,135 @@ var CourseAuthoringService = class {
3419
4017
  });
3420
4018
  }
3421
4019
  /**
4020
+ * Appends exactly one lesson (with its materials and question records) to an
4021
+ * existing released course as a new revision and, unless `release: false`,
4022
+ * releases that revision in the same transaction. Append-only: no existing
4023
+ * lesson, section order or revision is changed; the new revision is the old
4024
+ * head plus the lesson.
4025
+ *
4026
+ * Refused: a foreign or unknown course (`CourseNotFoundError`), a discarded
4027
+ * one, an archived one (`CourseArchivedError`), a course with no release yet
4028
+ * (`CourseAppendRefusedError` `not-released`), a head holding unreleased
4029
+ * changes (`unreleased-changes`, so nothing unreviewed rides along), a
4030
+ * `lessonKey` already in the course (`duplicate-lesson`), and a head that moved
4031
+ * since `expectedHeadRevisionId` (`CourseStaleHeadError`). Idempotent by
4032
+ * `requestKey`: the same request replays the original result; a changed
4033
+ * lesson, section or release intent (`release` true versus false) under the
4034
+ * same key is `CourseRequestKeyConflictError`; release a `release: false`
4035
+ * append later with `release` and its own key.
4036
+ *
4037
+ * Authority is unchanged: `education.course.author` (operation
4038
+ * `append-lesson`, plus `generate` for generated provenance) and, when
4039
+ * releasing, `education.course.release` through the same review gate,
4040
+ * `policyBasis` and delegation rules as `release`; a gate that is not current
4041
+ * rolls the whole append back. Enrollments keep their pinned release: the new
4042
+ * release is a new Course row, so an enrollment sees the lesson only when the
4043
+ * host moves it with `upgradeEnrollment`, or it opted in with `setAutoUpgrade`
4044
+ * (then `release` moves it, unstarted lessons only, as for any release).
4045
+ */
4046
+ async appendLesson(request) {
4047
+ const requestKey = requireKey$1(request.requestKey);
4048
+ const expected = requireKey$1(request.expectedHeadRevisionId, "expected head");
4049
+ const sectionKey = requireKey$1(request.sectionKey, "section key");
4050
+ const lessonKey = requireKey$1(request.lesson?.lessonKey, "lesson key");
4051
+ return this.run(async (tx) => {
4052
+ await this.authorize(tx, request, "education.course.author", "append-lesson", { courseId: request.courseId });
4053
+ await this.writableDraft(tx, request.tenantId, request.courseId);
4054
+ const revisions = await CourseRevisionCollection.create({ db: tx });
4055
+ const stored = await revisions.get({
4056
+ tenantId: request.tenantId,
4057
+ courseId: request.courseId,
4058
+ requestKey
4059
+ }, { cache: false });
4060
+ const baseId = stored ? String(stored.parentRevisionId ?? "") : expected;
4061
+ if (stored && !baseId) throw new CourseRequestKeyConflictError();
4062
+ const base = stored ? await revisions.get({
4063
+ id: baseId,
4064
+ tenantId: request.tenantId,
4065
+ courseId: request.courseId
4066
+ }, { cache: false }) : await this.headRevision(tx, request, expected);
4067
+ if (!base) throw new CourseNotFoundError();
4068
+ if (!stored) {
4069
+ if (await this.state(tx, request.tenantId, request.courseId) === "archived") throw new CourseArchivedError();
4070
+ const releases = await tx.list("course_releases", {
4071
+ tenant_id: request.tenantId,
4072
+ course_id: request.courseId
4073
+ });
4074
+ if (!releases.length) throw new CourseAppendRefusedError("not-released");
4075
+ if (!releases.some((r) => String(r.revision_id) === String(base.id))) throw new CourseAppendRefusedError("unreleased-changes");
4076
+ }
4077
+ const view = await this.revisionView(tx, request.tenantId, base);
4078
+ if (view.sections.some((sec) => sec.lessons.some((l) => l.lessonKey === lessonKey))) throw stored ? new CourseRequestKeyConflictError() : new CourseAppendRefusedError("duplicate-lesson");
4079
+ const draft = draftFromRevision(view);
4080
+ let section = draft.sections.find((sec) => sec.sectionKey === sectionKey);
4081
+ if (!section) {
4082
+ if (!request.sectionTitle) throw new CourseAuthoringValidationError("A new section needs a sectionTitle");
4083
+ section = {
4084
+ sectionKey,
4085
+ title: request.sectionTitle,
4086
+ lessons: []
4087
+ };
4088
+ draft.sections.push(section);
4089
+ }
4090
+ section.lessons.push({
4091
+ ...request.lesson,
4092
+ lessonKey
4093
+ });
4094
+ const saved = await this.saveDraftIn(tx, {
4095
+ ...request,
4096
+ expectedHeadRevisionId: String(base.id),
4097
+ requestKey,
4098
+ draft
4099
+ });
4100
+ if (stored) {
4101
+ const released = await tx.get("course_releases", {
4102
+ tenant_id: request.tenantId,
4103
+ course_id: request.courseId,
4104
+ request_key: requestKey
4105
+ });
4106
+ if (request.release === false !== !released) throw new CourseRequestKeyConflictError();
4107
+ }
4108
+ const release = request.release === false ? null : await this.releaseIn(tx, {
4109
+ ...request,
4110
+ revisionId: saved.revisionId,
4111
+ requestKey
4112
+ });
4113
+ return {
4114
+ courseId: request.courseId,
4115
+ revisionId: saved.revisionId,
4116
+ version: saved.version,
4117
+ lessonKey,
4118
+ release
4119
+ };
4120
+ });
4121
+ }
4122
+ /** The head revision row, requiring it to be the revision the caller saw. */
4123
+ async headRevision(tx, request, expected) {
4124
+ const head = await this.headView(tx, request.tenantId, request.courseId);
4125
+ if (head.headRevisionId !== expected) throw new CourseStaleHeadError(head.headRevisionId);
4126
+ return (await CourseRevisionCollection.create({ db: tx })).get({
4127
+ id: expected,
4128
+ tenantId: request.tenantId,
4129
+ courseId: request.courseId
4130
+ }, { cache: false });
4131
+ }
4132
+ /**
3422
4133
  * Freezes one revision for assignment. Requires a current review of that exact
3423
4134
  * revision from the injected gate; materializes immutable Course, section and
3424
- * Lesson rows for this release only.
4135
+ * Lesson rows for this release only. An optional `policyBasis` (or one in the
4136
+ * authorizer's decision, which must then agree) records the host setting the
4137
+ * release relied on, for example a per-learner auto-approve; it grants nothing
4138
+ * and never bypasses the gate, the authorizer or the delegation check.
3425
4139
  */
3426
4140
  async release(request) {
4141
+ requireKey$1(request.requestKey);
4142
+ return this.run((tx) => this.releaseIn(tx, request));
4143
+ }
4144
+ /** The release path on a caller-supplied transaction (see `appendLesson`). */
4145
+ async releaseIn(tx, request) {
3427
4146
  const requestKey = requireKey$1(request.requestKey);
3428
- return this.run(async (tx) => {
3429
- await this.authorize(tx, request, "education.course.release", "release", { courseId: request.courseId });
4147
+ {
4148
+ const policyBasis = resolveOptionalPolicyBasis((await this.authorize(tx, request, "education.course.release", "release", { courseId: request.courseId })).policyBasis, request.policyBasis);
3430
4149
  await this.writableDraft(tx, request.tenantId, request.courseId);
3431
4150
  const releases = await CourseReleaseCollection.create({ db: tx });
3432
4151
  const replay = await releases.get({
@@ -3435,7 +4154,7 @@ var CourseAuthoringService = class {
3435
4154
  requestKey
3436
4155
  }, { cache: false });
3437
4156
  if (replay) {
3438
- if (replay.revisionId !== request.revisionId || replay.delegationId !== (request.delegationId ?? "")) throw new CourseRequestKeyConflictError();
4157
+ if (replay.revisionId !== request.revisionId || replay.delegationId !== (request.delegationId ?? "") || (replay.policyBasis ?? "") !== policyBasis) throw new CourseRequestKeyConflictError();
3439
4158
  return this.releaseView(tx, request.tenantId, replay);
3440
4159
  }
3441
4160
  if (await this.state(tx, request.tenantId, request.courseId) === "archived") throw new CourseArchivedError();
@@ -3472,11 +4191,12 @@ var CourseAuthoringService = class {
3472
4191
  releaseNumber,
3473
4192
  releaseCourseId,
3474
4193
  reviewId: review.reviewId ?? "",
3475
- requestKey
4194
+ requestKey,
4195
+ policyBasis
3476
4196
  });
3477
4197
  await this.autoUpgrade(tx, request.tenantId, request.courseId, release);
3478
4198
  return this.releaseView(tx, request.tenantId, release);
3479
- });
4199
+ }
3480
4200
  }
3481
4201
  async materialize(tx, actor, input) {
3482
4202
  const { view } = input;
@@ -3513,6 +4233,7 @@ var CourseAuthoringService = class {
3513
4233
  reviewId: input.reviewId,
3514
4234
  actorUserId: actor.actorUserId,
3515
4235
  ...delegationRecordFields(actor),
4236
+ policyBasis: input.policyBasis,
3516
4237
  requestKey: input.requestKey,
3517
4238
  recordedAt: /* @__PURE__ */ new Date()
3518
4239
  });
@@ -3620,6 +4341,7 @@ var CourseAuthoringService = class {
3620
4341
  actorUserId: release.actorUserId,
3621
4342
  delegatingGuardianUserId: optional(release.delegatingGuardianUserId),
3622
4343
  delegationId: optional(release.delegationId),
4344
+ policyBasis: optional(release.policyBasis),
3623
4345
  recordedAt: iso$1(release.recordedAt),
3624
4346
  lessons: lessons.sort((a, b) => a.lessonKey.localeCompare(b.lessonKey))
3625
4347
  };
@@ -4320,198 +5042,6 @@ var CourseAuthoringService = class {
4320
5042
  }
4321
5043
  };
4322
5044
  //#endregion
4323
- //#region src/question-content.ts
4324
- var ID = /^[A-Za-z0-9._:-]{1,80}$/;
4325
- var CONCEPT = /^[A-Za-z0-9._:/-]{1,200}$/;
4326
- var bad$1 = (message) => {
4327
- throw new CourseAuthoringValidationError(message);
4328
- };
4329
- var isObject$1 = (v) => !!v && typeof v === "object" && !Array.isArray(v);
4330
- function onlyKeys(value, allowed, what) {
4331
- for (const key of Object.keys(value)) if (!allowed.includes(key)) bad$1(`Unknown ${what} field`);
4332
- }
4333
- function strings(value, what, max, itemMax) {
4334
- if (!Array.isArray(value) || value.length > max || value.some((v) => typeof v !== "string" || !v.trim() || v.length > itemMax)) return bad$1(`Invalid ${what}`);
4335
- return value;
4336
- }
4337
- function unique(values, what) {
4338
- if (new Set(values).size !== values.length) bad$1(`Duplicate ${what}`);
4339
- return values;
4340
- }
4341
- function atom(value, choiceIds, what) {
4342
- if (!isObject$1(value)) return bad$1(`Invalid ${what}`);
4343
- onlyKeys(value, [
4344
- "correctChoiceIds",
4345
- "acceptedAnswers",
4346
- "numeric"
4347
- ], what);
4348
- const result = {};
4349
- if (value.correctChoiceIds !== void 0) {
4350
- const ids = unique(strings(value.correctChoiceIds, `${what} choices`, 50, 80), "correct choice");
4351
- if (!ids.length || !choiceIds || ids.some((id) => !choiceIds.has(id))) bad$1(`${what} names an unknown choice`);
4352
- result.correctChoiceIds = ids;
4353
- }
4354
- if (value.acceptedAnswers !== void 0) {
4355
- const answers = strings(value.acceptedAnswers, `${what} answers`, 50, 1e3);
4356
- if (!answers.length) bad$1(`Invalid ${what} answers`);
4357
- result.acceptedAnswers = answers;
4358
- }
4359
- if (value.numeric !== void 0) {
4360
- const n = value.numeric;
4361
- if (!isObject$1(n) || typeof n.value !== "number" || !Number.isFinite(n.value) || n.tolerance !== void 0 && (typeof n.tolerance !== "number" || !Number.isFinite(n.tolerance) || n.tolerance < 0)) return bad$1(`Invalid ${what} number`);
4362
- onlyKeys(n, ["value", "tolerance"], what);
4363
- result.numeric = {
4364
- value: n.value,
4365
- tolerance: n.tolerance ?? 0
4366
- };
4367
- }
4368
- if (!Object.keys(result).length) bad$1(`${what} cannot be empty`);
4369
- return result;
4370
- }
4371
- function normalizeQuestionContent(input, options = {}) {
4372
- if (!isObject$1(input)) return bad$1("Invalid question");
4373
- const prompt = input.prompt;
4374
- if (typeof prompt !== "string" || !prompt.trim() || prompt.length > 2e4) bad$1("Invalid prompt");
4375
- const responseType = input.responseType;
4376
- if (!QUESTION_RESPONSE_TYPES.includes(responseType)) bad$1("Invalid response type");
4377
- if (!QUESTION_PROVENANCES.includes(input.provenance)) bad$1("Invalid provenance");
4378
- const hasChoices = ["multiple-choice", "multi-select"].includes(responseType);
4379
- const choices = input.choices ?? [];
4380
- if (!Array.isArray(choices) || choices.length > 50) bad$1("Invalid choices");
4381
- if (hasChoices ? choices.length < 2 : choices.length) bad$1(hasChoices ? "Choice questions need at least two choices" : "Choices are only for choice questions");
4382
- const normalizedChoices = choices.map((c) => {
4383
- if (!isObject$1(c)) return bad$1("Invalid choice");
4384
- onlyKeys(c, ["id", "label"], "choice");
4385
- if (typeof c.id !== "string" || !ID.test(c.id) || typeof c.label !== "string" || !c.label.trim() || c.label.length > 2e3) bad$1("Invalid choice");
4386
- return {
4387
- id: c.id,
4388
- label: c.label
4389
- };
4390
- });
4391
- unique(normalizedChoices.map((c) => c.id), "choice id");
4392
- const parts = input.subparts ?? [];
4393
- if (!Array.isArray(parts) || parts.length > 50) bad$1("Invalid subparts");
4394
- if (responseType === "multi-part" ? !parts.length : parts.length) bad$1("Subparts are required for, and only for, multi-part questions");
4395
- const subparts = parts.map((p) => {
4396
- if (!isObject$1(p)) return bad$1("Invalid subpart");
4397
- onlyKeys(p, [
4398
- "partKey",
4399
- "prompt",
4400
- "responseType",
4401
- "pointsPossible"
4402
- ], "subpart");
4403
- if (typeof p.partKey !== "string" || !ID.test(p.partKey) || typeof p.prompt !== "string" || !p.prompt.trim() || p.prompt.length > 2e4 || p.responseType !== void 0 && (!QUESTION_RESPONSE_TYPES.includes(p.responseType) || [
4404
- "multi-part",
4405
- "multiple-choice",
4406
- "multi-select"
4407
- ].includes(p.responseType)) || p.pointsPossible !== void 0 && (typeof p.pointsPossible !== "number" || !Number.isFinite(p.pointsPossible) || p.pointsPossible <= 0)) bad$1("Invalid subpart");
4408
- return {
4409
- partKey: p.partKey,
4410
- prompt: p.prompt,
4411
- ...p.responseType ? { responseType: p.responseType } : {},
4412
- ...p.pointsPossible !== void 0 ? { pointsPossible: p.pointsPossible } : {}
4413
- };
4414
- });
4415
- unique(subparts.map((p) => p.partKey), "subpart key");
4416
- const mediaIn = input.media ?? [];
4417
- if (!Array.isArray(mediaIn) || mediaIn.length > 20) bad$1("Invalid media");
4418
- const media = mediaIn.map((m) => {
4419
- if (!isObject$1(m)) return bad$1("Invalid media");
4420
- onlyKeys(m, [
4421
- "assetId",
4422
- "role",
4423
- "alt"
4424
- ], "media");
4425
- if (typeof m.assetId !== "string" || !m.assetId || m.assetId.length > 200 || m.role !== void 0 && ![
4426
- "figure",
4427
- "passage",
4428
- "audio",
4429
- "other"
4430
- ].includes(m.role) || m.alt !== void 0 && (typeof m.alt !== "string" || m.alt.length > 500)) bad$1("Invalid media");
4431
- return {
4432
- assetId: m.assetId,
4433
- role: m.role ?? "other",
4434
- alt: m.alt ?? ""
4435
- };
4436
- });
4437
- const objectiveIds = unique(strings(input.objectiveIds ?? [], "objectives", 50, 200), "objective");
4438
- const conceptIds = unique(strings(input.conceptIds ?? [], "concepts", 50, 200), "concept");
4439
- for (const id of conceptIds) if (!CONCEPT.test(id) || options.conceptVocabulary && !options.conceptVocabulary.has(id)) bad$1("Unknown concept");
4440
- let answerKey = null;
4441
- if (input.answerKey !== void 0 && input.answerKey !== null) {
4442
- const key = input.answerKey;
4443
- if (!isObject$1(key)) return bad$1("Invalid answer key");
4444
- onlyKeys(key, [
4445
- "correctChoiceIds",
4446
- "acceptedAnswers",
4447
- "numeric",
4448
- "parts",
4449
- "explanation"
4450
- ], "answer key");
4451
- const choiceIds = hasChoices ? new Set(normalizedChoices.map((c) => c.id)) : null;
4452
- const { parts: keyParts, explanation, ...rest } = key;
4453
- const normalized = Object.keys(rest).length ? atom(rest, choiceIds, "answer key") : {};
4454
- if (keyParts !== void 0) {
4455
- if (!isObject$1(keyParts) || !Object.keys(keyParts).length) return bad$1("Invalid answer key parts");
4456
- const known = new Set(subparts.map((p) => p.partKey));
4457
- normalized.parts = Object.fromEntries(Object.entries(keyParts).map(([partKey, value]) => {
4458
- if (!known.has(partKey)) bad$1("Answer key names an unknown subpart");
4459
- return [partKey, atom(value, null, "answer key part")];
4460
- }));
4461
- }
4462
- if (explanation !== void 0) {
4463
- if (typeof explanation !== "string" || !explanation.trim() || explanation.length > 5e3) bad$1("Invalid answer explanation");
4464
- normalized.explanation = explanation;
4465
- }
4466
- if (!normalized.correctChoiceIds && !normalized.acceptedAnswers && !normalized.numeric && !normalized.parts) bad$1("An answer key cannot be empty; omit it when unknown");
4467
- if (responseType === "multiple-choice" && normalized.correctChoiceIds && normalized.correctChoiceIds.length !== 1) bad$1("A multiple-choice key names exactly one choice");
4468
- answerKey = normalized;
4469
- }
4470
- let rubric = null;
4471
- if (input.rubric !== void 0 && input.rubric !== null) try {
4472
- rubric = parseQuestionRubric(input.rubric);
4473
- } catch (error) {
4474
- bad$1(error.message);
4475
- }
4476
- let pointsPossible = null;
4477
- if (input.pointsPossible !== void 0 && input.pointsPossible !== null) {
4478
- if (typeof input.pointsPossible !== "number" || !Number.isFinite(input.pointsPossible) || input.pointsPossible <= 0) bad$1("Invalid points possible");
4479
- pointsPossible = input.pointsPossible;
4480
- }
4481
- if (rubric && pointsPossible !== null && rubric.criteria.reduce((s, c) => s + c.maxPoints, 0) !== pointsPossible) bad$1("Points possible must equal the rubric total");
4482
- const present = {
4483
- prompt: true,
4484
- responseType: true,
4485
- choices: normalizedChoices.length > 0,
4486
- subparts: subparts.length > 0,
4487
- media: media.length > 0,
4488
- objectives: objectiveIds.length > 0,
4489
- concepts: conceptIds.length > 0,
4490
- answerKey: answerKey !== null,
4491
- rubric: rubric !== null,
4492
- pointsPossible: pointsPossible !== null
4493
- };
4494
- const explicit = input.fieldProvenance ?? {};
4495
- if (!isObject$1(explicit)) return bad$1("Invalid field provenance");
4496
- const fieldProvenance = {};
4497
- for (const [field, value] of Object.entries(explicit)) if (!QUESTION_FIELDS.includes(field) || !QUESTION_PROVENANCES.includes(value) || !present[field]) bad$1("Provenance may only describe a present field");
4498
- for (const field of QUESTION_FIELDS) if (present[field]) fieldProvenance[field] = explicit[field] ?? input.provenance;
4499
- return {
4500
- prompt,
4501
- responseType,
4502
- choices: normalizedChoices,
4503
- subparts,
4504
- media,
4505
- objectiveIds,
4506
- conceptIds,
4507
- answerKey,
4508
- rubric,
4509
- pointsPossible,
4510
- provenance: input.provenance,
4511
- fieldProvenance
4512
- };
4513
- }
4514
- //#endregion
4515
5045
  //#region src/question-models.ts
4516
5046
  var __defProp$6 = Object.defineProperty;
4517
5047
  var __getOwnPropDesc$6 = Object.getOwnPropertyDescriptor;
@@ -27984,6 +28514,16 @@ var QuestionAuthoringService = class extends CourseAuthoringService {
27984
28514
  }
27985
28515
  /** Creates the stable question identity and its first immutable revision. */
27986
28516
  async createQuestion(request) {
28517
+ requireKey(request.requestKey);
28518
+ this.normalize(request.content);
28519
+ return this.run((tx) => this.createQuestionIn(tx, request));
28520
+ }
28521
+ /**
28522
+ * The create path on a caller-supplied transaction, so a composed write (a
28523
+ * generated lesson's worksheet) shares authorization, the request key and the
28524
+ * transaction with the rest of the unit.
28525
+ */
28526
+ async createQuestionIn(tx, request) {
27987
28527
  const requestKey = requireKey(request.requestKey);
27988
28528
  const content = this.normalize(request.content);
27989
28529
  const payloadDigest = digest$1({
@@ -27991,49 +28531,47 @@ var QuestionAuthoringService = class extends CourseAuthoringService {
27991
28531
  courseId: request.courseId ?? null,
27992
28532
  delegationId: request.delegationId ?? ""
27993
28533
  });
27994
- return this.run(async (tx) => {
27995
- await this.authorizeWrite(tx, request, usesGenerated(content), "create-question", request.courseId);
27996
- if (request.courseId) await this.identityRow(tx, request.tenantId, request.courseId);
27997
- const questionId = derived("question", request.tenantId, requestKey);
27998
- if (await tx.get("questions", {
27999
- id: questionId,
28000
- tenant_id: request.tenantId
28001
- })) {
28002
- const first = await tx.get("question_revisions", {
28003
- tenant_id: request.tenantId,
28004
- question_id: questionId,
28005
- request_key: requestKey
28006
- });
28007
- if (!first || first.payload_digest !== payloadDigest) throw new CourseRequestKeyConflictError();
28008
- return {
28009
- questionId,
28010
- headRevisionId: String(first.id),
28011
- sequence: 1
28012
- };
28013
- }
28014
- await this.requireReferences(tx, request.tenantId, content);
28015
- await (await QuestionCollection.create({ db: tx })).create({
28016
- _insertOnly: true,
28017
- id: questionId,
28018
- tenantId: request.tenantId,
28019
- courseId: request.courseId ?? null,
28020
- createdByUserId: request.actorUserId,
28021
- requestKey,
28022
- recordedAt: /* @__PURE__ */ new Date()
28534
+ await this.authorizeWrite(tx, request, usesGenerated(content), "create-question", request.courseId);
28535
+ if (request.courseId) await this.identityRow(tx, request.tenantId, request.courseId);
28536
+ const questionId = derived("question", request.tenantId, requestKey);
28537
+ if (await tx.get("questions", {
28538
+ id: questionId,
28539
+ tenant_id: request.tenantId
28540
+ })) {
28541
+ const first = await tx.get("question_revisions", {
28542
+ tenant_id: request.tenantId,
28543
+ question_id: questionId,
28544
+ request_key: requestKey
28023
28545
  });
28546
+ if (!first || first.payload_digest !== payloadDigest) throw new CourseRequestKeyConflictError();
28024
28547
  return {
28025
28548
  questionId,
28026
- headRevisionId: await this.appendQuestionRevision(tx, request, {
28027
- questionId,
28028
- parent: null,
28029
- sequence: 1,
28030
- requestKey,
28031
- payloadDigest,
28032
- content
28033
- }),
28549
+ headRevisionId: String(first.id),
28034
28550
  sequence: 1
28035
28551
  };
28552
+ }
28553
+ await this.requireReferences(tx, request.tenantId, content);
28554
+ await (await QuestionCollection.create({ db: tx })).create({
28555
+ _insertOnly: true,
28556
+ id: questionId,
28557
+ tenantId: request.tenantId,
28558
+ courseId: request.courseId ?? null,
28559
+ createdByUserId: request.actorUserId,
28560
+ requestKey,
28561
+ recordedAt: /* @__PURE__ */ new Date()
28036
28562
  });
28563
+ return {
28564
+ questionId,
28565
+ headRevisionId: await this.appendQuestionRevision(tx, request, {
28566
+ questionId,
28567
+ parent: null,
28568
+ sequence: 1,
28569
+ requestKey,
28570
+ payloadDigest,
28571
+ content
28572
+ }),
28573
+ sequence: 1
28574
+ };
28037
28575
  }
28038
28576
  async appendQuestionRevision(tx, actor, input) {
28039
28577
  const { content } = input;
@@ -29861,6 +30399,55 @@ EnrollmentPaceChange = __decorateClass$3([TenantScoped({ mode: "required" }), sm
29861
30399
  var EnrollmentPaceChangeCollection = class extends SmrtCollection {
29862
30400
  static _itemClass = EnrollmentPaceChange;
29863
30401
  };
30402
+ var LearnerInterestThemeChange = class extends SmrtObject {
30403
+ tenantId = "";
30404
+ learnerId = "";
30405
+ previousLabel = "";
30406
+ previousKey = "";
30407
+ newLabel = "";
30408
+ newKey = "";
30409
+ actorUserId = "";
30410
+ policyBasis = "";
30411
+ recordedAt = /* @__PURE__ */ new Date();
30412
+ constructor(options = {}) {
30413
+ super(options);
30414
+ adopt$1(this, options);
30415
+ }
30416
+ async delete() {
30417
+ noDelete$1();
30418
+ }
30419
+ async save(options) {
30420
+ await requireInsert$1(this);
30421
+ if (!await this.db.get("learners", {
30422
+ id: this.learnerId,
30423
+ tenant_id: this.tenantId
30424
+ })) throw new Error("Interest theme history requires its tenant learner");
30425
+ for (const [label, key] of [[this.previousLabel, this.previousKey], [this.newLabel, this.newKey]]) {
30426
+ if (!label && !key) continue;
30427
+ const theme = parseInterestTheme({
30428
+ label,
30429
+ key
30430
+ });
30431
+ if (!theme || theme.label !== label || theme.key !== key) throw new Error("Invalid interest theme change");
30432
+ }
30433
+ if (this.previousLabel === this.newLabel && this.previousKey === this.newKey) throw new Error("Invalid interest theme change");
30434
+ requireText$1(this.actorUserId, "Theme history requires an actor");
30435
+ requireText$1(this.policyBasis, "Theme history requires a policy basis");
30436
+ validDate$1(this.recordedAt);
30437
+ this.requireInsertOnSave();
30438
+ return super.save(options);
30439
+ }
30440
+ };
30441
+ __publicField$3(LearnerInterestThemeChange, "__smrtPackage__", "@happyvertical/smrt-education");
30442
+ __decorateClass$3([tenantId()], LearnerInterestThemeChange.prototype, "tenantId", 2);
30443
+ __decorateClass$3([foreignKey("Learner", { required: true })], LearnerInterestThemeChange.prototype, "learnerId", 2);
30444
+ LearnerInterestThemeChange = __decorateClass$3([TenantScoped({ mode: "required" }), smrt({
30445
+ tableName: "learner_interest_theme_changes",
30446
+ ...closed$2
30447
+ })], LearnerInterestThemeChange);
30448
+ var LearnerInterestThemeChangeCollection = class extends SmrtCollection {
30449
+ static _itemClass = LearnerInterestThemeChange;
30450
+ };
29864
30451
  //#endregion
29865
30452
  //#region src/workbook-completion.ts
29866
30453
  var __defProp$2 = Object.defineProperty;
@@ -30113,11 +30700,29 @@ var EnrollmentPaceError = class extends LearnerValidationError {
30113
30700
  this.name = "EnrollmentPaceError";
30114
30701
  }
30115
30702
  };
30703
+ var InterestThemeError = class extends LearnerValidationError {
30704
+ constructor() {
30705
+ super("Interest theme must be null or a short plain label (1-60 characters) with an optional lowercase key");
30706
+ this.name = "InterestThemeError";
30707
+ }
30708
+ };
30116
30709
  var POLICY_BASIS_CONTROL = /[\u0000-\u001f\u007f]/;
30117
30710
  function requirePolicyBasis(value) {
30118
30711
  if (typeof value !== "string" || !value.trim() || value.length > 200 || POLICY_BASIS_CONTROL.test(value)) throw new LearnerValidationError("A valid policy basis is required");
30119
30712
  return value;
30120
30713
  }
30714
+ function learnerView(row) {
30715
+ return {
30716
+ id: String(row.id),
30717
+ tenantId: row.tenantId,
30718
+ userId: row.userId,
30719
+ profileId: row.profileId,
30720
+ displayName: row.displayName,
30721
+ level: row.level,
30722
+ settings: row.getSettings(),
30723
+ interestTheme: interestThemeFromColumns(row.interestThemeLabel, row.interestThemeKey)
30724
+ };
30725
+ }
30121
30726
  function curriculumView(row) {
30122
30727
  return {
30123
30728
  id: String(row.id),
@@ -30374,16 +30979,7 @@ var LearnerService = class LearnerService {
30374
30979
  return learner;
30375
30980
  }
30376
30981
  async getLearner(request) {
30377
- const row = await this.requireActor(request, "education.learner.read");
30378
- return {
30379
- id: String(row.id),
30380
- tenantId: row.tenantId,
30381
- userId: row.userId,
30382
- profileId: row.profileId,
30383
- displayName: row.displayName,
30384
- level: row.level,
30385
- settings: row.getSettings()
30386
- };
30982
+ return learnerView(await this.requireActor(request, "education.learner.read"));
30387
30983
  }
30388
30984
  /**
30389
30985
  * Checks whether an active tenant member still needs a Learner row.
@@ -30488,15 +31084,7 @@ var LearnerService = class LearnerService {
30488
31084
  }, { cache: false });
30489
31085
  if (row) {
30490
31086
  if (row.profileId !== request.learnerProfileId) throw new LearnerAuthorizationError();
30491
- return {
30492
- id: String(row.id),
30493
- tenantId: row.tenantId,
30494
- userId: row.userId,
30495
- profileId: row.profileId,
30496
- displayName: row.displayName,
30497
- level: row.level,
30498
- settings: row.getSettings()
30499
- };
31087
+ return learnerView(row);
30500
31088
  }
30501
31089
  row = await learners.create({
30502
31090
  tenantId: request.tenantId,
@@ -30506,15 +31094,7 @@ var LearnerService = class LearnerService {
30506
31094
  level: request.level ?? 0,
30507
31095
  _insertOnly: true
30508
31096
  });
30509
- return {
30510
- id: String(row.id),
30511
- tenantId: row.tenantId,
30512
- userId: row.userId,
30513
- profileId: row.profileId,
30514
- displayName: row.displayName,
30515
- level: row.level,
30516
- settings: row.getSettings()
30517
- };
31097
+ return learnerView(row);
30518
31098
  });
30519
31099
  }
30520
31100
  /**
@@ -31099,6 +31679,60 @@ var LearnerService = class LearnerService {
31099
31679
  };
31100
31680
  });
31101
31681
  }
31682
+ /**
31683
+ * Sets or clears (`theme: null`) the learner's interest theme under
31684
+ * `education.learner.set-interest-theme`. The theme is a short plain label
31685
+ * with an optional stable key (the host owns its starter list) that colours
31686
+ * stories and examples only, never an objective. The host authorizer decides
31687
+ * who may set it: a guardian with an effective relationship, and the learner
31688
+ * themself only when the host allows. Another learner's theme needs the
31689
+ * actor's guardian relationship, so a sibling or unrelated actor is refused.
31690
+ * Appends an immutable `LearnerInterestThemeChange` (actor, `policyBasis`,
31691
+ * previous and new theme). An unchanged theme is an idempotent
31692
+ * `{ changed: false }`; an invalid theme throws `InterestThemeError`.
31693
+ */
31694
+ async setInterestTheme(request) {
31695
+ const policyBasis = requirePolicyBasis(request.policyBasis);
31696
+ let next = null;
31697
+ if (request.theme !== null) {
31698
+ next = parseInterestTheme(request.theme);
31699
+ if (!next) throw new InterestThemeError();
31700
+ }
31701
+ return this.mutate(async (service) => {
31702
+ await service.requireActor(request, "education.learner.set-interest-theme");
31703
+ await service.lock("learners", request.learnerId, request.tenantId);
31704
+ const learner = await (await LearnerCollection.create({ db: service.db })).get({
31705
+ id: request.learnerId,
31706
+ tenantId: request.tenantId
31707
+ }, { cache: false });
31708
+ if (!learner) throw new LearnerAuthorizationError();
31709
+ const previous = interestThemeFromColumns(learner.interestThemeLabel, learner.interestThemeKey);
31710
+ if ((previous?.label ?? "") === (next?.label ?? "") && (previous?.key ?? "") === (next?.key ?? "")) return {
31711
+ changed: false,
31712
+ interestTheme: previous
31713
+ };
31714
+ if (!await service.db.tableExists("learner_interest_theme_changes")) throw new LearnerHistoryUnavailableError("learner_interest_theme_changes");
31715
+ learner.interestThemeLabel = next?.label ?? "";
31716
+ learner.interestThemeKey = next?.key ?? "";
31717
+ await learner.save();
31718
+ await (await LearnerInterestThemeChangeCollection.create({ db: service.db })).create({
31719
+ _insertOnly: true,
31720
+ tenantId: request.tenantId,
31721
+ learnerId: String(learner.id),
31722
+ previousLabel: previous?.label ?? "",
31723
+ previousKey: previous?.key ?? "",
31724
+ newLabel: next?.label ?? "",
31725
+ newKey: next?.key ?? "",
31726
+ actorUserId: request.actorUserId,
31727
+ policyBasis,
31728
+ recordedAt: /* @__PURE__ */ new Date()
31729
+ });
31730
+ return {
31731
+ changed: true,
31732
+ interestTheme: next
31733
+ };
31734
+ });
31735
+ }
31102
31736
  };
31103
31737
  //#endregion
31104
31738
  //#region src/question-extraction-contracts.ts
@@ -35187,6 +35821,6 @@ var WorkbookPhotoService = class WorkbookPhotoService extends WorkbookService {
35187
35821
  }
35188
35822
  };
35189
35823
  //#endregion
35190
- export { EnrollmentPaceError as $, CourseArchiveDecisionCollection as $n, CatalogService as $r, FamilyIntakeAssetCollection as $t, normalizeQuestionExtraction as A, parseQuestionRubric as Ai, MAX_PRINTABLE_PAGES as An, mostRestrictiveLicense as Ar, WorkbookAttemptCollection as At, MAX_EXTRACTED_OMISSIONS as B, CourseSection as Bi, QUESTION_PROVENANCES as Bn, CourseMaterialCopyError as Br, WorkbookCompletionConflictError as Bt, ExtractedQuestionLink as C, ExternalCompletion as Ci, normalizeQuestionContent as Cn, LessonRevisionCollection as Cr, LessonProgressTransitionCollection as Ct, QUESTION_EXTRACTION_ARGS_SCHEMA as D, Submission as Di, LearningObjectiveKeyConflictError as Dn, AUTHORED_RELEASE_SOURCE_KEY as Dr, WorkbookAssessmentRevision as Dt, QuestionExtractionRecordCollection as E, ScannedSubmission as Ei, GENERATED_PRINTABLE_RELATIONSHIPS as En, AUTHORED_IDENTITY_SOURCE_KEY as Er, WorkbookActivityCollection as Et, EXTRACTED_ORIGINS as F, Learner as Fi, PrintableStorageCleanupError as Fn, CourseAuthoringError as Fr, WorkbookFeedbackReleaseCollection as Ft, MAX_EXTRACTED_TEXT as G, Video as Gi, groupSharingDigest as Gn, CourseStaleHeadError as Gr, summarizeWorkbookEvidence as Gt, MAX_EXTRACTED_QUESTIONS as H, ExternalCourse as Hi, QuestionNotFoundError as Hn, CoursePolicyBasisMismatchError as Hr, validateWorkbookAssessment as Ht, EXTRACTED_PAGE_ROLES as I, LessonProgress as Ii, PrintableStorageNotConfiguredError as In, CourseAuthoringValidationError as Ir, WorkbookWorkFiling as It, QuestionExtractionNotFoundError as J, resolveEnrollmentLessons as Jn, EnrollmentPinConflictError as Jr, GroupSharingPolicyValidationError as Jt, QUESTION_EXTRACTION_SOURCE_KINDS as K, Worksheet as Ki, authoredVisibilityPermitsGroups as Kn, CourseVisibilityRefusedError as Kr, GroupSharingAuthorizationError as Kt, EXTRACTED_QUESTION_STATUSES as L, AuthoredCourse as Li, QUESTION_COMPLETION_MODES as Ln, CourseCoverInvalidError as Lr, WorkbookWorkFilingCollection as Lt, EXTRACTED_LEGIBILITY as M, LessonAsset as Mi, MAX_PRINTABLE_TOTAL_TEXT as Mn, COURSE_PROVENANCES as Mr, WorkbookFeedbackDelivery as Mt, EXTRACTED_MEDIA_KINDS as N, Curriculum as Ni, MAX_RESOLVE_QUESTION_REVISIONS as Nn, CourseArchivedError as Nr, WorkbookFeedbackDeliveryCollection as Nt, extractedQuestionContent as O, SubmissionAsset as Oi, LearningObjectiveNotFoundError as On, isAuthoredReleaseGroupShareable as Or, WorkbookAssessmentRevisionCollection as Ot, EXTRACTED_OMISSION_REASONS as P, Enrollment as Pi, PrintableOwnerError as Pn, CourseAuthoringAuthorizationError as Pr, WorkbookFeedbackRelease as Pt, EnrollmentNotActiveError as Q, CourseArchiveDecision as Qn, CatalogAuthorizationError as Qr, FamilyIntakeAsset as Qt, EXTRACTED_READABILITY as R, COURSE_VISIBILITIES as Ri, QUESTION_FIELDS as Rn, CourseDiscardRefusedError as Rr, WORKBOOK_MATH_VOCABULARY as Rt, questionExtractionProposalDigest as S, Attestation as Si, QuestionRevisionCollection as Sn, LessonRevision as Sr, LessonProgressTransition as St, QuestionExtractionRecord as T, LessonSelfAttestation as Ti, GENERATED_PRINTABLE_AUDIENCES as Tn, ReleasedLessonCollection as Tr, WorkbookActivity as Tt, MAX_EXTRACTED_SECTIONS as U, Lesson as Ui, QuestionReferenceError as Un, CourseRequestKeyConflictError as Ur, workbookAggregateFeedback as Ut, MAX_EXTRACTED_PAGES as V, Exercise as Vi, QUESTION_RESPONSE_TYPES as Vn, CourseNotFoundError as Vr, aggregateWorkbookQuestions as Vt, MAX_EXTRACTED_STUDENT_WORK as W, Reading as Wi, WorksheetVersionMismatchError as Wn, CourseRevisionAlreadyReleasedError as Wr, evaluateGroupSharingPolicy as Wt, QuestionExtractionValidationError as X, delegationRecordFields as Xn, ReviewGateRequiredError as Xr, validateGroupSharingPolicy as Xt, QuestionExtractionPendingError as Y, delegationActorFields as Yn, MAX_PAGE_SIZE as Yr, canonicalGroupSharingPolicy as Yt, ENROLLMENT_STATUS_TRANSITIONS as Z, readDelegation as Zn, ReviewNotCurrentError as Zr, FamilyIntake as Zt, SELF_ATTESTABLE_LESSON_KINDS as _, SubmissionAssetCollection as _i, Question as _n, EnrollmentAssignmentRecordCollection as _r, EnrollmentPaceChangeCollection as _t, WorkbookContextRevisionCollection as a, EnrollmentCollection as ai, buildGuardianWorksheet as an, CourseDraftHeadCollection as ar, LessonAlreadyCompleteError as at, WorkbookWorkflowRequiredError as b, VideoCollection as bi, QuestionResponseCollection as bn, EnrollmentReleasePin as br, LessonCompletionRecord as bt, WorkbookPhotoDecision as c, ExternalCourseCollection as ci, worksheetVersionId as cn, CourseRevision as cr, MAX_LESSONS_PER_WEEK as ct, WorkbookPhotoIntakeCollection as d, LessonAssetCollection as di, GeneratedPrintable as dn, CourseUpgradeChoiceCollection as dr, WorkbookOwnedLessonError as dt, AttestationCollection as ei, FamilyIntakeCollection as en, CourseCoverChange as er, EnrollmentStatusTransitionError as et, validateWorkbookPhotoIdentification as f, LessonCollection as fi, GeneratedPrintableCollection as fn, CourseVisibilityChange as fr, enrollmentView as ft, OwnAttestationLessonKindError as g, ScannedSubmissionCollection as gi, LearningObjectiveRevisionCollection as gn, EnrollmentAssignmentRecord as gr, EnrollmentPaceChange as gt, OwnAttestationConflictError as h, ReadingCollection as hi, LearningObjectiveRevision as hn, CurriculumCreationRecordCollection as hr, WorkbookCompletionDecisionCollection as ht, WorkbookContextRevision as i, CurriculumCollection as ii, QuestionAuthoringService as in, CourseDraftHead as ir, LearnerValidationError as it, questionExtractionSourceIdentity as j, validateRubricResults as ji, MAX_PRINTABLE_TEXT_LENGTH as jn, COURSE_AUTHORING_ACTIONS as jr, WorkbookCollection as jt, matchExtractedQuestions as k, SubmissionIntakeBinding as ki, MAX_PRINTABLE_ITEMS as kn, isGroupShareableLicense as kr, WorkbookAttempt as kt, WorkbookPhotoDecisionCollection as l, GradeCollection as li, renderGuardianWorksheetPdf as ln, CourseRevisionCollection as lr, MAX_NEXT_LESSONS_COUNT as lt, normalizeWorkbookAnswerRegions as m, LessonSelfAttestationCollection as mi, LearningObjectiveCollection as mn, CurriculumCreationRecord as mr, WorkbookCompletionDecision as mt, preservePhotoProposal as n, CourseCollection as ni, FamilyIntakeDecisionCollection as nn, CourseDiscardTombstone as nr, LearnerHistoryUnavailableError as nt, WorkbookPhotoAsset as o, ExerciseCollection as oi, buildLearnerWorksheet as on, CourseRelease as or, LessonPutBackError as ot, normalizeWorkbookObservedLayout as p, LessonProgressCollection as pi, LearningObjective as pn, CourseVisibilityChangeCollection as pr, requirePolicyBasis as pt, QUESTION_EXTRACTION_VERSION as q, effectiveCourseVisibility as qn, DEFAULT_PAGE_SIZE as qr, GroupSharingConflictError as qt, WorkbookService as r, CourseSectionCollection as ri, familyIntakeScopeId as rn, CourseDiscardTombstoneCollection as rr, LearnerService as rt, WorkbookPhotoAssetCollection as s, ExternalCompletionCollection as si, canonical$2 as sn, CourseReleaseCollection as sr, LessonSkippedError as st, WorkbookPhotoService as t, AuthoredCourseCollection as ti, FamilyIntakeDecision as tn, CourseCoverChangeCollection as tr, LearnerAuthorizationError as tt, WorkbookPhotoIntake as u, LearnerCollection as ui, renderLearnerWorksheetPdf as un, CourseUpgradeChoice as ur, MIN_LESSONS_PER_WEEK as ut, SubmissionService as v, SubmissionCollection as vi, QuestionCollection as vn, EnrollmentAutoUpgradeSkip as vr, EnrollmentStatusChange as vt, ExtractedQuestionLinkCollection as w, Grade as wi, CourseAuthoringService as wn, ReleasedLesson as wr, Workbook as wt, QuestionExtractionService as x, WorksheetCollection as xi, QuestionRevision as xn, EnrollmentReleasePinCollection as xr, LessonCompletionRecordCollection as xt, SubmissionStorageCleanupError as y, SubmissionIntakeBindingCollection as yi, QuestionResponse as yn, EnrollmentAutoUpgradeSkipCollection as yr, EnrollmentStatusChangeCollection as yt, MAX_EXTRACTED_ANCHORS as z, Course as zi, QUESTION_PROMPT_SUMMARY_LENGTH as zn, CourseDiscardedError as zr, WorkbookAssessmentValidationError as zt };
35824
+ export { EnrollmentPaceError as $, SubmissionIntakeBinding as $i, QuestionReferenceError as $n, mostRestrictiveLicense as $r, validateGroupSharingPolicy as $t, normalizeQuestionExtraction as A, ExerciseCollection as Ai, applyProposalFields as An, CourseReleaseCollection as Ar, WorkbookAssessmentRevision as At, MAX_EXTRACTED_OMISSIONS as B, ScannedSubmissionCollection as Bi, MAX_PRINTABLE_PAGES as Bn, EnrollmentAssignmentRecordCollection as Br, WorkbookWorkFilingCollection as Bt, ExtractedQuestionLink as C, CatalogService as Ci, QuestionResponseCollection as Cn, CourseCoverChange as Cr, LessonCompletionRecord as Ct, QUESTION_EXTRACTION_ARGS_SCHEMA as D, CourseSectionCollection as Di, authoredVisibilityPermitsGroups as Dn, CourseDraftHead as Dr, Workbook as Dt, QuestionExtractionRecordCollection as E, CourseCollection as Ei, CourseAuthoringService as En, CourseDiscardTombstoneCollection as Er, LessonProgressTransitionCollection as Et, EXTRACTED_ORIGINS as F, LessonAssetCollection as Fi, GENERATED_PRINTABLE_AUDIENCES as Fn, CourseVisibilityChange as Fr, WorkbookFeedbackDelivery as Ft, MAX_EXTRACTED_TEXT as G, WorksheetCollection as Gi, PrintableStorageCleanupError as Gn, LessonRevision as Gr, validateWorkbookAssessment as Gt, MAX_EXTRACTED_QUESTIONS as H, SubmissionCollection as Hi, MAX_PRINTABLE_TOTAL_TEXT as Hn, EnrollmentAutoUpgradeSkipCollection as Hr, WorkbookAssessmentValidationError as Ht, EXTRACTED_PAGE_ROLES as I, LessonCollection as Ii, GENERATED_PRINTABLE_RELATIONSHIPS as In, CourseVisibilityChangeCollection as Ir, WorkbookFeedbackDeliveryCollection as It, QuestionExtractionNotFoundError as J, Grade as Ji, QUESTION_FIELDS as Jn, ReleasedLessonCollection as Jr, summarizeWorkbookEvidence as Jt, QUESTION_EXTRACTION_SOURCE_KINDS as K, Attestation as Ki, PrintableStorageNotConfiguredError as Kn, LessonRevisionCollection as Kr, workbookAggregateFeedback as Kt, EXTRACTED_QUESTION_STATUSES as L, LessonProgressCollection as Li, LearningObjectiveKeyConflictError as Ln, CurriculumCreationRecord as Lr, WorkbookFeedbackRelease as Lt, EXTRACTED_LEGIBILITY as M, ExternalCourseCollection as Mi, planProposal as Mn, CourseRevisionCollection as Mr, WorkbookAttempt as Mt, EXTRACTED_MEDIA_KINDS as N, GradeCollection as Ni, validateCourseGenerationOutput as Nn, CourseUpgradeChoice as Nr, WorkbookAttemptCollection as Nt, extractedQuestionContent as O, CurriculumCollection as Oi, effectiveCourseVisibility as On, CourseDraftHeadCollection as Or, WorkbookActivity as Ot, EXTRACTED_OMISSION_REASONS as P, LearnerCollection as Pi, normalizeQuestionContent as Pn, CourseUpgradeChoiceCollection as Pr, WorkbookCollection as Pt, EnrollmentNotActiveError as Q, SubmissionAsset as Qi, QuestionNotFoundError as Qn, isGroupShareableLicense as Qr, canonicalGroupSharingPolicy as Qt, EXTRACTED_READABILITY as R, LessonSelfAttestationCollection as Ri, LearningObjectiveNotFoundError as Rn, CurriculumCreationRecordCollection as Rr, WorkbookFeedbackReleaseCollection as Rt, questionExtractionProposalDigest as S, CatalogAuthorizationError as Si, QuestionResponse as Sn, CourseArchiveDecisionCollection as Sr, LearnerInterestThemeChangeCollection as St, QuestionExtractionRecord as T, AuthoredCourseCollection as Ti, QuestionRevisionCollection as Tn, CourseDiscardTombstone as Tr, LessonProgressTransition as Tt, MAX_EXTRACTED_SECTIONS as U, SubmissionIntakeBindingCollection as Ui, MAX_RESOLVE_QUESTION_REVISIONS as Un, EnrollmentReleasePin as Ur, WorkbookCompletionConflictError as Ut, MAX_EXTRACTED_PAGES as V, SubmissionAssetCollection as Vi, MAX_PRINTABLE_TEXT_LENGTH as Vn, EnrollmentAutoUpgradeSkip as Vr, WORKBOOK_MATH_VOCABULARY as Vt, MAX_EXTRACTED_STUDENT_WORK as W, VideoCollection as Wi, PrintableOwnerError as Wn, EnrollmentReleasePinCollection as Wr, aggregateWorkbookQuestions as Wt, QuestionExtractionValidationError as X, ScannedSubmission as Xi, QUESTION_PROVENANCES as Xn, AUTHORED_RELEASE_SOURCE_KEY as Xr, GroupSharingConflictError as Xt, QuestionExtractionPendingError as Y, LessonSelfAttestation as Yi, QUESTION_PROMPT_SUMMARY_LENGTH as Yn, AUTHORED_IDENTITY_SOURCE_KEY as Yr, GroupSharingAuthorizationError as Yt, ENROLLMENT_STATUS_TRANSITIONS as Z, Submission as Zi, QUESTION_RESPONSE_TYPES as Zn, isAuthoredReleaseGroupShareable as Zr, GroupSharingPolicyValidationError as Zt, SELF_ATTESTABLE_LESSON_KINDS as _, Lesson as _a, DEFAULT_PAGE_SIZE as _i, LearningObjectiveCollection as _n, MAX_REFUSAL_EXPLANATION_LENGTH as _r, EnrollmentPaceChange as _t, WorkbookContextRevisionCollection as a, Learner as aa, CourseAuthoringError as ai, FamilyIntakeDecisionCollection as an, CourseGenerationBriefError as ar, LearnerValidationError as at, WorkbookWorkflowRequiredError as b, Worksheet as ba, ReviewGateRequiredError as bi, Question as bn, readDelegation as br, EnrollmentStatusChangeCollection as bt, WorkbookPhotoDecision as c, MAX_INTEREST_THEME_LABEL_CHARS as ca, CourseDiscardRefusedError as ci, buildGuardianWorksheet as cn, CourseGenerationOutputError as cr, LessonSkippedError as ct, WorkbookPhotoIntakeCollection as d, AuthoredCourse as da, CourseNotFoundError as di, worksheetVersionId as dn, CourseProposalFieldError as dr, MIN_LESSONS_PER_WEEK as dt, parseQuestionRubric as ea, COURSE_AUTHORING_ACTIONS as ei, FamilyIntake as en, WorksheetVersionMismatchError as er, EnrollmentStatusTransitionError as et, validateWorkbookPhotoIdentification as f, COURSE_VISIBILITIES as fa, CoursePolicyBasisMismatchError as fi, renderGuardianWorksheetPdf as fn, CourseProposalNotFoundError as fr, WorkbookOwnedLessonError as ft, OwnAttestationLessonKindError as g, ExternalCourse as ga, CourseVisibilityRefusedError as gi, LearningObjective as gn, MAX_REFUSAL_CATEGORY_LENGTH as gr, WorkbookCompletionDecisionCollection as gt, OwnAttestationConflictError as h, Exercise as ha, CourseStaleHeadError as hi, GeneratedPrintableCollection as hn, MAX_GENERATION_OBJECTIVE_CHARS as hr, WorkbookCompletionDecision as ht, WorkbookContextRevision as i, Enrollment as ia, CourseAuthoringAuthorizationError as ii, FamilyIntakeDecision as in, COURSE_GENERATION_TERMINAL as ir, LearnerService as it, questionExtractionSourceIdentity as j, ExternalCompletionCollection as ji, draftFromRevision as jn, CourseRevision as jr, WorkbookAssessmentRevisionCollection as jt, matchExtractedQuestions as k, EnrollmentCollection as ki, resolveEnrollmentLessons as kn, CourseRelease as kr, WorkbookActivityCollection as kt, WorkbookPhotoDecisionCollection as l, interestThemeFromColumns as la, CourseDiscardedError as li, buildLearnerWorksheet as ln, CourseGenerationRefusal as lr, MAX_LESSONS_PER_WEEK as lt, normalizeWorkbookAnswerRegions as m, CourseSection as ma, CourseRevisionAlreadyReleasedError as mi, GeneratedPrintable as mn, MAX_GENERATION_GRADE as mr, requirePolicyBasis as mt, preservePhotoProposal as n, LessonAsset as na, CourseAppendRefusedError as ni, FamilyIntakeAssetCollection as nn, COURSE_GENERATION_STATUSES as nr, LearnerAuthorizationError as nt, WorkbookPhotoAsset as o, LessonProgress as oa, CourseAuthoringValidationError as oi, familyIntakeScopeId as on, CourseGenerationInProgressError as or, LessonAlreadyCompleteError as ot, normalizeWorkbookObservedLayout as p, Course as pa, CourseRequestKeyConflictError as pi, renderLearnerWorksheetPdf as pn, MAX_GENERATION_BRIEF_FIELD_CHARS as pr, enrollmentView as pt, QUESTION_EXTRACTION_VERSION as q, ExternalCompletion as qi, QUESTION_COMPLETION_MODES as qn, ReleasedLesson as qr, evaluateGroupSharingPolicy as qt, WorkbookService as r, Curriculum as ra, CourseArchivedError as ri, FamilyIntakeCollection as rn, COURSE_GENERATION_TARGETS as rr, LearnerHistoryUnavailableError as rt, WorkbookPhotoAssetCollection as s, MAX_INTEREST_THEME_KEY_CHARS as sa, CourseCoverInvalidError as si, QuestionAuthoringService as sn, CourseGenerationNotFoundError as sr, LessonPutBackError as st, WorkbookPhotoService as t, validateRubricResults as ta, COURSE_PROVENANCES as ti, FamilyIntakeAsset as tn, groupSharingDigest as tr, InterestThemeError as tt, WorkbookPhotoIntake as u, parseInterestTheme as ua, CourseMaterialCopyError as ui, canonical$2 as un, CourseGeneratorNotConfiguredError as ur, MAX_NEXT_LESSONS_COUNT as ut, SubmissionService as v, Reading as va, EnrollmentPinConflictError as vi, LearningObjectiveRevision as vn, delegationActorFields as vr, EnrollmentPaceChangeCollection as vt, ExtractedQuestionLinkCollection as w, AttestationCollection as wi, QuestionRevision as wn, CourseCoverChangeCollection as wr, LessonCompletionRecordCollection as wt, QuestionExtractionService as x, ReviewNotCurrentError as xi, QuestionCollection as xn, CourseArchiveDecision as xr, LearnerInterestThemeChange as xt, SubmissionStorageCleanupError as y, Video as ya, MAX_PAGE_SIZE as yi, LearningObjectiveRevisionCollection as yn, delegationRecordFields as yr, EnrollmentStatusChange as yt, MAX_EXTRACTED_ANCHORS as z, ReadingCollection as zi, MAX_PRINTABLE_ITEMS as zn, EnrollmentAssignmentRecord as zr, WorkbookWorkFiling as zt };
35191
35825
 
35192
- //# sourceMappingURL=workbook-photo-service-WwDDC5GF.js.map
35826
+ //# sourceMappingURL=workbook-photo-service-BtMQsbhq.js.map