@playcademy/sandbox 0.10.1-beta.4 → 0.10.1-beta.6

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cli.js +357 -110
  2. package/dist/server.js +357 -110
  3. package/package.json +1 -1
package/dist/cli.js CHANGED
@@ -1128,7 +1128,7 @@ var package_default;
1128
1128
  var init_package = __esm(() => {
1129
1129
  package_default = {
1130
1130
  name: "@playcademy/sandbox",
1131
- version: "0.10.1-beta.4",
1131
+ version: "0.10.1-beta.6",
1132
1132
  description: "Local development server for Playcademy game development",
1133
1133
  type: "module",
1134
1134
  exports: {
@@ -47989,7 +47989,7 @@ function rejectSystemManagedReview(input, context2) {
47989
47989
  });
47990
47990
  }
47991
47991
  }
47992
- var TimebackGradeSchema, TimebackSubjectSchema, CourseGoalsSchema, UpdateGameTimebackIntegrationRequestSchema, CreateGameTimebackIntegrationRequestSchema, TimebackActivityDataSchema, EndActivityRequestSchema, GameRunMetricsSchema, GameCourseMetricsSchema, GameMetricsResponseSchema, AdvanceCourseRequestSchema, UnenrollCourseRequestSchema, HeartbeatRequestSchema, PopulateStudentRequestSchema, DerivedPlatformCourseConfigSchema, TimebackBaseConfigSchema, PlatformTimebackSetupRequestSchema, AdminTimebackMutationBaseSchema, AdminAttributionDateSchema, ADMIN_GRANT_XP_MIN = -1e5, ADMIN_GRANT_XP_MAX = 1e5, ADMIN_GRANT_XP_AMOUNT_RANGE_MESSAGE, GrantTimebackXpRequestSchema, AdjustTimebackTimeRequestSchema, AdjustTimebackMasteryRequestSchema, ReconcileMasteryForConfigChangeSchema, EnrollStudentRequestSchema, UnenrollStudentRequestSchema, ReactivateEnrollmentRequestSchema, VerifyTimebackMetricDiscrepancyRequestSchema, AssessmentPurposeSchema, AssessmentStatusSchema, AssessmentKeySchema, DiagnosticKeySchema, DiagnosticRoutingManifestSchema, DiagnosticAssessmentDefinitionInputSchema, AssessmentStandardRefSchema3, CreateAssessmentRequestSchema, UpdateAssessmentRequestSchema, CopyAssessmentRequestSchema, AssessmentAssociationImportEntrySchema, AttachExistingAssessmentsRequestSchema, ReorderQuestionsRequestSchema;
47992
+ var TimebackGradeSchema, TimebackSubjectSchema, CourseGoalsSchema, UpdateGameTimebackIntegrationRequestSchema, CreateGameTimebackIntegrationRequestSchema, TimebackActivityDataSchema, EndActivityRequestSchema, GameRunMetricsSchema, GameCourseMetricsSchema, GameMetricsResponseSchema, AdvanceCourseRequestSchema, UnenrollCourseRequestSchema, HeartbeatRequestSchema, PopulateStudentRequestSchema, DerivedPlatformCourseConfigSchema, TimebackBaseConfigSchema, PlatformTimebackSetupRequestSchema, AdminTimebackMutationBaseSchema, AdminAttributionDateSchema, ADMIN_GRANT_XP_MIN = -1e5, ADMIN_GRANT_XP_MAX = 1e5, ADMIN_GRANT_XP_AMOUNT_RANGE_MESSAGE, GrantTimebackXpRequestSchema, AdjustTimebackTimeRequestSchema, AdjustTimebackMasteryRequestSchema, ReconcileMasteryForConfigChangeSchema, EnrollStudentRequestSchema, UnenrollStudentRequestSchema, ReactivateEnrollmentRequestSchema, VerifyTimebackMetricDiscrepancyRequestSchema, AssessmentPurposeSchema, AssessmentStatusSchema, AssessmentKeySchema, DiagnosticKeySchema, DiagnosticRoutingManifestSchema, DiagnosticAssessmentDefinitionInputSchema, AssessmentStandardRefSchema3, CreateAssessmentRequestSchema, UpdateAssessmentRequestSchema, CopyAssessmentRequestSchema, ReviseAssessmentRequestSchema, AssessmentAssociationImportEntrySchema, AttachExistingAssessmentsRequestSchema, ReorderQuestionsRequestSchema;
47993
47993
  var init_schemas4 = __esm(() => {
47994
47994
  init_esm();
47995
47995
  init_src();
@@ -48272,6 +48272,11 @@ var init_schemas4 = __esm(() => {
48272
48272
  requireMasteryStandard(input, context2);
48273
48273
  rejectSystemManagedReview(input, context2);
48274
48274
  });
48275
+ ReviseAssessmentRequestSchema = exports_external.object({
48276
+ currentQtiTestIdentifier: exports_external.string().trim().min(1, "The current assessment identifier is required"),
48277
+ title: exports_external.string().trim().min(1, "Assessment title is required"),
48278
+ questions: exports_external.array(exports_external.record(exports_external.string(), exports_external.unknown())).min(1, "A revision must contain at least one question").max(500, "A revision cannot contain more than 500 questions")
48279
+ });
48275
48280
  AssessmentAssociationImportEntrySchema = exports_external.object({
48276
48281
  assessmentKey: AssessmentKeySchema,
48277
48282
  qtiTestIdentifier: exports_external.string().trim().min(1, "Assessment identifier is required"),
@@ -94023,8 +94028,20 @@ function compareEnrollmentsByRecency(a, b) {
94023
94028
  }
94024
94029
  return (b.dateLastModified ?? "").localeCompare(a.dateLastModified ?? "");
94025
94030
  }
94031
+ function resolvePrimaryAccountRole(roles2) {
94032
+ if (!roles2?.length) {
94033
+ return null;
94034
+ }
94035
+ return ACCOUNT_ROLE_PRECEDENCE.find((role) => roles2.some((entry) => entry.role === role)) ?? null;
94036
+ }
94037
+ var ACCOUNT_ROLE_PRECEDENCE;
94026
94038
  var init_timeback_admin_util = __esm(() => {
94027
94039
  init_errors2();
94040
+ ACCOUNT_ROLE_PRECEDENCE = [
94041
+ "administrator",
94042
+ "teacher",
94043
+ "student"
94044
+ ];
94028
94045
  });
94029
94046
 
94030
94047
  // ../api-core/src/utils/timeback-discrepancy-queue.util.ts
@@ -98734,6 +98751,7 @@ class TimebackAdminService {
98734
98751
  className: rosterEntry.class?.title || null,
98735
98752
  name: name3,
98736
98753
  email: rosterEntry.user?.email || null,
98754
+ primaryRole: resolvePrimaryAccountRole(rosterEntry.user?.roles),
98737
98755
  analyticsUnavailable,
98738
98756
  totalXp: summary?.totalXp ?? 0,
98739
98757
  todayXp: summary?.todayXp ?? 0,
@@ -99697,15 +99715,22 @@ class TimebackAdminService {
99697
99715
  });
99698
99716
  pastEnrollmentsByStudent.set(studentId, list);
99699
99717
  }
99700
- const students = allUsers.filter((entry) => Boolean(entry.sourcedId) && entry.roles?.some((role) => role.role === "student") === true).map((entry) => {
99718
+ const students = allUsers.flatMap((entry) => {
99719
+ const primaryRole = resolvePrimaryAccountRole(entry.roles);
99720
+ if (!entry.sourcedId || !primaryRole) {
99721
+ return [];
99722
+ }
99701
99723
  const past = pastEnrollmentsByStudent.get(entry.sourcedId) ?? [];
99702
- return {
99703
- studentId: entry.sourcedId,
99704
- name: `${entry.givenName || ""} ${entry.familyName || ""}`.trim() || "No name specified",
99705
- email: entry.email || null,
99706
- alreadyEnrolled: enrolledStudentIds.has(entry.sourcedId),
99707
- ...past.length > 0 ? { pastEnrollments: past } : {}
99708
- };
99724
+ return [
99725
+ {
99726
+ studentId: entry.sourcedId,
99727
+ name: `${entry.givenName || ""} ${entry.familyName || ""}`.trim() || "No name specified",
99728
+ email: entry.email || null,
99729
+ primaryRole,
99730
+ alreadyEnrolled: enrolledStudentIds.has(entry.sourcedId),
99731
+ ...past.length > 0 ? { pastEnrollments: past } : {}
99732
+ }
99733
+ ];
99709
99734
  });
99710
99735
  setAttribute("app.timeback.result_count", students.length);
99711
99736
  return { students };
@@ -106222,6 +106247,69 @@ var init_timeback_assessment_import_util = __esm(() => {
106222
106247
  init_timeback_qti_authoring_util();
106223
106248
  });
106224
106249
 
106250
+ // ../api-core/src/utils/timeback-assessment-revision.util.ts
106251
+ function assertRevisableAssessment(row, reviewBankIdentifier) {
106252
+ if (row.purpose === "review" || row.qtiTestIdentifier === reviewBankIdentifier) {
106253
+ throw new ValidationError("The review bank is system-managed. Revise its mastery sources instead.");
106254
+ }
106255
+ if (row.purpose === "diagnostic") {
106256
+ throw new ValidationError("Diagnostics are revised by importing a new revision and archiving the current one.");
106257
+ }
106258
+ if (row.status !== "live") {
106259
+ throw new ValidationError(row.status === "draft" ? "Draft assessments are edited directly. Only live assessments can be revised in place." : "Archived assessments cannot be revised. Restore it to live first.");
106260
+ }
106261
+ if (!row.assessmentKey) {
106262
+ throw new ValidationError("This unmanaged assessment cannot be revised. Republish it with an explicit assessment key.");
106263
+ }
106264
+ }
106265
+ function assertRevisableTestStructure(current) {
106266
+ const parts2 = qtiTestParts(current);
106267
+ const sectionCount = parts2.reduce((count, part) => count + part["qti-assessment-section"].length, 0);
106268
+ if (parts2.length !== 1 || sectionCount !== 1) {
106269
+ throw new ValidationError(`Assessment ${current.identifier} has ${parts2.length} test part(s) and ${sectionCount} section(s). Revisions can only replace content in single-section assessments.`);
106270
+ }
106271
+ }
106272
+ function buildRevisionSourceTestInput(input) {
106273
+ assertRevisableTestStructure(input.current);
106274
+ const { "qti-outcome-declaration": outcomeDeclarations, ...structure } = buildQtiTestStructureInput(input.current, input.sourceIdentifier);
106275
+ return {
106276
+ identifier: input.sourceIdentifier,
106277
+ title: input.title,
106278
+ ...qtiTestOptionalAttributes(input.current),
106279
+ metadata: {
106280
+ ...sourceMetadata(input.current.metadata),
106281
+ ownerSystem: PLAYCADEMY_QTI_OWNER_SYSTEM,
106282
+ ownerGameSlug: input.gameSlug,
106283
+ assessmentKey: input.assessmentKey,
106284
+ integrationId: input.integrationId,
106285
+ subject: input.subject,
106286
+ grade: String(input.grade),
106287
+ [PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY]: emptyReviewBankManifest(input.sourceIdentifier)
106288
+ },
106289
+ ...outcomeDeclarations && outcomeDeclarations.length > 0 ? { "qti-outcome-declaration": outcomeDeclarations } : {},
106290
+ "qti-test-part": structure["qti-test-part"].map((part) => ({
106291
+ ...part,
106292
+ "qti-assessment-section": part["qti-assessment-section"].map((section) => ({
106293
+ ...section,
106294
+ "qti-assessment-item-ref": []
106295
+ }))
106296
+ }))
106297
+ };
106298
+ }
106299
+ function revisionConflict(requestedIdentifier, currentIdentifier) {
106300
+ return new ConflictError("This assessment was revised by someone else. Reload to see the current version before revising again.", {
106301
+ requestedQtiTestIdentifier: requestedIdentifier,
106302
+ currentQtiTestIdentifier: currentIdentifier
106303
+ });
106304
+ }
106305
+ var init_timeback_assessment_revision_util = __esm(() => {
106306
+ init_assessment_runtime2();
106307
+ init_timeback3();
106308
+ init_errors2();
106309
+ init_timeback_assessment_publication_util();
106310
+ init_timeback_qti_authoring_util();
106311
+ });
106312
+
106225
106313
  // ../api-core/src/utils/timeback-qti-library.util.ts
106226
106314
  function newPlaycademyTestIdentifier() {
106227
106315
  return `${PLAYCADEMY_QTI_SOURCE_PREFIX}${crypto.randomUUID()}`;
@@ -106647,6 +106735,109 @@ class TimebackAssessmentsService {
106647
106735
  throw error88;
106648
106736
  }
106649
106737
  }
106738
+ async reviseAssessment(integrationId, input) {
106739
+ const client2 = this.requireClient();
106740
+ const reviewBankIdentifier = reviewBankSinkIdentifier(integrationId);
106741
+ const current = await this.requireCurrentRevisionRow(integrationId, input.currentQtiTestIdentifier);
106742
+ assertRevisableAssessment(current, reviewBankIdentifier);
106743
+ const ownership = await this.requireQtiTestOwnershipContext(integrationId);
106744
+ const { integration } = ownership;
106745
+ const creations = input.questions.map((question) => {
106746
+ const creation = parseQtiQuestionCreationInput(question);
106747
+ if (creation.kind === "authoring") {
106748
+ assertSupportedQtiQuestionInteraction(creation.input);
106749
+ }
106750
+ return creation;
106751
+ });
106752
+ assertAssessmentHasQuestions(creations);
106753
+ const currentTest = await client2.qtiApi.assessmentTests.get(current.qtiTestIdentifier);
106754
+ const sourceIdentifier = input.sourceTestIdentifier;
106755
+ const sourceInput = buildRevisionSourceTestInput({
106756
+ sourceIdentifier,
106757
+ title: input.title,
106758
+ current: currentTest,
106759
+ assessmentKey: current.assessmentKey,
106760
+ integrationId,
106761
+ gameSlug: ownership.gameSlug,
106762
+ subject: integration.subject,
106763
+ grade: integration.grade
106764
+ });
106765
+ const createdItemIdentifiers = [];
106766
+ let sourceCreateAttempted = false;
106767
+ try {
106768
+ sourceCreateAttempted = true;
106769
+ await client2.qtiApi.assessmentTests.create(sourceInput);
106770
+ const sourceTest = await client2.qtiApi.assessmentTests.get(sourceIdentifier);
106771
+ for (const creation of creations) {
106772
+ const created = await this.createQuestionInOwnedTest({
106773
+ client: client2,
106774
+ integrationId,
106775
+ qtiTestIdentifier: sourceIdentifier,
106776
+ test: sourceTest,
106777
+ gameSlug: ownership.gameSlug,
106778
+ creation
106779
+ });
106780
+ createdItemIdentifiers.push(created.reference.identifier);
106781
+ }
106782
+ const loaded = await loadHydratedQtiTest(client2, sourceIdentifier);
106783
+ await this.validateAssessmentHasQuestions(loaded);
106784
+ const publication = await this.publishManagedAssessment(current, current.purpose, null, loaded);
106785
+ if (!publication.artifactsAvailable) {
106786
+ throw new ValidationError("The revised assessment could not be compiled for the runtime. The current version stays live; try again.");
106787
+ }
106788
+ const result = await this.deps.db.transaction(async (tx) => {
106789
+ const rows = await tx.select().from(gameTimebackAssessmentTests).where(and(eq(gameTimebackAssessmentTests.integrationId, integrationId), eq(gameTimebackAssessmentTests.assessmentKey, current.assessmentKey))).for("update");
106790
+ const row = rows[0];
106791
+ if (!row) {
106792
+ throw new NotFoundError(`Assessment not found: ${input.currentQtiTestIdentifier}`);
106793
+ }
106794
+ if (row.qtiTestIdentifier !== input.currentQtiTestIdentifier) {
106795
+ throw revisionConflict(input.currentQtiTestIdentifier, row.qtiTestIdentifier);
106796
+ }
106797
+ assertRevisableAssessment(row, reviewBankIdentifier);
106798
+ const previousQtiTestIdentifier = row.qtiTestIdentifier;
106799
+ const unchanged = publication.qtiTestIdentifier === previousQtiTestIdentifier;
106800
+ if (unchanged) {
106801
+ setAttribute("app.assessment.operation", "revise_unchanged");
106802
+ return { row, previousQtiTestIdentifier, unchanged };
106803
+ }
106804
+ const [updatedRow] = await tx.update(gameTimebackAssessmentTests).set({
106805
+ qtiTestIdentifier: publication.qtiTestIdentifier,
106806
+ updatedAt: new Date
106807
+ }).where(eq(gameTimebackAssessmentTests.id, row.id)).returning();
106808
+ setAttribute("app.assessment.operation", "revise");
106809
+ addEvent("assessment.revised", {
106810
+ "app.assessment.assessment_key": row.assessmentKey,
106811
+ "app.assessment.previous_qti_test_identifier": previousQtiTestIdentifier,
106812
+ "app.assessment.qti_test_identifier": publication.qtiTestIdentifier
106813
+ });
106814
+ return { row: updatedRow ?? row, previousQtiTestIdentifier, unchanged };
106815
+ });
106816
+ let warning;
106817
+ if (!result.unchanged && result.row.purpose === "mastery") {
106818
+ try {
106819
+ const sync = await this.synchronizeReviewBank(integrationId);
106820
+ if (sync.warnings.length > 0) {
106821
+ warning = `Review bank: ${sync.warnings.map((entry) => entry.message).join(" ")}`;
106822
+ }
106823
+ } catch (error88) {
106824
+ addEvent("assessment.revision_review_bank_sync_failed", {
106825
+ "app.assessment.qti_test_identifier": result.row.qtiTestIdentifier,
106826
+ "exception.type": errorType(error88),
106827
+ "app.error.message": errorMessage(error88)
106828
+ });
106829
+ warning = `The review bank could not be synchronized: ${errorMessage(error88)}. Use Sync Review Bank to retry.`;
106830
+ }
106831
+ }
106832
+ return {
106833
+ ...this.associationSummary(result.row, warning),
106834
+ previousQtiTestIdentifier: result.previousQtiTestIdentifier,
106835
+ unchanged: result.unchanged
106836
+ };
106837
+ } finally {
106838
+ await this.cleanupQtiAssessmentCopy(client2, sourceCreateAttempted ? sourceIdentifier : undefined, createdItemIdentifiers);
106839
+ }
106840
+ }
106650
106841
  async removeAssessment(integrationId, qtiTestIdentifier) {
106651
106842
  return this.withLockedAssessmentAssociations(integrationId, qtiTestIdentifier, async (row, tx) => {
106652
106843
  if (qtiTestIdentifier === reviewBankSinkIdentifier(integrationId)) {
@@ -106919,106 +107110,118 @@ class TimebackAssessmentsService {
106919
107110
  assertAllAssessmentAssociationsDraft(associations);
106920
107111
  const ownership = await this.requireOwnedDraftAssessment(integrationId, qtiTestIdentifier, row, tx);
106921
107112
  const creation = parseQtiQuestionCreationInput(input);
106922
- const itemIdentifier = newPlaycademyQuestionIdentifier(qtiTestIdentifier);
106923
- let title;
106924
- let metadata2;
106925
- let xml;
106926
- let structuredInput;
106927
- let fallbackItem;
106928
- if (creation.kind === "copy") {
106929
- const source = await client2.qtiApi.assessmentItems.get(creation.sourceItemIdentifier);
106930
- const copy = buildQtiQuestionCopyInput({
106931
- source,
106932
- targetIdentifier: itemIdentifier,
106933
- targetTestIdentifier: qtiTestIdentifier,
106934
- gameSlug: ownership.gameSlug,
106935
- integrationId
106936
- });
106937
- title = copy.title;
106938
- metadata2 = copy.metadata;
106939
- xml = copy.xml;
106940
- fallbackItem = {
106941
- ...source,
106942
- identifier: itemIdentifier,
106943
- title,
106944
- rawXml: xml,
106945
- metadata: metadata2
106946
- };
106947
- } else {
106948
- structuredInput = creation.input;
106949
- assertSupportedQtiQuestionInteraction(structuredInput);
106950
- title = typeof structuredInput.title === "string" ? structuredInput.title.trim() : "";
106951
- metadata2 = buildOwnedQtiQuestionMetadata(structuredInput, {
106952
- gameSlug: ownership.gameSlug,
106953
- testIdentifier: qtiTestIdentifier
106954
- });
106955
- xml = buildQuestionXml(structuredInput, itemIdentifier, title);
106956
- fallbackItem = {
106957
- ...structuredInput,
106958
- identifier: itemIdentifier,
106959
- title,
106960
- ...xml ? { rawXml: xml } : {}
106961
- };
106962
- }
106963
- const section = await this.qtiSectionItems(client2, qtiTestIdentifier, undefined, undefined, ownership.test);
106964
- const href = qtiItemHref(client2, itemIdentifier);
106965
- let item;
106966
- let itemCreationAttempted = false;
106967
- let referenceCreationAttempted = false;
106968
- try {
106969
- itemCreationAttempted = true;
106970
- item = xml ? await client2.course.createAssessmentItemXml({ xml, metadata: metadata2 }) : await client2.qtiApi.assessmentItems.create({
106971
- ...structuredInput,
106972
- identifier: itemIdentifier,
106973
- metadata: metadata2
106974
- });
106975
- referenceCreationAttempted = true;
106976
- await section.items.add({
106977
- identifier: itemIdentifier,
106978
- href
106979
- });
106980
- } catch (error88) {
106981
- let referenceRemoved = !referenceCreationAttempted;
106982
- const creationStatus = isApiError(error88) ? error88.statusCode : undefined;
106983
- const itemCreationDefinitelyRejected = !referenceCreationAttempted && typeof creationStatus === "number" && creationStatus >= 400 && creationStatus < 500 && creationStatus !== 408;
106984
- if (referenceCreationAttempted) {
106985
- try {
106986
- await section.items.remove(itemIdentifier);
106987
- referenceRemoved = true;
106988
- } catch (cleanupError) {
106989
- referenceRemoved = isApiError(cleanupError) && cleanupError.statusCode === 404;
106990
- addEvent("assessment.qti_question_reference_cleanup_failed", {
106991
- "app.assessment.qti_test_identifier": qtiTestIdentifier,
106992
- "app.assessment.qti_item_identifier": itemIdentifier,
106993
- "exception.type": errorType(cleanupError),
106994
- "app.error.message": errorMessage(cleanupError)
106995
- });
106996
- }
106997
- }
106998
- if (itemCreationAttempted && !itemCreationDefinitelyRejected && referenceRemoved) {
106999
- try {
107000
- await client2.qtiApi.assessmentItems.delete(itemIdentifier);
107001
- } catch (cleanupError) {
107002
- addEvent("assessment.qti_question_cleanup_failed", {
107003
- "app.assessment.qti_test_identifier": qtiTestIdentifier,
107004
- "app.assessment.qti_item_identifier": itemIdentifier,
107005
- "exception.type": errorType(cleanupError),
107006
- "app.error.message": errorMessage(cleanupError)
107007
- });
107008
- }
107009
- }
107010
- throw error88;
107011
- }
107113
+ const created = await this.createQuestionInOwnedTest({
107114
+ client: client2,
107115
+ integrationId,
107116
+ qtiTestIdentifier,
107117
+ test: ownership.test,
107118
+ gameSlug: ownership.gameSlug,
107119
+ creation
107120
+ });
107012
107121
  setAttribute("app.assessment.operation", creation.kind === "copy" ? "copy_question" : "create_question");
107013
- return buildCreatedQtiQuestionReference({
107014
- item,
107015
- fallbackItem,
107016
- itemIdentifier,
107017
- href,
107018
- partIdentifier: section.partIdentifier,
107019
- sectionIdentifier: section.sectionIdentifier,
107122
+ return created;
107123
+ });
107124
+ }
107125
+ async createQuestionInOwnedTest(input) {
107126
+ const { client: client2, integrationId, qtiTestIdentifier, creation } = input;
107127
+ const itemIdentifier = newPlaycademyQuestionIdentifier(qtiTestIdentifier);
107128
+ let title;
107129
+ let metadata2;
107130
+ let xml;
107131
+ let structuredInput;
107132
+ let fallbackItem;
107133
+ if (creation.kind === "copy") {
107134
+ const source = await client2.qtiApi.assessmentItems.get(creation.sourceItemIdentifier);
107135
+ const copy = buildQtiQuestionCopyInput({
107136
+ source,
107137
+ targetIdentifier: itemIdentifier,
107138
+ targetTestIdentifier: qtiTestIdentifier,
107139
+ gameSlug: input.gameSlug,
107140
+ integrationId
107141
+ });
107142
+ title = copy.title;
107143
+ metadata2 = copy.metadata;
107144
+ xml = copy.xml;
107145
+ fallbackItem = {
107146
+ ...source,
107147
+ identifier: itemIdentifier,
107148
+ title,
107149
+ rawXml: xml,
107150
+ metadata: metadata2
107151
+ };
107152
+ } else {
107153
+ structuredInput = creation.input;
107154
+ assertSupportedQtiQuestionInteraction(structuredInput);
107155
+ title = typeof structuredInput.title === "string" ? structuredInput.title.trim() : "";
107156
+ metadata2 = buildOwnedQtiQuestionMetadata(structuredInput, {
107157
+ gameSlug: input.gameSlug,
107158
+ testIdentifier: qtiTestIdentifier
107159
+ });
107160
+ xml = buildQuestionXml(structuredInput, itemIdentifier, title);
107161
+ fallbackItem = {
107162
+ ...structuredInput,
107163
+ identifier: itemIdentifier,
107164
+ title,
107165
+ ...xml ? { rawXml: xml } : {}
107166
+ };
107167
+ }
107168
+ const section = await this.qtiSectionItems(client2, qtiTestIdentifier, undefined, undefined, input.test);
107169
+ const href = qtiItemHref(client2, itemIdentifier);
107170
+ let item;
107171
+ let itemCreationAttempted = false;
107172
+ let referenceCreationAttempted = false;
107173
+ try {
107174
+ itemCreationAttempted = true;
107175
+ item = xml ? await client2.course.createAssessmentItemXml({ xml, metadata: metadata2 }) : await client2.qtiApi.assessmentItems.create({
107176
+ ...structuredInput,
107177
+ identifier: itemIdentifier,
107020
107178
  metadata: metadata2
107021
107179
  });
107180
+ referenceCreationAttempted = true;
107181
+ await section.items.add({
107182
+ identifier: itemIdentifier,
107183
+ href
107184
+ });
107185
+ } catch (error88) {
107186
+ let referenceRemoved = !referenceCreationAttempted;
107187
+ const creationStatus = isApiError(error88) ? error88.statusCode : undefined;
107188
+ const itemCreationDefinitelyRejected = !referenceCreationAttempted && typeof creationStatus === "number" && creationStatus >= 400 && creationStatus < 500 && creationStatus !== 408;
107189
+ if (referenceCreationAttempted) {
107190
+ try {
107191
+ await section.items.remove(itemIdentifier);
107192
+ referenceRemoved = true;
107193
+ } catch (cleanupError) {
107194
+ referenceRemoved = isApiError(cleanupError) && cleanupError.statusCode === 404;
107195
+ addEvent("assessment.qti_question_reference_cleanup_failed", {
107196
+ "app.assessment.qti_test_identifier": qtiTestIdentifier,
107197
+ "app.assessment.qti_item_identifier": itemIdentifier,
107198
+ "exception.type": errorType(cleanupError),
107199
+ "app.error.message": errorMessage(cleanupError)
107200
+ });
107201
+ }
107202
+ }
107203
+ if (itemCreationAttempted && !itemCreationDefinitelyRejected && referenceRemoved) {
107204
+ try {
107205
+ await client2.qtiApi.assessmentItems.delete(itemIdentifier);
107206
+ } catch (cleanupError) {
107207
+ addEvent("assessment.qti_question_cleanup_failed", {
107208
+ "app.assessment.qti_test_identifier": qtiTestIdentifier,
107209
+ "app.assessment.qti_item_identifier": itemIdentifier,
107210
+ "exception.type": errorType(cleanupError),
107211
+ "app.error.message": errorMessage(cleanupError)
107212
+ });
107213
+ }
107214
+ }
107215
+ throw error88;
107216
+ }
107217
+ return buildCreatedQtiQuestionReference({
107218
+ item,
107219
+ fallbackItem,
107220
+ itemIdentifier,
107221
+ href,
107222
+ partIdentifier: section.partIdentifier,
107223
+ sectionIdentifier: section.sectionIdentifier,
107224
+ metadata: metadata2
107022
107225
  });
107023
107226
  }
107024
107227
  async updateQuestion(integrationId, qtiTestIdentifier, itemIdentifier, input) {
@@ -107266,6 +107469,22 @@ class TimebackAssessmentsService {
107266
107469
  }
107267
107470
  return row;
107268
107471
  }
107472
+ async requireCurrentRevisionRow(integrationId, qtiTestIdentifier) {
107473
+ const row = await this.deps.db.query.gameTimebackAssessmentTests.findFirst({
107474
+ where: and(eq(gameTimebackAssessmentTests.integrationId, integrationId), eq(gameTimebackAssessmentTests.qtiTestIdentifier, qtiTestIdentifier))
107475
+ });
107476
+ if (row) {
107477
+ return row;
107478
+ }
107479
+ const assessmentKey = assessmentKeyFromManagedQtiIdentifier(qtiTestIdentifier);
107480
+ const successor = assessmentKey ? await this.deps.db.query.gameTimebackAssessmentTests.findFirst({
107481
+ where: and(eq(gameTimebackAssessmentTests.integrationId, integrationId), eq(gameTimebackAssessmentTests.assessmentKey, assessmentKey))
107482
+ }) : undefined;
107483
+ if (successor) {
107484
+ throw revisionConflict(qtiTestIdentifier, successor.qtiTestIdentifier);
107485
+ }
107486
+ throw new NotFoundError(`Assessment not found: ${qtiTestIdentifier}`);
107487
+ }
107269
107488
  async requireDraftAssessmentRow(integrationId, qtiTestIdentifier, lockedRow) {
107270
107489
  const row = lockedRow ?? await this.requireAssessmentRow(integrationId, qtiTestIdentifier);
107271
107490
  assertDraftAssessment(row);
@@ -107413,9 +107632,21 @@ class TimebackAssessmentsService {
107413
107632
  itemHref: (identifier) => qtiItemHref(client2, identifier)
107414
107633
  });
107415
107634
  const artifactSource = await this.ensureManagedQtiPublication(client2, plan);
107416
- const artifactsAvailable = await this.storeManagedArtifacts(artifactSource, managedAssessmentDiagnosticRoutingManifest(artifactSource.test, plan.assessmentKey));
107635
+ const artifactsAvailable = await this.managedArtifactsStored(plan.qtiTestIdentifier) || await this.storeManagedArtifacts(artifactSource, managedAssessmentDiagnosticRoutingManifest(artifactSource.test, plan.assessmentKey));
107417
107636
  const warning = assessmentArtifactPublicationWarning(artifactsAvailable);
107418
- return { ...plan, ...warning ? { warning } : {} };
107637
+ return { ...plan, artifactsAvailable, ...warning ? { warning } : {} };
107638
+ }
107639
+ async managedArtifactsStored(qtiTestIdentifier) {
107640
+ try {
107641
+ return await this.deps.assessmentArtifacts.loadManagedPlayable(qtiTestIdentifier) !== null;
107642
+ } catch (error88) {
107643
+ addEvent("assessment.artifact_lookup_failed", {
107644
+ "app.assessment.qti_test_identifier": qtiTestIdentifier,
107645
+ "exception.type": errorType(error88),
107646
+ "app.error.message": errorMessage(error88)
107647
+ });
107648
+ return false;
107649
+ }
107419
107650
  }
107420
107651
  async cleanupQtiAssessmentCopy(client2, testIdentifier, itemIdentifiers) {
107421
107652
  let testDeleted = !testIdentifier;
@@ -107542,6 +107773,7 @@ var init_timeback_assessments_service = __esm(async () => {
107542
107773
  init_timeback_assessment_diagnostic_util();
107543
107774
  init_timeback_assessment_import_util();
107544
107775
  init_timeback_assessment_publication_util();
107776
+ init_timeback_assessment_revision_util();
107545
107777
  init_timeback_assessment_rules_util();
107546
107778
  init_timeback_qti_authoring_util();
107547
107779
  init_timeback_qti_hydration_util();
@@ -167551,7 +167783,7 @@ function parseQtiLibraryParams(searchParams) {
167551
167783
  limit
167552
167784
  };
167553
167785
  }
167554
- var populateStudent, getUser, getUserEnrollments, getUserById, setupIntegration, getIntegrations, getRemovedIntegrations, createIntegration, updateIntegration, deactivateCourse, reactivateCourse, getIntegrationConfig, verifyIntegration, getConfig, deleteIntegrations, endActivity, heartbeat, advanceCourse, unenrollCourse, prepareRuntimeAssessment, startRuntimeAssessment, getRuntimeAssessment, getLatestRuntimeAssessment, resumeRuntimeAssessment, saveRuntimeAssessment, recordRuntimeAssessmentProgress, submitRuntimeAssessmentItem, submitRuntimeAssessment, finalizeRuntimeAssessment, exportRuntimeAssessmentFixture, getStudentXp, getStudentMastery, getStudentHighestGradeMastered, getRoster, getStudentOverview, getGameMetrics, getStudentActivity, getGradeLevelTestResults, getGradeLevelTestReview, getActivityDetail, listMetricDiscrepancies, verifyMetricDiscrepancy, grantXp, adjustTime, adjustMastery, reconcileMasteryForConfigChange, searchStudents, enrollStudent, unenrollStudent, reactivateEnrollment, listAssessments, createAssessment, attachExistingAssessments, updateAssessment, synchronizeReviewBank, reorderQuestions, removeAssessment, listQuestions, listQuestionLibrary, listTestLibrary, copyAssessment, createQuestion, updateQuestion, removeQuestion, timeback2;
167786
+ var populateStudent, getUser, getUserEnrollments, getUserById, setupIntegration, getIntegrations, getRemovedIntegrations, createIntegration, updateIntegration, deactivateCourse, reactivateCourse, getIntegrationConfig, verifyIntegration, getConfig, deleteIntegrations, endActivity, heartbeat, advanceCourse, unenrollCourse, prepareRuntimeAssessment, startRuntimeAssessment, getRuntimeAssessment, getLatestRuntimeAssessment, resumeRuntimeAssessment, saveRuntimeAssessment, recordRuntimeAssessmentProgress, submitRuntimeAssessmentItem, submitRuntimeAssessment, finalizeRuntimeAssessment, exportRuntimeAssessmentFixture, getStudentXp, getStudentMastery, getStudentHighestGradeMastered, getRoster, getStudentOverview, getGameMetrics, getStudentActivity, getGradeLevelTestResults, getGradeLevelTestReview, getActivityDetail, listMetricDiscrepancies, verifyMetricDiscrepancy, grantXp, adjustTime, adjustMastery, reconcileMasteryForConfigChange, searchStudents, enrollStudent, unenrollStudent, reactivateEnrollment, listAssessments, createAssessment, attachExistingAssessments, updateAssessment, reviseAssessment, synchronizeReviewBank, reorderQuestions, removeAssessment, listQuestions, listQuestionLibrary, listTestLibrary, copyAssessment, createQuestion, updateQuestion, removeQuestion, timeback2;
167555
167787
  var init_timeback_controller = __esm(() => {
167556
167788
  init_esm();
167557
167789
  init_src();
@@ -168296,6 +168528,20 @@ var init_timeback_controller = __esm(() => {
168296
168528
  const body2 = await parseRequestBody(ctx.request, UpdateAssessmentRequestSchema);
168297
168529
  return ctx.services.timebackAssessments.updateAssessment(integrationId, testIdentifier, body2);
168298
168530
  });
168531
+ reviseAssessment = requireDeveloper(async (ctx) => {
168532
+ const { gameId, courseId } = ctx.params;
168533
+ if (!gameId || !courseId) {
168534
+ throw ApiError.badRequest("Missing gameId or courseId parameter");
168535
+ }
168536
+ const body2 = await parseRequestBody(ctx.request, ReviseAssessmentRequestSchema);
168537
+ const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
168538
+ return ctx.services.timebackAssessments.reviseAssessment(integrationId, {
168539
+ currentQtiTestIdentifier: body2.currentQtiTestIdentifier,
168540
+ sourceTestIdentifier: newPlaycademyTestIdentifier(),
168541
+ title: body2.title,
168542
+ questions: body2.questions
168543
+ });
168544
+ });
168299
168545
  synchronizeReviewBank = requireDeveloper(async (ctx) => {
168300
168546
  const { gameId, courseId } = ctx.params;
168301
168547
  if (!gameId || !courseId) {
@@ -168446,6 +168692,7 @@ var init_timeback_controller = __esm(() => {
168446
168692
  createAssessment,
168447
168693
  attachExistingAssessments,
168448
168694
  updateAssessment,
168695
+ reviseAssessment,
168449
168696
  synchronizeReviewBank,
168450
168697
  removeAssessment,
168451
168698
  reorderQuestions,
package/dist/server.js CHANGED
@@ -1127,7 +1127,7 @@ var package_default;
1127
1127
  var init_package = __esm(() => {
1128
1128
  package_default = {
1129
1129
  name: "@playcademy/sandbox",
1130
- version: "0.10.1-beta.4",
1130
+ version: "0.10.1-beta.6",
1131
1131
  description: "Local development server for Playcademy game development",
1132
1132
  type: "module",
1133
1133
  exports: {
@@ -47988,7 +47988,7 @@ function rejectSystemManagedReview(input, context2) {
47988
47988
  });
47989
47989
  }
47990
47990
  }
47991
- var TimebackGradeSchema, TimebackSubjectSchema, CourseGoalsSchema, UpdateGameTimebackIntegrationRequestSchema, CreateGameTimebackIntegrationRequestSchema, TimebackActivityDataSchema, EndActivityRequestSchema, GameRunMetricsSchema, GameCourseMetricsSchema, GameMetricsResponseSchema, AdvanceCourseRequestSchema, UnenrollCourseRequestSchema, HeartbeatRequestSchema, PopulateStudentRequestSchema, DerivedPlatformCourseConfigSchema, TimebackBaseConfigSchema, PlatformTimebackSetupRequestSchema, AdminTimebackMutationBaseSchema, AdminAttributionDateSchema, ADMIN_GRANT_XP_MIN = -1e5, ADMIN_GRANT_XP_MAX = 1e5, ADMIN_GRANT_XP_AMOUNT_RANGE_MESSAGE, GrantTimebackXpRequestSchema, AdjustTimebackTimeRequestSchema, AdjustTimebackMasteryRequestSchema, ReconcileMasteryForConfigChangeSchema, EnrollStudentRequestSchema, UnenrollStudentRequestSchema, ReactivateEnrollmentRequestSchema, VerifyTimebackMetricDiscrepancyRequestSchema, AssessmentPurposeSchema, AssessmentStatusSchema, AssessmentKeySchema, DiagnosticKeySchema, DiagnosticRoutingManifestSchema, DiagnosticAssessmentDefinitionInputSchema, AssessmentStandardRefSchema3, CreateAssessmentRequestSchema, UpdateAssessmentRequestSchema, CopyAssessmentRequestSchema, AssessmentAssociationImportEntrySchema, AttachExistingAssessmentsRequestSchema, ReorderQuestionsRequestSchema;
47991
+ var TimebackGradeSchema, TimebackSubjectSchema, CourseGoalsSchema, UpdateGameTimebackIntegrationRequestSchema, CreateGameTimebackIntegrationRequestSchema, TimebackActivityDataSchema, EndActivityRequestSchema, GameRunMetricsSchema, GameCourseMetricsSchema, GameMetricsResponseSchema, AdvanceCourseRequestSchema, UnenrollCourseRequestSchema, HeartbeatRequestSchema, PopulateStudentRequestSchema, DerivedPlatformCourseConfigSchema, TimebackBaseConfigSchema, PlatformTimebackSetupRequestSchema, AdminTimebackMutationBaseSchema, AdminAttributionDateSchema, ADMIN_GRANT_XP_MIN = -1e5, ADMIN_GRANT_XP_MAX = 1e5, ADMIN_GRANT_XP_AMOUNT_RANGE_MESSAGE, GrantTimebackXpRequestSchema, AdjustTimebackTimeRequestSchema, AdjustTimebackMasteryRequestSchema, ReconcileMasteryForConfigChangeSchema, EnrollStudentRequestSchema, UnenrollStudentRequestSchema, ReactivateEnrollmentRequestSchema, VerifyTimebackMetricDiscrepancyRequestSchema, AssessmentPurposeSchema, AssessmentStatusSchema, AssessmentKeySchema, DiagnosticKeySchema, DiagnosticRoutingManifestSchema, DiagnosticAssessmentDefinitionInputSchema, AssessmentStandardRefSchema3, CreateAssessmentRequestSchema, UpdateAssessmentRequestSchema, CopyAssessmentRequestSchema, ReviseAssessmentRequestSchema, AssessmentAssociationImportEntrySchema, AttachExistingAssessmentsRequestSchema, ReorderQuestionsRequestSchema;
47992
47992
  var init_schemas4 = __esm(() => {
47993
47993
  init_esm();
47994
47994
  init_src();
@@ -48271,6 +48271,11 @@ var init_schemas4 = __esm(() => {
48271
48271
  requireMasteryStandard(input, context2);
48272
48272
  rejectSystemManagedReview(input, context2);
48273
48273
  });
48274
+ ReviseAssessmentRequestSchema = exports_external.object({
48275
+ currentQtiTestIdentifier: exports_external.string().trim().min(1, "The current assessment identifier is required"),
48276
+ title: exports_external.string().trim().min(1, "Assessment title is required"),
48277
+ questions: exports_external.array(exports_external.record(exports_external.string(), exports_external.unknown())).min(1, "A revision must contain at least one question").max(500, "A revision cannot contain more than 500 questions")
48278
+ });
48274
48279
  AssessmentAssociationImportEntrySchema = exports_external.object({
48275
48280
  assessmentKey: AssessmentKeySchema,
48276
48281
  qtiTestIdentifier: exports_external.string().trim().min(1, "Assessment identifier is required"),
@@ -94022,8 +94027,20 @@ function compareEnrollmentsByRecency(a, b) {
94022
94027
  }
94023
94028
  return (b.dateLastModified ?? "").localeCompare(a.dateLastModified ?? "");
94024
94029
  }
94030
+ function resolvePrimaryAccountRole(roles2) {
94031
+ if (!roles2?.length) {
94032
+ return null;
94033
+ }
94034
+ return ACCOUNT_ROLE_PRECEDENCE.find((role) => roles2.some((entry) => entry.role === role)) ?? null;
94035
+ }
94036
+ var ACCOUNT_ROLE_PRECEDENCE;
94025
94037
  var init_timeback_admin_util = __esm(() => {
94026
94038
  init_errors2();
94039
+ ACCOUNT_ROLE_PRECEDENCE = [
94040
+ "administrator",
94041
+ "teacher",
94042
+ "student"
94043
+ ];
94027
94044
  });
94028
94045
 
94029
94046
  // ../api-core/src/utils/timeback-discrepancy-queue.util.ts
@@ -98733,6 +98750,7 @@ class TimebackAdminService {
98733
98750
  className: rosterEntry.class?.title || null,
98734
98751
  name: name3,
98735
98752
  email: rosterEntry.user?.email || null,
98753
+ primaryRole: resolvePrimaryAccountRole(rosterEntry.user?.roles),
98736
98754
  analyticsUnavailable,
98737
98755
  totalXp: summary?.totalXp ?? 0,
98738
98756
  todayXp: summary?.todayXp ?? 0,
@@ -99696,15 +99714,22 @@ class TimebackAdminService {
99696
99714
  });
99697
99715
  pastEnrollmentsByStudent.set(studentId, list);
99698
99716
  }
99699
- const students = allUsers.filter((entry) => Boolean(entry.sourcedId) && entry.roles?.some((role) => role.role === "student") === true).map((entry) => {
99717
+ const students = allUsers.flatMap((entry) => {
99718
+ const primaryRole = resolvePrimaryAccountRole(entry.roles);
99719
+ if (!entry.sourcedId || !primaryRole) {
99720
+ return [];
99721
+ }
99700
99722
  const past = pastEnrollmentsByStudent.get(entry.sourcedId) ?? [];
99701
- return {
99702
- studentId: entry.sourcedId,
99703
- name: `${entry.givenName || ""} ${entry.familyName || ""}`.trim() || "No name specified",
99704
- email: entry.email || null,
99705
- alreadyEnrolled: enrolledStudentIds.has(entry.sourcedId),
99706
- ...past.length > 0 ? { pastEnrollments: past } : {}
99707
- };
99723
+ return [
99724
+ {
99725
+ studentId: entry.sourcedId,
99726
+ name: `${entry.givenName || ""} ${entry.familyName || ""}`.trim() || "No name specified",
99727
+ email: entry.email || null,
99728
+ primaryRole,
99729
+ alreadyEnrolled: enrolledStudentIds.has(entry.sourcedId),
99730
+ ...past.length > 0 ? { pastEnrollments: past } : {}
99731
+ }
99732
+ ];
99708
99733
  });
99709
99734
  setAttribute("app.timeback.result_count", students.length);
99710
99735
  return { students };
@@ -106221,6 +106246,69 @@ var init_timeback_assessment_import_util = __esm(() => {
106221
106246
  init_timeback_qti_authoring_util();
106222
106247
  });
106223
106248
 
106249
+ // ../api-core/src/utils/timeback-assessment-revision.util.ts
106250
+ function assertRevisableAssessment(row, reviewBankIdentifier) {
106251
+ if (row.purpose === "review" || row.qtiTestIdentifier === reviewBankIdentifier) {
106252
+ throw new ValidationError("The review bank is system-managed. Revise its mastery sources instead.");
106253
+ }
106254
+ if (row.purpose === "diagnostic") {
106255
+ throw new ValidationError("Diagnostics are revised by importing a new revision and archiving the current one.");
106256
+ }
106257
+ if (row.status !== "live") {
106258
+ throw new ValidationError(row.status === "draft" ? "Draft assessments are edited directly. Only live assessments can be revised in place." : "Archived assessments cannot be revised. Restore it to live first.");
106259
+ }
106260
+ if (!row.assessmentKey) {
106261
+ throw new ValidationError("This unmanaged assessment cannot be revised. Republish it with an explicit assessment key.");
106262
+ }
106263
+ }
106264
+ function assertRevisableTestStructure(current) {
106265
+ const parts2 = qtiTestParts(current);
106266
+ const sectionCount = parts2.reduce((count, part) => count + part["qti-assessment-section"].length, 0);
106267
+ if (parts2.length !== 1 || sectionCount !== 1) {
106268
+ throw new ValidationError(`Assessment ${current.identifier} has ${parts2.length} test part(s) and ${sectionCount} section(s). Revisions can only replace content in single-section assessments.`);
106269
+ }
106270
+ }
106271
+ function buildRevisionSourceTestInput(input) {
106272
+ assertRevisableTestStructure(input.current);
106273
+ const { "qti-outcome-declaration": outcomeDeclarations, ...structure } = buildQtiTestStructureInput(input.current, input.sourceIdentifier);
106274
+ return {
106275
+ identifier: input.sourceIdentifier,
106276
+ title: input.title,
106277
+ ...qtiTestOptionalAttributes(input.current),
106278
+ metadata: {
106279
+ ...sourceMetadata(input.current.metadata),
106280
+ ownerSystem: PLAYCADEMY_QTI_OWNER_SYSTEM,
106281
+ ownerGameSlug: input.gameSlug,
106282
+ assessmentKey: input.assessmentKey,
106283
+ integrationId: input.integrationId,
106284
+ subject: input.subject,
106285
+ grade: String(input.grade),
106286
+ [PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY]: emptyReviewBankManifest(input.sourceIdentifier)
106287
+ },
106288
+ ...outcomeDeclarations && outcomeDeclarations.length > 0 ? { "qti-outcome-declaration": outcomeDeclarations } : {},
106289
+ "qti-test-part": structure["qti-test-part"].map((part) => ({
106290
+ ...part,
106291
+ "qti-assessment-section": part["qti-assessment-section"].map((section) => ({
106292
+ ...section,
106293
+ "qti-assessment-item-ref": []
106294
+ }))
106295
+ }))
106296
+ };
106297
+ }
106298
+ function revisionConflict(requestedIdentifier, currentIdentifier) {
106299
+ return new ConflictError("This assessment was revised by someone else. Reload to see the current version before revising again.", {
106300
+ requestedQtiTestIdentifier: requestedIdentifier,
106301
+ currentQtiTestIdentifier: currentIdentifier
106302
+ });
106303
+ }
106304
+ var init_timeback_assessment_revision_util = __esm(() => {
106305
+ init_assessment_runtime2();
106306
+ init_timeback3();
106307
+ init_errors2();
106308
+ init_timeback_assessment_publication_util();
106309
+ init_timeback_qti_authoring_util();
106310
+ });
106311
+
106224
106312
  // ../api-core/src/utils/timeback-qti-library.util.ts
106225
106313
  function newPlaycademyTestIdentifier() {
106226
106314
  return `${PLAYCADEMY_QTI_SOURCE_PREFIX}${crypto.randomUUID()}`;
@@ -106646,6 +106734,109 @@ class TimebackAssessmentsService {
106646
106734
  throw error88;
106647
106735
  }
106648
106736
  }
106737
+ async reviseAssessment(integrationId, input) {
106738
+ const client2 = this.requireClient();
106739
+ const reviewBankIdentifier = reviewBankSinkIdentifier(integrationId);
106740
+ const current = await this.requireCurrentRevisionRow(integrationId, input.currentQtiTestIdentifier);
106741
+ assertRevisableAssessment(current, reviewBankIdentifier);
106742
+ const ownership = await this.requireQtiTestOwnershipContext(integrationId);
106743
+ const { integration } = ownership;
106744
+ const creations = input.questions.map((question) => {
106745
+ const creation = parseQtiQuestionCreationInput(question);
106746
+ if (creation.kind === "authoring") {
106747
+ assertSupportedQtiQuestionInteraction(creation.input);
106748
+ }
106749
+ return creation;
106750
+ });
106751
+ assertAssessmentHasQuestions(creations);
106752
+ const currentTest = await client2.qtiApi.assessmentTests.get(current.qtiTestIdentifier);
106753
+ const sourceIdentifier = input.sourceTestIdentifier;
106754
+ const sourceInput = buildRevisionSourceTestInput({
106755
+ sourceIdentifier,
106756
+ title: input.title,
106757
+ current: currentTest,
106758
+ assessmentKey: current.assessmentKey,
106759
+ integrationId,
106760
+ gameSlug: ownership.gameSlug,
106761
+ subject: integration.subject,
106762
+ grade: integration.grade
106763
+ });
106764
+ const createdItemIdentifiers = [];
106765
+ let sourceCreateAttempted = false;
106766
+ try {
106767
+ sourceCreateAttempted = true;
106768
+ await client2.qtiApi.assessmentTests.create(sourceInput);
106769
+ const sourceTest = await client2.qtiApi.assessmentTests.get(sourceIdentifier);
106770
+ for (const creation of creations) {
106771
+ const created = await this.createQuestionInOwnedTest({
106772
+ client: client2,
106773
+ integrationId,
106774
+ qtiTestIdentifier: sourceIdentifier,
106775
+ test: sourceTest,
106776
+ gameSlug: ownership.gameSlug,
106777
+ creation
106778
+ });
106779
+ createdItemIdentifiers.push(created.reference.identifier);
106780
+ }
106781
+ const loaded = await loadHydratedQtiTest(client2, sourceIdentifier);
106782
+ await this.validateAssessmentHasQuestions(loaded);
106783
+ const publication = await this.publishManagedAssessment(current, current.purpose, null, loaded);
106784
+ if (!publication.artifactsAvailable) {
106785
+ throw new ValidationError("The revised assessment could not be compiled for the runtime. The current version stays live; try again.");
106786
+ }
106787
+ const result = await this.deps.db.transaction(async (tx) => {
106788
+ const rows = await tx.select().from(gameTimebackAssessmentTests).where(and(eq(gameTimebackAssessmentTests.integrationId, integrationId), eq(gameTimebackAssessmentTests.assessmentKey, current.assessmentKey))).for("update");
106789
+ const row = rows[0];
106790
+ if (!row) {
106791
+ throw new NotFoundError(`Assessment not found: ${input.currentQtiTestIdentifier}`);
106792
+ }
106793
+ if (row.qtiTestIdentifier !== input.currentQtiTestIdentifier) {
106794
+ throw revisionConflict(input.currentQtiTestIdentifier, row.qtiTestIdentifier);
106795
+ }
106796
+ assertRevisableAssessment(row, reviewBankIdentifier);
106797
+ const previousQtiTestIdentifier = row.qtiTestIdentifier;
106798
+ const unchanged = publication.qtiTestIdentifier === previousQtiTestIdentifier;
106799
+ if (unchanged) {
106800
+ setAttribute("app.assessment.operation", "revise_unchanged");
106801
+ return { row, previousQtiTestIdentifier, unchanged };
106802
+ }
106803
+ const [updatedRow] = await tx.update(gameTimebackAssessmentTests).set({
106804
+ qtiTestIdentifier: publication.qtiTestIdentifier,
106805
+ updatedAt: new Date
106806
+ }).where(eq(gameTimebackAssessmentTests.id, row.id)).returning();
106807
+ setAttribute("app.assessment.operation", "revise");
106808
+ addEvent("assessment.revised", {
106809
+ "app.assessment.assessment_key": row.assessmentKey,
106810
+ "app.assessment.previous_qti_test_identifier": previousQtiTestIdentifier,
106811
+ "app.assessment.qti_test_identifier": publication.qtiTestIdentifier
106812
+ });
106813
+ return { row: updatedRow ?? row, previousQtiTestIdentifier, unchanged };
106814
+ });
106815
+ let warning;
106816
+ if (!result.unchanged && result.row.purpose === "mastery") {
106817
+ try {
106818
+ const sync = await this.synchronizeReviewBank(integrationId);
106819
+ if (sync.warnings.length > 0) {
106820
+ warning = `Review bank: ${sync.warnings.map((entry) => entry.message).join(" ")}`;
106821
+ }
106822
+ } catch (error88) {
106823
+ addEvent("assessment.revision_review_bank_sync_failed", {
106824
+ "app.assessment.qti_test_identifier": result.row.qtiTestIdentifier,
106825
+ "exception.type": errorType(error88),
106826
+ "app.error.message": errorMessage(error88)
106827
+ });
106828
+ warning = `The review bank could not be synchronized: ${errorMessage(error88)}. Use Sync Review Bank to retry.`;
106829
+ }
106830
+ }
106831
+ return {
106832
+ ...this.associationSummary(result.row, warning),
106833
+ previousQtiTestIdentifier: result.previousQtiTestIdentifier,
106834
+ unchanged: result.unchanged
106835
+ };
106836
+ } finally {
106837
+ await this.cleanupQtiAssessmentCopy(client2, sourceCreateAttempted ? sourceIdentifier : undefined, createdItemIdentifiers);
106838
+ }
106839
+ }
106649
106840
  async removeAssessment(integrationId, qtiTestIdentifier) {
106650
106841
  return this.withLockedAssessmentAssociations(integrationId, qtiTestIdentifier, async (row, tx) => {
106651
106842
  if (qtiTestIdentifier === reviewBankSinkIdentifier(integrationId)) {
@@ -106918,106 +107109,118 @@ class TimebackAssessmentsService {
106918
107109
  assertAllAssessmentAssociationsDraft(associations);
106919
107110
  const ownership = await this.requireOwnedDraftAssessment(integrationId, qtiTestIdentifier, row, tx);
106920
107111
  const creation = parseQtiQuestionCreationInput(input);
106921
- const itemIdentifier = newPlaycademyQuestionIdentifier(qtiTestIdentifier);
106922
- let title;
106923
- let metadata2;
106924
- let xml;
106925
- let structuredInput;
106926
- let fallbackItem;
106927
- if (creation.kind === "copy") {
106928
- const source = await client2.qtiApi.assessmentItems.get(creation.sourceItemIdentifier);
106929
- const copy = buildQtiQuestionCopyInput({
106930
- source,
106931
- targetIdentifier: itemIdentifier,
106932
- targetTestIdentifier: qtiTestIdentifier,
106933
- gameSlug: ownership.gameSlug,
106934
- integrationId
106935
- });
106936
- title = copy.title;
106937
- metadata2 = copy.metadata;
106938
- xml = copy.xml;
106939
- fallbackItem = {
106940
- ...source,
106941
- identifier: itemIdentifier,
106942
- title,
106943
- rawXml: xml,
106944
- metadata: metadata2
106945
- };
106946
- } else {
106947
- structuredInput = creation.input;
106948
- assertSupportedQtiQuestionInteraction(structuredInput);
106949
- title = typeof structuredInput.title === "string" ? structuredInput.title.trim() : "";
106950
- metadata2 = buildOwnedQtiQuestionMetadata(structuredInput, {
106951
- gameSlug: ownership.gameSlug,
106952
- testIdentifier: qtiTestIdentifier
106953
- });
106954
- xml = buildQuestionXml(structuredInput, itemIdentifier, title);
106955
- fallbackItem = {
106956
- ...structuredInput,
106957
- identifier: itemIdentifier,
106958
- title,
106959
- ...xml ? { rawXml: xml } : {}
106960
- };
106961
- }
106962
- const section = await this.qtiSectionItems(client2, qtiTestIdentifier, undefined, undefined, ownership.test);
106963
- const href = qtiItemHref(client2, itemIdentifier);
106964
- let item;
106965
- let itemCreationAttempted = false;
106966
- let referenceCreationAttempted = false;
106967
- try {
106968
- itemCreationAttempted = true;
106969
- item = xml ? await client2.course.createAssessmentItemXml({ xml, metadata: metadata2 }) : await client2.qtiApi.assessmentItems.create({
106970
- ...structuredInput,
106971
- identifier: itemIdentifier,
106972
- metadata: metadata2
106973
- });
106974
- referenceCreationAttempted = true;
106975
- await section.items.add({
106976
- identifier: itemIdentifier,
106977
- href
106978
- });
106979
- } catch (error88) {
106980
- let referenceRemoved = !referenceCreationAttempted;
106981
- const creationStatus = isApiError(error88) ? error88.statusCode : undefined;
106982
- const itemCreationDefinitelyRejected = !referenceCreationAttempted && typeof creationStatus === "number" && creationStatus >= 400 && creationStatus < 500 && creationStatus !== 408;
106983
- if (referenceCreationAttempted) {
106984
- try {
106985
- await section.items.remove(itemIdentifier);
106986
- referenceRemoved = true;
106987
- } catch (cleanupError) {
106988
- referenceRemoved = isApiError(cleanupError) && cleanupError.statusCode === 404;
106989
- addEvent("assessment.qti_question_reference_cleanup_failed", {
106990
- "app.assessment.qti_test_identifier": qtiTestIdentifier,
106991
- "app.assessment.qti_item_identifier": itemIdentifier,
106992
- "exception.type": errorType(cleanupError),
106993
- "app.error.message": errorMessage(cleanupError)
106994
- });
106995
- }
106996
- }
106997
- if (itemCreationAttempted && !itemCreationDefinitelyRejected && referenceRemoved) {
106998
- try {
106999
- await client2.qtiApi.assessmentItems.delete(itemIdentifier);
107000
- } catch (cleanupError) {
107001
- addEvent("assessment.qti_question_cleanup_failed", {
107002
- "app.assessment.qti_test_identifier": qtiTestIdentifier,
107003
- "app.assessment.qti_item_identifier": itemIdentifier,
107004
- "exception.type": errorType(cleanupError),
107005
- "app.error.message": errorMessage(cleanupError)
107006
- });
107007
- }
107008
- }
107009
- throw error88;
107010
- }
107112
+ const created = await this.createQuestionInOwnedTest({
107113
+ client: client2,
107114
+ integrationId,
107115
+ qtiTestIdentifier,
107116
+ test: ownership.test,
107117
+ gameSlug: ownership.gameSlug,
107118
+ creation
107119
+ });
107011
107120
  setAttribute("app.assessment.operation", creation.kind === "copy" ? "copy_question" : "create_question");
107012
- return buildCreatedQtiQuestionReference({
107013
- item,
107014
- fallbackItem,
107015
- itemIdentifier,
107016
- href,
107017
- partIdentifier: section.partIdentifier,
107018
- sectionIdentifier: section.sectionIdentifier,
107121
+ return created;
107122
+ });
107123
+ }
107124
+ async createQuestionInOwnedTest(input) {
107125
+ const { client: client2, integrationId, qtiTestIdentifier, creation } = input;
107126
+ const itemIdentifier = newPlaycademyQuestionIdentifier(qtiTestIdentifier);
107127
+ let title;
107128
+ let metadata2;
107129
+ let xml;
107130
+ let structuredInput;
107131
+ let fallbackItem;
107132
+ if (creation.kind === "copy") {
107133
+ const source = await client2.qtiApi.assessmentItems.get(creation.sourceItemIdentifier);
107134
+ const copy = buildQtiQuestionCopyInput({
107135
+ source,
107136
+ targetIdentifier: itemIdentifier,
107137
+ targetTestIdentifier: qtiTestIdentifier,
107138
+ gameSlug: input.gameSlug,
107139
+ integrationId
107140
+ });
107141
+ title = copy.title;
107142
+ metadata2 = copy.metadata;
107143
+ xml = copy.xml;
107144
+ fallbackItem = {
107145
+ ...source,
107146
+ identifier: itemIdentifier,
107147
+ title,
107148
+ rawXml: xml,
107149
+ metadata: metadata2
107150
+ };
107151
+ } else {
107152
+ structuredInput = creation.input;
107153
+ assertSupportedQtiQuestionInteraction(structuredInput);
107154
+ title = typeof structuredInput.title === "string" ? structuredInput.title.trim() : "";
107155
+ metadata2 = buildOwnedQtiQuestionMetadata(structuredInput, {
107156
+ gameSlug: input.gameSlug,
107157
+ testIdentifier: qtiTestIdentifier
107158
+ });
107159
+ xml = buildQuestionXml(structuredInput, itemIdentifier, title);
107160
+ fallbackItem = {
107161
+ ...structuredInput,
107162
+ identifier: itemIdentifier,
107163
+ title,
107164
+ ...xml ? { rawXml: xml } : {}
107165
+ };
107166
+ }
107167
+ const section = await this.qtiSectionItems(client2, qtiTestIdentifier, undefined, undefined, input.test);
107168
+ const href = qtiItemHref(client2, itemIdentifier);
107169
+ let item;
107170
+ let itemCreationAttempted = false;
107171
+ let referenceCreationAttempted = false;
107172
+ try {
107173
+ itemCreationAttempted = true;
107174
+ item = xml ? await client2.course.createAssessmentItemXml({ xml, metadata: metadata2 }) : await client2.qtiApi.assessmentItems.create({
107175
+ ...structuredInput,
107176
+ identifier: itemIdentifier,
107019
107177
  metadata: metadata2
107020
107178
  });
107179
+ referenceCreationAttempted = true;
107180
+ await section.items.add({
107181
+ identifier: itemIdentifier,
107182
+ href
107183
+ });
107184
+ } catch (error88) {
107185
+ let referenceRemoved = !referenceCreationAttempted;
107186
+ const creationStatus = isApiError(error88) ? error88.statusCode : undefined;
107187
+ const itemCreationDefinitelyRejected = !referenceCreationAttempted && typeof creationStatus === "number" && creationStatus >= 400 && creationStatus < 500 && creationStatus !== 408;
107188
+ if (referenceCreationAttempted) {
107189
+ try {
107190
+ await section.items.remove(itemIdentifier);
107191
+ referenceRemoved = true;
107192
+ } catch (cleanupError) {
107193
+ referenceRemoved = isApiError(cleanupError) && cleanupError.statusCode === 404;
107194
+ addEvent("assessment.qti_question_reference_cleanup_failed", {
107195
+ "app.assessment.qti_test_identifier": qtiTestIdentifier,
107196
+ "app.assessment.qti_item_identifier": itemIdentifier,
107197
+ "exception.type": errorType(cleanupError),
107198
+ "app.error.message": errorMessage(cleanupError)
107199
+ });
107200
+ }
107201
+ }
107202
+ if (itemCreationAttempted && !itemCreationDefinitelyRejected && referenceRemoved) {
107203
+ try {
107204
+ await client2.qtiApi.assessmentItems.delete(itemIdentifier);
107205
+ } catch (cleanupError) {
107206
+ addEvent("assessment.qti_question_cleanup_failed", {
107207
+ "app.assessment.qti_test_identifier": qtiTestIdentifier,
107208
+ "app.assessment.qti_item_identifier": itemIdentifier,
107209
+ "exception.type": errorType(cleanupError),
107210
+ "app.error.message": errorMessage(cleanupError)
107211
+ });
107212
+ }
107213
+ }
107214
+ throw error88;
107215
+ }
107216
+ return buildCreatedQtiQuestionReference({
107217
+ item,
107218
+ fallbackItem,
107219
+ itemIdentifier,
107220
+ href,
107221
+ partIdentifier: section.partIdentifier,
107222
+ sectionIdentifier: section.sectionIdentifier,
107223
+ metadata: metadata2
107021
107224
  });
107022
107225
  }
107023
107226
  async updateQuestion(integrationId, qtiTestIdentifier, itemIdentifier, input) {
@@ -107265,6 +107468,22 @@ class TimebackAssessmentsService {
107265
107468
  }
107266
107469
  return row;
107267
107470
  }
107471
+ async requireCurrentRevisionRow(integrationId, qtiTestIdentifier) {
107472
+ const row = await this.deps.db.query.gameTimebackAssessmentTests.findFirst({
107473
+ where: and(eq(gameTimebackAssessmentTests.integrationId, integrationId), eq(gameTimebackAssessmentTests.qtiTestIdentifier, qtiTestIdentifier))
107474
+ });
107475
+ if (row) {
107476
+ return row;
107477
+ }
107478
+ const assessmentKey = assessmentKeyFromManagedQtiIdentifier(qtiTestIdentifier);
107479
+ const successor = assessmentKey ? await this.deps.db.query.gameTimebackAssessmentTests.findFirst({
107480
+ where: and(eq(gameTimebackAssessmentTests.integrationId, integrationId), eq(gameTimebackAssessmentTests.assessmentKey, assessmentKey))
107481
+ }) : undefined;
107482
+ if (successor) {
107483
+ throw revisionConflict(qtiTestIdentifier, successor.qtiTestIdentifier);
107484
+ }
107485
+ throw new NotFoundError(`Assessment not found: ${qtiTestIdentifier}`);
107486
+ }
107268
107487
  async requireDraftAssessmentRow(integrationId, qtiTestIdentifier, lockedRow) {
107269
107488
  const row = lockedRow ?? await this.requireAssessmentRow(integrationId, qtiTestIdentifier);
107270
107489
  assertDraftAssessment(row);
@@ -107412,9 +107631,21 @@ class TimebackAssessmentsService {
107412
107631
  itemHref: (identifier) => qtiItemHref(client2, identifier)
107413
107632
  });
107414
107633
  const artifactSource = await this.ensureManagedQtiPublication(client2, plan);
107415
- const artifactsAvailable = await this.storeManagedArtifacts(artifactSource, managedAssessmentDiagnosticRoutingManifest(artifactSource.test, plan.assessmentKey));
107634
+ const artifactsAvailable = await this.managedArtifactsStored(plan.qtiTestIdentifier) || await this.storeManagedArtifacts(artifactSource, managedAssessmentDiagnosticRoutingManifest(artifactSource.test, plan.assessmentKey));
107416
107635
  const warning = assessmentArtifactPublicationWarning(artifactsAvailable);
107417
- return { ...plan, ...warning ? { warning } : {} };
107636
+ return { ...plan, artifactsAvailable, ...warning ? { warning } : {} };
107637
+ }
107638
+ async managedArtifactsStored(qtiTestIdentifier) {
107639
+ try {
107640
+ return await this.deps.assessmentArtifacts.loadManagedPlayable(qtiTestIdentifier) !== null;
107641
+ } catch (error88) {
107642
+ addEvent("assessment.artifact_lookup_failed", {
107643
+ "app.assessment.qti_test_identifier": qtiTestIdentifier,
107644
+ "exception.type": errorType(error88),
107645
+ "app.error.message": errorMessage(error88)
107646
+ });
107647
+ return false;
107648
+ }
107418
107649
  }
107419
107650
  async cleanupQtiAssessmentCopy(client2, testIdentifier, itemIdentifiers) {
107420
107651
  let testDeleted = !testIdentifier;
@@ -107541,6 +107772,7 @@ var init_timeback_assessments_service = __esm(async () => {
107541
107772
  init_timeback_assessment_diagnostic_util();
107542
107773
  init_timeback_assessment_import_util();
107543
107774
  init_timeback_assessment_publication_util();
107775
+ init_timeback_assessment_revision_util();
107544
107776
  init_timeback_assessment_rules_util();
107545
107777
  init_timeback_qti_authoring_util();
107546
107778
  init_timeback_qti_hydration_util();
@@ -167550,7 +167782,7 @@ function parseQtiLibraryParams(searchParams) {
167550
167782
  limit
167551
167783
  };
167552
167784
  }
167553
- var populateStudent, getUser, getUserEnrollments, getUserById, setupIntegration, getIntegrations, getRemovedIntegrations, createIntegration, updateIntegration, deactivateCourse, reactivateCourse, getIntegrationConfig, verifyIntegration, getConfig, deleteIntegrations, endActivity, heartbeat, advanceCourse, unenrollCourse, prepareRuntimeAssessment, startRuntimeAssessment, getRuntimeAssessment, getLatestRuntimeAssessment, resumeRuntimeAssessment, saveRuntimeAssessment, recordRuntimeAssessmentProgress, submitRuntimeAssessmentItem, submitRuntimeAssessment, finalizeRuntimeAssessment, exportRuntimeAssessmentFixture, getStudentXp, getStudentMastery, getStudentHighestGradeMastered, getRoster, getStudentOverview, getGameMetrics, getStudentActivity, getGradeLevelTestResults, getGradeLevelTestReview, getActivityDetail, listMetricDiscrepancies, verifyMetricDiscrepancy, grantXp, adjustTime, adjustMastery, reconcileMasteryForConfigChange, searchStudents, enrollStudent, unenrollStudent, reactivateEnrollment, listAssessments, createAssessment, attachExistingAssessments, updateAssessment, synchronizeReviewBank, reorderQuestions, removeAssessment, listQuestions, listQuestionLibrary, listTestLibrary, copyAssessment, createQuestion, updateQuestion, removeQuestion, timeback2;
167785
+ var populateStudent, getUser, getUserEnrollments, getUserById, setupIntegration, getIntegrations, getRemovedIntegrations, createIntegration, updateIntegration, deactivateCourse, reactivateCourse, getIntegrationConfig, verifyIntegration, getConfig, deleteIntegrations, endActivity, heartbeat, advanceCourse, unenrollCourse, prepareRuntimeAssessment, startRuntimeAssessment, getRuntimeAssessment, getLatestRuntimeAssessment, resumeRuntimeAssessment, saveRuntimeAssessment, recordRuntimeAssessmentProgress, submitRuntimeAssessmentItem, submitRuntimeAssessment, finalizeRuntimeAssessment, exportRuntimeAssessmentFixture, getStudentXp, getStudentMastery, getStudentHighestGradeMastered, getRoster, getStudentOverview, getGameMetrics, getStudentActivity, getGradeLevelTestResults, getGradeLevelTestReview, getActivityDetail, listMetricDiscrepancies, verifyMetricDiscrepancy, grantXp, adjustTime, adjustMastery, reconcileMasteryForConfigChange, searchStudents, enrollStudent, unenrollStudent, reactivateEnrollment, listAssessments, createAssessment, attachExistingAssessments, updateAssessment, reviseAssessment, synchronizeReviewBank, reorderQuestions, removeAssessment, listQuestions, listQuestionLibrary, listTestLibrary, copyAssessment, createQuestion, updateQuestion, removeQuestion, timeback2;
167554
167786
  var init_timeback_controller = __esm(() => {
167555
167787
  init_esm();
167556
167788
  init_src();
@@ -168295,6 +168527,20 @@ var init_timeback_controller = __esm(() => {
168295
168527
  const body2 = await parseRequestBody(ctx.request, UpdateAssessmentRequestSchema);
168296
168528
  return ctx.services.timebackAssessments.updateAssessment(integrationId, testIdentifier, body2);
168297
168529
  });
168530
+ reviseAssessment = requireDeveloper(async (ctx) => {
168531
+ const { gameId, courseId } = ctx.params;
168532
+ if (!gameId || !courseId) {
168533
+ throw ApiError.badRequest("Missing gameId or courseId parameter");
168534
+ }
168535
+ const body2 = await parseRequestBody(ctx.request, ReviseAssessmentRequestSchema);
168536
+ const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
168537
+ return ctx.services.timebackAssessments.reviseAssessment(integrationId, {
168538
+ currentQtiTestIdentifier: body2.currentQtiTestIdentifier,
168539
+ sourceTestIdentifier: newPlaycademyTestIdentifier(),
168540
+ title: body2.title,
168541
+ questions: body2.questions
168542
+ });
168543
+ });
168298
168544
  synchronizeReviewBank = requireDeveloper(async (ctx) => {
168299
168545
  const { gameId, courseId } = ctx.params;
168300
168546
  if (!gameId || !courseId) {
@@ -168445,6 +168691,7 @@ var init_timeback_controller = __esm(() => {
168445
168691
  createAssessment,
168446
168692
  attachExistingAssessments,
168447
168693
  updateAssessment,
168694
+ reviseAssessment,
168448
168695
  synchronizeReviewBank,
168449
168696
  removeAssessment,
168450
168697
  reorderQuestions,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcademy/sandbox",
3
- "version": "0.10.1-beta.4",
3
+ "version": "0.10.1-beta.6",
4
4
  "description": "Local development server for Playcademy game development",
5
5
  "type": "module",
6
6
  "exports": {