@happyvertical/smrt-education 0.57.50 → 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.
- package/README.md +18 -1
- package/agents/course-authoring-service.md +39 -0
- package/agents/course-generation-review.md +53 -0
- package/agents/learner-service.md +20 -0
- package/agents/tutor-context.md +2 -1
- package/dist/chunks/{workbook-photo-service-WwDDC5GF.js → workbook-photo-service-BtMQsbhq.js} +1036 -402
- package/dist/chunks/workbook-photo-service-BtMQsbhq.js.map +1 -0
- package/dist/course-authoring-contracts.d.ts +23 -1
- package/dist/course-authoring-contracts.d.ts.map +1 -1
- package/dist/course-authoring-models.d.ts +2 -0
- package/dist/course-authoring-models.d.ts.map +1 -1
- package/dist/course-authoring-service.d.ts +56 -2
- package/dist/course-authoring-service.d.ts.map +1 -1
- package/dist/course-generation-contracts.d.ts +91 -0
- package/dist/course-generation-contracts.d.ts.map +1 -1
- package/dist/course-generation-models.d.ts +29 -0
- package/dist/course-generation-models.d.ts.map +1 -1
- package/dist/course-generation-output.d.ts +9 -3
- package/dist/course-generation-output.d.ts.map +1 -1
- package/dist/course-generation-proposal-generator.d.ts +171 -1
- package/dist/course-generation-proposal-generator.d.ts.map +1 -1
- package/dist/course-generation-review-service.d.ts +15 -1
- package/dist/course-generation-review-service.d.ts.map +1 -1
- package/dist/index.d.ts +6 -4
- package/dist/index.d.ts.map +1 -1
- package/dist/index.js +364 -327
- package/dist/index.js.map +1 -1
- package/dist/ingestion.js +1 -1
- package/dist/learner-history.d.ts +23 -0
- package/dist/learner-history.d.ts.map +1 -1
- package/dist/learner-interest-theme.d.ts +25 -0
- package/dist/learner-interest-theme.d.ts.map +1 -0
- package/dist/learner-service.d.ts +31 -2
- package/dist/learner-service.d.ts.map +1 -1
- package/dist/learners.d.ts +4 -0
- package/dist/learners.d.ts.map +1 -1
- package/dist/manifest.json +3699 -2802
- package/dist/question-authoring-service.d.ts +12 -0
- package/dist/question-authoring-service.d.ts.map +1 -1
- package/dist/smrt-knowledge.json +363 -13
- package/dist/tutor-context-service.d.ts.map +1 -1
- package/dist/tutor-contracts.d.ts +8 -0
- package/dist/tutor-contracts.d.ts.map +1 -1
- package/package.json +8 -8
- package/dist/chunks/workbook-photo-service-WwDDC5GF.js.map +0 -1
package/dist/chunks/{workbook-photo-service-WwDDC5GF.js → workbook-photo-service-BtMQsbhq.js}
RENAMED
|
@@ -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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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$
|
|
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
|
-
|
|
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
|
-
|
|
27995
|
-
|
|
27996
|
-
|
|
27997
|
-
|
|
27998
|
-
|
|
27999
|
-
|
|
28000
|
-
|
|
28001
|
-
|
|
28002
|
-
|
|
28003
|
-
|
|
28004
|
-
|
|
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:
|
|
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
|
-
|
|
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 $,
|
|
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-
|
|
35826
|
+
//# sourceMappingURL=workbook-photo-service-BtMQsbhq.js.map
|