@playcademy/sandbox 0.10.1-beta.3 → 0.10.1-beta.5

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/dist/server.js CHANGED
@@ -284,7 +284,7 @@ var init_platform = __esm(() => {
284
284
  var PLATFORM_TIMEZONE = "America/New_York";
285
285
 
286
286
  // ../constants/src/timeback.ts
287
- var TIMEBACK_GRADES, TIMEBACK_SUBJECTS, ASSESSMENT_PURPOSES, PLAYCADEMY_ASSESSMENT_RESULT_METADATA_VERSION = 1, PLAYCADEMY_ASSESSMENT_CONTRACT_VERSION = 1, PLAYCADEMY_ASSESSMENT_RESULT_METADATA_KEY = "playcademyAssessment", PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY = "playcademyAssessmentItem", TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS, TIMEBACK_ORG_SOURCED_ID = "PLAYCADEMY", TIMEBACK_ORG_NAME = "Playcademy Studios", TIMEBACK_ORG_TYPE = "department", TIMEBACK_COURSE_DEFAULTS, TIMEBACK_RESOURCE_DEFAULTS, TIMEBACK_COMPONENT_DEFAULTS, TIMEBACK_COMPONENT_RESOURCE_DEFAULTS, TIMEBACK_ASSESSMENT_RESPONSE_VALUE_MAX_LENGTH = 2048, TIMEBACK_GAME_METRIC_DECIMAL_PLACES, TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE;
287
+ var TIMEBACK_GRADES, TIMEBACK_SUBJECTS, ASSESSMENT_PURPOSES, PLAYCADEMY_ASSESSMENT_RESULT_METADATA_VERSION = 1, PLAYCADEMY_ASSESSMENT_CONTRACT_VERSION = 2, PLAYCADEMY_ASSESSMENT_RESULT_METADATA_KEY = "playcademyAssessment", PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY = "playcademyAssessmentItem", TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS, TIMEBACK_ORG_SOURCED_ID = "PLAYCADEMY", TIMEBACK_ORG_NAME = "Playcademy Studios", TIMEBACK_ORG_TYPE = "department", TIMEBACK_COURSE_DEFAULTS, TIMEBACK_RESOURCE_DEFAULTS, TIMEBACK_COMPONENT_DEFAULTS, TIMEBACK_COMPONENT_RESOURCE_DEFAULTS, TIMEBACK_ASSESSMENT_RESPONSE_VALUE_MAX_LENGTH = 2048, TIMEBACK_GAME_METRIC_DECIMAL_PLACES, TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE;
288
288
  var init_timeback2 = __esm(() => {
289
289
  TIMEBACK_GRADES = [-1, 0, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11, 12, 13];
290
290
  TIMEBACK_SUBJECTS = [
@@ -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.3",
1130
+ version: "0.10.1-beta.5",
1131
1131
  description: "Local development server for Playcademy game development",
1132
1132
  type: "module",
1133
1133
  exports: {
@@ -10833,30 +10833,60 @@ function comparePresentationText(left, right) {
10833
10833
  }
10834
10834
  return 0;
10835
10835
  }
10836
+ function presentationRandomIndex(seed) {
10837
+ let state = presentationHash(seed);
10838
+ const range = 2 ** 32;
10839
+ return (bound) => {
10840
+ const limit = range - range % bound;
10841
+ let value;
10842
+ do {
10843
+ state = state + 1831565813 >>> 0;
10844
+ let mixed = Math.imul(state ^ state >>> 15, state | 1);
10845
+ mixed ^= mixed + Math.imul(mixed ^ mixed >>> 7, mixed | 61);
10846
+ value = (mixed ^ mixed >>> 14) >>> 0;
10847
+ } while (value >= limit);
10848
+ return value % bound;
10849
+ };
10850
+ }
10836
10851
  function presentationChoices(choices, attemptId, itemIdentifier, domain) {
10837
- const seed = `${attemptId}\x00${itemIdentifier}\x00${domain}\x00`;
10838
- return [...choices].toSorted((left, right) => {
10839
- const hashDifference = presentationHash(`${seed}${left.identifier}`) - presentationHash(`${seed}${right.identifier}`);
10840
- return hashDifference || comparePresentationText(left.identifier, right.identifier);
10841
- });
10852
+ const randomIndex = presentationRandomIndex(JSON.stringify([attemptId, itemIdentifier, domain]));
10853
+ const movable = choices.filter((choice) => !choice.fixed).toSorted((left, right) => comparePresentationText(left.identifier, right.identifier));
10854
+ for (let index = movable.length - 1;index > 0; index--) {
10855
+ const other = randomIndex(index + 1);
10856
+ [movable[index], movable[other]] = [movable[other], movable[index]];
10857
+ }
10858
+ let next = 0;
10859
+ return choices.map((choice) => choice.fixed ? choice : movable[next++]);
10842
10860
  }
10843
10861
  function assessmentPresentationForAttempt(assessment, attemptId) {
10844
10862
  const items = assessment.items.map((item) => {
10845
10863
  const interactions = item.interactions.map((interaction) => {
10846
- if (interaction.type === "order" && interaction.shuffle) {
10847
- return {
10848
- ...interaction,
10849
- choices: presentationChoices(interaction.choices, attemptId, item.identifier, `order\x00${interaction.responseIdentifier}`)
10850
- };
10864
+ if (!("shuffle" in interaction) || !interaction.shuffle) {
10865
+ return interaction;
10851
10866
  }
10852
- if (interaction.type === "match" && interaction.shuffle) {
10853
- return {
10854
- ...interaction,
10855
- sourceChoices: presentationChoices(interaction.sourceChoices, attemptId, item.identifier, `match-source\x00${interaction.responseIdentifier}`),
10856
- targetChoices: presentationChoices(interaction.targetChoices, attemptId, item.identifier, `match-target\x00${interaction.responseIdentifier}`)
10857
- };
10867
+ function shuffled(choices, domain) {
10868
+ return presentationChoices(choices, attemptId, item.identifier, `${domain}\x00${interaction.responseIdentifier}`);
10869
+ }
10870
+ switch (interaction.type) {
10871
+ case "choice":
10872
+ case "inline-choice":
10873
+ case "order": {
10874
+ return {
10875
+ ...interaction,
10876
+ choices: shuffled(interaction.choices, interaction.type)
10877
+ };
10878
+ }
10879
+ case "gap-match": {
10880
+ return { ...interaction, gapTexts: shuffled(interaction.gapTexts, "gap-match") };
10881
+ }
10882
+ case "match": {
10883
+ return {
10884
+ ...interaction,
10885
+ sourceChoices: shuffled(interaction.sourceChoices, "match-source"),
10886
+ targetChoices: shuffled(interaction.targetChoices, "match-target")
10887
+ };
10888
+ }
10858
10889
  }
10859
- return interaction;
10860
10890
  });
10861
10891
  return { ...item, interactions };
10862
10892
  });
@@ -11811,6 +11841,7 @@ var init_assessment_runtime2 = __esm(() => {
11811
11841
  init_src();
11812
11842
  init_timeback3();
11813
11843
  init_src();
11844
+ init_src();
11814
11845
  init_uuid();
11815
11846
  init_esm();
11816
11847
  init_src();
@@ -39208,7 +39239,7 @@ function assertMatchingAssessmentArtifactPair(playable, scoring, expectedIdentit
39208
39239
  throw new ValidationError("Assessment artifact pair does not describe one revision");
39209
39240
  }
39210
39241
  }
39211
- var ASSESSMENT_ARTIFACT_SCHEMA_VERSION = 1, ASSESSMENT_GRADER_PROTOCOL_VERSION = 1, ARTIFACT_KEY_PREFIX = "assessment-artifacts", SHA256_HEX, textEncoder;
39242
+ var ASSESSMENT_ARTIFACT_SCHEMA_VERSION = 2, ASSESSMENT_GRADER_PROTOCOL_VERSION = 1, ARTIFACT_KEY_PREFIX = "assessment-artifacts", SHA256_HEX, textEncoder;
39212
39243
  var init_assessment_artifact_envelope_util = __esm(() => {
39213
39244
  init_errors2();
39214
39245
  SHA256_HEX = /^[a-f0-9]{64}$/;
@@ -47957,7 +47988,7 @@ function rejectSystemManagedReview(input, context2) {
47957
47988
  });
47958
47989
  }
47959
47990
  }
47960
- 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;
47961
47992
  var init_schemas4 = __esm(() => {
47962
47993
  init_esm();
47963
47994
  init_src();
@@ -48240,6 +48271,11 @@ var init_schemas4 = __esm(() => {
48240
48271
  requireMasteryStandard(input, context2);
48241
48272
  rejectSystemManagedReview(input, context2);
48242
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
+ });
48243
48279
  AssessmentAssociationImportEntrySchema = exports_external.object({
48244
48280
  assessmentKey: AssessmentKeySchema,
48245
48281
  qtiTestIdentifier: exports_external.string().trim().min(1, "Assessment identifier is required"),
@@ -94302,6 +94338,30 @@ var init_timeback_game_metrics_comparison_util = __esm(() => {
94302
94338
  });
94303
94339
 
94304
94340
  // ../timeback/dist/engines/qti/index.js
94341
+ function qtiAttributeNumber(attributes, name3, fallback) {
94342
+ const raw = attributes[name3]?.trim();
94343
+ if (!raw) {
94344
+ return fallback;
94345
+ }
94346
+ const value = Number(raw);
94347
+ return Number.isFinite(value) ? value : fallback;
94348
+ }
94349
+ function qtiAttributeCapacity(attributes, name3, fallback) {
94350
+ const raw = attributes[name3]?.trim();
94351
+ if (!raw) {
94352
+ return fallback;
94353
+ }
94354
+ const value = Number(raw);
94355
+ return Number.isInteger(value) && value >= 0 ? value : null;
94356
+ }
94357
+ function qtiAttributeMaximum(attributes, name3, fallback, unlimited) {
94358
+ const maximum = qtiAttributeNumber(attributes, name3, fallback);
94359
+ return maximum === 0 ? unlimited : maximum;
94360
+ }
94361
+ function qtiAttributeBoolean(attributes, name3) {
94362
+ const value = attributes[name3];
94363
+ return value === "true" || value === "1";
94364
+ }
94305
94365
  function decodeCssEscapes(value) {
94306
94366
  return value.replace(/\\([0-9a-fA-F]{1,6})\s?/g, (_, hex3) => {
94307
94367
  const code = Number.parseInt(hex3, 16);
@@ -94889,7 +94949,8 @@ function parseGapTexts(interaction) {
94889
94949
  return {
94890
94950
  identifier: attributes2.identifier || "",
94891
94951
  content: plainTextFromNodes(gapText.children),
94892
- matchMax: finiteNumber(attributes2["match-max"]) ?? 1
94952
+ matchMax: finiteNumber(attributes2["match-max"]) ?? 1,
94953
+ ...qtiAttributeBoolean(attributes2, "fixed") ? { fixed: true } : {}
94893
94954
  };
94894
94955
  }).filter((gapText) => gapText.identifier);
94895
94956
  }
@@ -95989,30 +96050,6 @@ ${body2}
95989
96050
  ${processing}` : ""}
95990
96051
  </qti-assessment-item>`;
95991
96052
  }
95992
- function qtiAttributeNumber(attributes2, name3, fallback) {
95993
- const raw = attributes2[name3]?.trim();
95994
- if (!raw) {
95995
- return fallback;
95996
- }
95997
- const value = Number(raw);
95998
- return Number.isFinite(value) ? value : fallback;
95999
- }
96000
- function qtiAttributeCapacity(attributes2, name3, fallback) {
96001
- const raw = attributes2[name3]?.trim();
96002
- if (!raw) {
96003
- return fallback;
96004
- }
96005
- const value = Number(raw);
96006
- return Number.isInteger(value) && value >= 0 ? value : null;
96007
- }
96008
- function qtiAttributeMaximum(attributes2, name3, fallback, unlimited) {
96009
- const maximum = qtiAttributeNumber(attributes2, name3, fallback);
96010
- return maximum === 0 ? unlimited : maximum;
96011
- }
96012
- function qtiAttributeBoolean(attributes2, name3) {
96013
- const value = attributes2[name3];
96014
- return value === "true" || value === "1";
96015
- }
96016
96053
  function gapContentIdentifiers(nodes) {
96017
96054
  return nodes.flatMap((node) => {
96018
96055
  if (node.kind === "gap") {
@@ -96093,6 +96130,7 @@ function authorableQtiInteractionProjection(interaction) {
96093
96130
  type: interaction.type,
96094
96131
  choices: interaction.choices,
96095
96132
  minChoices: qtiAttributeNumber(attributes2, "min-choices", 1),
96133
+ shuffle: qtiAttributeBoolean(attributes2, "shuffle"),
96096
96134
  maxChoices: qtiAttributeMaximum(attributes2, "max-choices", 1, interaction.choices.length)
96097
96135
  };
96098
96136
  }
@@ -96101,6 +96139,7 @@ function authorableQtiInteractionProjection(interaction) {
96101
96139
  ...base,
96102
96140
  type: interaction.type,
96103
96141
  choices: interaction.choices,
96142
+ shuffle: qtiAttributeBoolean(attributes2, "shuffle"),
96104
96143
  blankIndex: interaction.blankIndex ?? 0
96105
96144
  } : null;
96106
96145
  }
@@ -99932,11 +99971,13 @@ var init_assessment_artifact_runtime_util = __esm(() => {
99932
99971
  playableContentNodesSchema = exports_external.array(exports_external.unknown());
99933
99972
  playableChoiceSchema = exports_external.object({
99934
99973
  identifier: trimmedNonEmptyString3,
99974
+ fixed: exports_external.boolean().optional(),
99935
99975
  content: exports_external.string(),
99936
99976
  contentNodes: playableContentNodesSchema.optional()
99937
99977
  }).strict();
99938
99978
  playableMatchChoiceSchema = exports_external.object({
99939
99979
  identifier: trimmedNonEmptyString3,
99980
+ fixed: exports_external.boolean().optional(),
99940
99981
  content: exports_external.string(),
99941
99982
  contentNodes: playableContentNodesSchema.optional(),
99942
99983
  matchMax: nonnegativeInteger
@@ -99956,6 +99997,7 @@ var init_assessment_artifact_runtime_util = __esm(() => {
99956
99997
  }).strict();
99957
99998
  playableGapTextSchema = exports_external.object({
99958
99999
  identifier: trimmedNonEmptyString3,
100000
+ fixed: exports_external.boolean().optional(),
99959
100001
  content: exports_external.string(),
99960
100002
  matchMax: nonnegativeInteger
99961
100003
  }).strict();
@@ -99973,6 +100015,7 @@ var init_assessment_artifact_runtime_util = __esm(() => {
99973
100015
  exports_external.object({
99974
100016
  ...playableInteractionBase,
99975
100017
  type: exports_external.literal("choice"),
100018
+ shuffle: exports_external.boolean(),
99976
100019
  choices: exports_external.array(playableChoiceSchema),
99977
100020
  minChoices: nonnegativeInteger,
99978
100021
  maxChoices: nonnegativeInteger
@@ -99980,6 +100023,7 @@ var init_assessment_artifact_runtime_util = __esm(() => {
99980
100023
  exports_external.object({
99981
100024
  ...playableInteractionBase,
99982
100025
  type: exports_external.literal("inline-choice"),
100026
+ shuffle: exports_external.boolean(),
99983
100027
  choices: exports_external.array(playableChoiceSchema)
99984
100028
  }).strict(),
99985
100029
  exports_external.object({
@@ -100023,6 +100067,7 @@ var init_assessment_artifact_runtime_util = __esm(() => {
100023
100067
  exports_external.object({
100024
100068
  ...playableInteractionBase,
100025
100069
  type: exports_external.literal("gap-match"),
100070
+ shuffle: exports_external.boolean(),
100026
100071
  content: exports_external.string(),
100027
100072
  contentNodes: playableContentNodesSchema,
100028
100073
  gapTexts: exports_external.array(playableGapTextSchema),
@@ -100535,6 +100580,7 @@ function playableChoice(choice) {
100535
100580
  return {
100536
100581
  identifier: choice.identifier,
100537
100582
  content: choice.content,
100583
+ ...qtiAttributeBoolean(choice.attributes, "fixed") ? { fixed: true } : {},
100538
100584
  ...choice.contentNodes ? { contentNodes: choice.contentNodes } : {}
100539
100585
  };
100540
100586
  }
@@ -100597,6 +100643,7 @@ function parseInteraction2(questionIdentifier, interaction) {
100597
100643
  responseIdentifier,
100598
100644
  ...prompt,
100599
100645
  choices,
100646
+ shuffle: qtiAttributeBoolean(interaction.attributes, "shuffle"),
100600
100647
  minChoices: qtiAttributeNumber(interaction.attributes, "min-choices", 1),
100601
100648
  maxChoices: qtiAttributeMaximum(interaction.attributes, "max-choices", 1, choices.length)
100602
100649
  };
@@ -100606,7 +100653,8 @@ function parseInteraction2(questionIdentifier, interaction) {
100606
100653
  type,
100607
100654
  responseIdentifier,
100608
100655
  ...prompt,
100609
- choices: requireChoices2(interaction.choices.map(playableChoice))
100656
+ choices: requireChoices2(interaction.choices.map(playableChoice)),
100657
+ shuffle: qtiAttributeBoolean(interaction.attributes, "shuffle")
100610
100658
  };
100611
100659
  }
100612
100660
  if (type === "text-entry") {
@@ -100667,11 +100715,13 @@ function parseInteraction2(questionIdentifier, interaction) {
100667
100715
  responseIdentifier,
100668
100716
  ...prompt,
100669
100717
  content: interaction.textWithGaps,
100718
+ shuffle: qtiAttributeBoolean(interaction.attributes, "shuffle"),
100670
100719
  contentNodes: interaction.gapContent,
100671
100720
  gapTexts: interaction.gapTexts.map((gapText) => ({
100672
100721
  identifier: gapText.identifier,
100673
100722
  content: gapText.content,
100674
- matchMax: gapText.matchMax
100723
+ matchMax: gapText.matchMax,
100724
+ ...gapText.fixed ? { fixed: true } : {}
100675
100725
  })),
100676
100726
  gaps: [...interaction.gaps],
100677
100727
  maxAssociations: qtiAttributeNumber(interaction.attributes, "max-associations", 0)
@@ -101490,7 +101540,7 @@ function buildInlineChoiceQuestionXml(input, identifier, title) {
101490
101540
  if (!envelope) {
101491
101541
  return null;
101492
101542
  }
101493
- const { structure, responseIdentifier, prompt } = envelope;
101543
+ const { interaction, structure, responseIdentifier, prompt } = envelope;
101494
101544
  const promptParts = prompt.split(INLINE_CHOICE_BLANK);
101495
101545
  if (promptParts.length !== 2) {
101496
101546
  throw new ValidationError("Inline-choice questions require exactly one blank");
@@ -101504,7 +101554,7 @@ function buildInlineChoiceQuestionXml(input, identifier, title) {
101504
101554
  const choicesXml = choices.map((choice) => ` <qti-inline-choice identifier="${escapeXml(choice.identifier)}">${escapeXml(choice.content)}</qti-inline-choice>`).join(`
101505
101555
  `);
101506
101556
  const [before = "", after = ""] = promptParts;
101507
- const itemBody = ` <p>${escapeXml(before)}<qti-inline-choice-interaction response-identifier="${escapeXml(responseIdentifier)}">
101557
+ const itemBody = ` <p>${escapeXml(before)}<qti-inline-choice-interaction response-identifier="${escapeXml(responseIdentifier)}" shuffle="${interaction.shuffle === true}">
101508
101558
  ${choicesXml}
101509
101559
  </qti-inline-choice-interaction>${escapeXml(after)}</p>`;
101510
101560
  return buildExactMatchQuestionXml({
@@ -102119,7 +102169,7 @@ async function prepareReviewMappingUpdate(test, questions) {
102119
102169
  const contentRevision = await assessmentContentRevision(test, questions);
102120
102170
  const assessment = {
102121
102171
  identifier: test.identifier,
102122
- contractVersion: 1,
102172
+ contractVersion: PLAYCADEMY_ASSESSMENT_CONTRACT_VERSION,
102123
102173
  contentRevision,
102124
102174
  title: test.title,
102125
102175
  items: questions.questions.map(({ question }) => ({
@@ -102158,6 +102208,7 @@ async function prepareReviewMappingUpdate(test, questions) {
102158
102208
  var PLAYCADEMY_REVIEW_BANK_TITLE = "Review Bank", PLAYCADEMY_REVIEW_BANK_SINK_METADATA_KEY = "playcademyReviewBankSink", PLAYCADEMY_REVIEW_BANK_SINK_VERSION = 1, StoredSinkWarningSchema, ReviewBankSinkMarkerSchema;
102159
102209
  var init_timeback_review_mapping_util = __esm(() => {
102160
102210
  init_esm();
102211
+ init_src();
102161
102212
  init_assessment_runtime2();
102162
102213
  init_qti();
102163
102214
  init_timeback3();
@@ -106175,6 +106226,69 @@ var init_timeback_assessment_import_util = __esm(() => {
106175
106226
  init_timeback_qti_authoring_util();
106176
106227
  });
106177
106228
 
106229
+ // ../api-core/src/utils/timeback-assessment-revision.util.ts
106230
+ function assertRevisableAssessment(row, reviewBankIdentifier) {
106231
+ if (row.purpose === "review" || row.qtiTestIdentifier === reviewBankIdentifier) {
106232
+ throw new ValidationError("The review bank is system-managed. Revise its mastery sources instead.");
106233
+ }
106234
+ if (row.purpose === "diagnostic") {
106235
+ throw new ValidationError("Diagnostics are revised by importing a new revision and archiving the current one.");
106236
+ }
106237
+ if (row.status !== "live") {
106238
+ 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.");
106239
+ }
106240
+ if (!row.assessmentKey) {
106241
+ throw new ValidationError("This unmanaged assessment cannot be revised. Republish it with an explicit assessment key.");
106242
+ }
106243
+ }
106244
+ function assertRevisableTestStructure(current) {
106245
+ const parts2 = qtiTestParts(current);
106246
+ const sectionCount = parts2.reduce((count, part) => count + part["qti-assessment-section"].length, 0);
106247
+ if (parts2.length !== 1 || sectionCount !== 1) {
106248
+ 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.`);
106249
+ }
106250
+ }
106251
+ function buildRevisionSourceTestInput(input) {
106252
+ assertRevisableTestStructure(input.current);
106253
+ const { "qti-outcome-declaration": outcomeDeclarations, ...structure } = buildQtiTestStructureInput(input.current, input.sourceIdentifier);
106254
+ return {
106255
+ identifier: input.sourceIdentifier,
106256
+ title: input.title,
106257
+ ...qtiTestOptionalAttributes(input.current),
106258
+ metadata: {
106259
+ ...sourceMetadata(input.current.metadata),
106260
+ ownerSystem: PLAYCADEMY_QTI_OWNER_SYSTEM,
106261
+ ownerGameSlug: input.gameSlug,
106262
+ assessmentKey: input.assessmentKey,
106263
+ integrationId: input.integrationId,
106264
+ subject: input.subject,
106265
+ grade: String(input.grade),
106266
+ [PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY]: emptyReviewBankManifest(input.sourceIdentifier)
106267
+ },
106268
+ ...outcomeDeclarations && outcomeDeclarations.length > 0 ? { "qti-outcome-declaration": outcomeDeclarations } : {},
106269
+ "qti-test-part": structure["qti-test-part"].map((part) => ({
106270
+ ...part,
106271
+ "qti-assessment-section": part["qti-assessment-section"].map((section) => ({
106272
+ ...section,
106273
+ "qti-assessment-item-ref": []
106274
+ }))
106275
+ }))
106276
+ };
106277
+ }
106278
+ function revisionConflict(requestedIdentifier, currentIdentifier) {
106279
+ return new ConflictError("This assessment was revised by someone else. Reload to see the current version before revising again.", {
106280
+ requestedQtiTestIdentifier: requestedIdentifier,
106281
+ currentQtiTestIdentifier: currentIdentifier
106282
+ });
106283
+ }
106284
+ var init_timeback_assessment_revision_util = __esm(() => {
106285
+ init_assessment_runtime2();
106286
+ init_timeback3();
106287
+ init_errors2();
106288
+ init_timeback_assessment_publication_util();
106289
+ init_timeback_qti_authoring_util();
106290
+ });
106291
+
106178
106292
  // ../api-core/src/utils/timeback-qti-library.util.ts
106179
106293
  function newPlaycademyTestIdentifier() {
106180
106294
  return `${PLAYCADEMY_QTI_SOURCE_PREFIX}${crypto.randomUUID()}`;
@@ -106600,6 +106714,109 @@ class TimebackAssessmentsService {
106600
106714
  throw error88;
106601
106715
  }
106602
106716
  }
106717
+ async reviseAssessment(integrationId, input) {
106718
+ const client2 = this.requireClient();
106719
+ const reviewBankIdentifier = reviewBankSinkIdentifier(integrationId);
106720
+ const current = await this.requireCurrentRevisionRow(integrationId, input.currentQtiTestIdentifier);
106721
+ assertRevisableAssessment(current, reviewBankIdentifier);
106722
+ const ownership = await this.requireQtiTestOwnershipContext(integrationId);
106723
+ const { integration } = ownership;
106724
+ const creations = input.questions.map((question) => {
106725
+ const creation = parseQtiQuestionCreationInput(question);
106726
+ if (creation.kind === "authoring") {
106727
+ assertSupportedQtiQuestionInteraction(creation.input);
106728
+ }
106729
+ return creation;
106730
+ });
106731
+ assertAssessmentHasQuestions(creations);
106732
+ const currentTest = await client2.qtiApi.assessmentTests.get(current.qtiTestIdentifier);
106733
+ const sourceIdentifier = input.sourceTestIdentifier;
106734
+ const sourceInput = buildRevisionSourceTestInput({
106735
+ sourceIdentifier,
106736
+ title: input.title,
106737
+ current: currentTest,
106738
+ assessmentKey: current.assessmentKey,
106739
+ integrationId,
106740
+ gameSlug: ownership.gameSlug,
106741
+ subject: integration.subject,
106742
+ grade: integration.grade
106743
+ });
106744
+ const createdItemIdentifiers = [];
106745
+ let sourceCreateAttempted = false;
106746
+ try {
106747
+ sourceCreateAttempted = true;
106748
+ await client2.qtiApi.assessmentTests.create(sourceInput);
106749
+ const sourceTest = await client2.qtiApi.assessmentTests.get(sourceIdentifier);
106750
+ for (const creation of creations) {
106751
+ const created = await this.createQuestionInOwnedTest({
106752
+ client: client2,
106753
+ integrationId,
106754
+ qtiTestIdentifier: sourceIdentifier,
106755
+ test: sourceTest,
106756
+ gameSlug: ownership.gameSlug,
106757
+ creation
106758
+ });
106759
+ createdItemIdentifiers.push(created.reference.identifier);
106760
+ }
106761
+ const loaded = await loadHydratedQtiTest(client2, sourceIdentifier);
106762
+ await this.validateAssessmentHasQuestions(loaded);
106763
+ const publication = await this.publishManagedAssessment(current, current.purpose, null, loaded);
106764
+ if (!publication.artifactsAvailable) {
106765
+ throw new ValidationError("The revised assessment could not be compiled for the runtime. The current version stays live; try again.");
106766
+ }
106767
+ const result = await this.deps.db.transaction(async (tx) => {
106768
+ const rows = await tx.select().from(gameTimebackAssessmentTests).where(and(eq(gameTimebackAssessmentTests.integrationId, integrationId), eq(gameTimebackAssessmentTests.assessmentKey, current.assessmentKey))).for("update");
106769
+ const row = rows[0];
106770
+ if (!row) {
106771
+ throw new NotFoundError(`Assessment not found: ${input.currentQtiTestIdentifier}`);
106772
+ }
106773
+ if (row.qtiTestIdentifier !== input.currentQtiTestIdentifier) {
106774
+ throw revisionConflict(input.currentQtiTestIdentifier, row.qtiTestIdentifier);
106775
+ }
106776
+ assertRevisableAssessment(row, reviewBankIdentifier);
106777
+ const previousQtiTestIdentifier = row.qtiTestIdentifier;
106778
+ const unchanged = publication.qtiTestIdentifier === previousQtiTestIdentifier;
106779
+ if (unchanged) {
106780
+ setAttribute("app.assessment.operation", "revise_unchanged");
106781
+ return { row, previousQtiTestIdentifier, unchanged };
106782
+ }
106783
+ const [updatedRow] = await tx.update(gameTimebackAssessmentTests).set({
106784
+ qtiTestIdentifier: publication.qtiTestIdentifier,
106785
+ updatedAt: new Date
106786
+ }).where(eq(gameTimebackAssessmentTests.id, row.id)).returning();
106787
+ setAttribute("app.assessment.operation", "revise");
106788
+ addEvent("assessment.revised", {
106789
+ "app.assessment.assessment_key": row.assessmentKey,
106790
+ "app.assessment.previous_qti_test_identifier": previousQtiTestIdentifier,
106791
+ "app.assessment.qti_test_identifier": publication.qtiTestIdentifier
106792
+ });
106793
+ return { row: updatedRow ?? row, previousQtiTestIdentifier, unchanged };
106794
+ });
106795
+ let warning;
106796
+ if (!result.unchanged && result.row.purpose === "mastery") {
106797
+ try {
106798
+ const sync = await this.synchronizeReviewBank(integrationId);
106799
+ if (sync.warnings.length > 0) {
106800
+ warning = `Review bank: ${sync.warnings.map((entry) => entry.message).join(" ")}`;
106801
+ }
106802
+ } catch (error88) {
106803
+ addEvent("assessment.revision_review_bank_sync_failed", {
106804
+ "app.assessment.qti_test_identifier": result.row.qtiTestIdentifier,
106805
+ "exception.type": errorType(error88),
106806
+ "app.error.message": errorMessage(error88)
106807
+ });
106808
+ warning = `The review bank could not be synchronized: ${errorMessage(error88)}. Use Sync Review Bank to retry.`;
106809
+ }
106810
+ }
106811
+ return {
106812
+ ...this.associationSummary(result.row, warning),
106813
+ previousQtiTestIdentifier: result.previousQtiTestIdentifier,
106814
+ unchanged: result.unchanged
106815
+ };
106816
+ } finally {
106817
+ await this.cleanupQtiAssessmentCopy(client2, sourceCreateAttempted ? sourceIdentifier : undefined, createdItemIdentifiers);
106818
+ }
106819
+ }
106603
106820
  async removeAssessment(integrationId, qtiTestIdentifier) {
106604
106821
  return this.withLockedAssessmentAssociations(integrationId, qtiTestIdentifier, async (row, tx) => {
106605
106822
  if (qtiTestIdentifier === reviewBankSinkIdentifier(integrationId)) {
@@ -106872,106 +107089,118 @@ class TimebackAssessmentsService {
106872
107089
  assertAllAssessmentAssociationsDraft(associations);
106873
107090
  const ownership = await this.requireOwnedDraftAssessment(integrationId, qtiTestIdentifier, row, tx);
106874
107091
  const creation = parseQtiQuestionCreationInput(input);
106875
- const itemIdentifier = newPlaycademyQuestionIdentifier(qtiTestIdentifier);
106876
- let title;
106877
- let metadata2;
106878
- let xml;
106879
- let structuredInput;
106880
- let fallbackItem;
106881
- if (creation.kind === "copy") {
106882
- const source = await client2.qtiApi.assessmentItems.get(creation.sourceItemIdentifier);
106883
- const copy = buildQtiQuestionCopyInput({
106884
- source,
106885
- targetIdentifier: itemIdentifier,
106886
- targetTestIdentifier: qtiTestIdentifier,
106887
- gameSlug: ownership.gameSlug,
106888
- integrationId
106889
- });
106890
- title = copy.title;
106891
- metadata2 = copy.metadata;
106892
- xml = copy.xml;
106893
- fallbackItem = {
106894
- ...source,
106895
- identifier: itemIdentifier,
106896
- title,
106897
- rawXml: xml,
106898
- metadata: metadata2
106899
- };
106900
- } else {
106901
- structuredInput = creation.input;
106902
- assertSupportedQtiQuestionInteraction(structuredInput);
106903
- title = typeof structuredInput.title === "string" ? structuredInput.title.trim() : "";
106904
- metadata2 = buildOwnedQtiQuestionMetadata(structuredInput, {
106905
- gameSlug: ownership.gameSlug,
106906
- testIdentifier: qtiTestIdentifier
106907
- });
106908
- xml = buildQuestionXml(structuredInput, itemIdentifier, title);
106909
- fallbackItem = {
106910
- ...structuredInput,
106911
- identifier: itemIdentifier,
106912
- title,
106913
- ...xml ? { rawXml: xml } : {}
106914
- };
106915
- }
106916
- const section = await this.qtiSectionItems(client2, qtiTestIdentifier, undefined, undefined, ownership.test);
106917
- const href = qtiItemHref(client2, itemIdentifier);
106918
- let item;
106919
- let itemCreationAttempted = false;
106920
- let referenceCreationAttempted = false;
106921
- try {
106922
- itemCreationAttempted = true;
106923
- item = xml ? await client2.course.createAssessmentItemXml({ xml, metadata: metadata2 }) : await client2.qtiApi.assessmentItems.create({
106924
- ...structuredInput,
106925
- identifier: itemIdentifier,
106926
- metadata: metadata2
106927
- });
106928
- referenceCreationAttempted = true;
106929
- await section.items.add({
106930
- identifier: itemIdentifier,
106931
- href
106932
- });
106933
- } catch (error88) {
106934
- let referenceRemoved = !referenceCreationAttempted;
106935
- const creationStatus = isApiError(error88) ? error88.statusCode : undefined;
106936
- const itemCreationDefinitelyRejected = !referenceCreationAttempted && typeof creationStatus === "number" && creationStatus >= 400 && creationStatus < 500 && creationStatus !== 408;
106937
- if (referenceCreationAttempted) {
106938
- try {
106939
- await section.items.remove(itemIdentifier);
106940
- referenceRemoved = true;
106941
- } catch (cleanupError) {
106942
- referenceRemoved = isApiError(cleanupError) && cleanupError.statusCode === 404;
106943
- addEvent("assessment.qti_question_reference_cleanup_failed", {
106944
- "app.assessment.qti_test_identifier": qtiTestIdentifier,
106945
- "app.assessment.qti_item_identifier": itemIdentifier,
106946
- "exception.type": errorType(cleanupError),
106947
- "app.error.message": errorMessage(cleanupError)
106948
- });
106949
- }
106950
- }
106951
- if (itemCreationAttempted && !itemCreationDefinitelyRejected && referenceRemoved) {
106952
- try {
106953
- await client2.qtiApi.assessmentItems.delete(itemIdentifier);
106954
- } catch (cleanupError) {
106955
- addEvent("assessment.qti_question_cleanup_failed", {
106956
- "app.assessment.qti_test_identifier": qtiTestIdentifier,
106957
- "app.assessment.qti_item_identifier": itemIdentifier,
106958
- "exception.type": errorType(cleanupError),
106959
- "app.error.message": errorMessage(cleanupError)
106960
- });
106961
- }
106962
- }
106963
- throw error88;
106964
- }
107092
+ const created = await this.createQuestionInOwnedTest({
107093
+ client: client2,
107094
+ integrationId,
107095
+ qtiTestIdentifier,
107096
+ test: ownership.test,
107097
+ gameSlug: ownership.gameSlug,
107098
+ creation
107099
+ });
106965
107100
  setAttribute("app.assessment.operation", creation.kind === "copy" ? "copy_question" : "create_question");
106966
- return buildCreatedQtiQuestionReference({
106967
- item,
106968
- fallbackItem,
106969
- itemIdentifier,
106970
- href,
106971
- partIdentifier: section.partIdentifier,
106972
- sectionIdentifier: section.sectionIdentifier,
107101
+ return created;
107102
+ });
107103
+ }
107104
+ async createQuestionInOwnedTest(input) {
107105
+ const { client: client2, integrationId, qtiTestIdentifier, creation } = input;
107106
+ const itemIdentifier = newPlaycademyQuestionIdentifier(qtiTestIdentifier);
107107
+ let title;
107108
+ let metadata2;
107109
+ let xml;
107110
+ let structuredInput;
107111
+ let fallbackItem;
107112
+ if (creation.kind === "copy") {
107113
+ const source = await client2.qtiApi.assessmentItems.get(creation.sourceItemIdentifier);
107114
+ const copy = buildQtiQuestionCopyInput({
107115
+ source,
107116
+ targetIdentifier: itemIdentifier,
107117
+ targetTestIdentifier: qtiTestIdentifier,
107118
+ gameSlug: input.gameSlug,
107119
+ integrationId
107120
+ });
107121
+ title = copy.title;
107122
+ metadata2 = copy.metadata;
107123
+ xml = copy.xml;
107124
+ fallbackItem = {
107125
+ ...source,
107126
+ identifier: itemIdentifier,
107127
+ title,
107128
+ rawXml: xml,
106973
107129
  metadata: metadata2
107130
+ };
107131
+ } else {
107132
+ structuredInput = creation.input;
107133
+ assertSupportedQtiQuestionInteraction(structuredInput);
107134
+ title = typeof structuredInput.title === "string" ? structuredInput.title.trim() : "";
107135
+ metadata2 = buildOwnedQtiQuestionMetadata(structuredInput, {
107136
+ gameSlug: input.gameSlug,
107137
+ testIdentifier: qtiTestIdentifier
106974
107138
  });
107139
+ xml = buildQuestionXml(structuredInput, itemIdentifier, title);
107140
+ fallbackItem = {
107141
+ ...structuredInput,
107142
+ identifier: itemIdentifier,
107143
+ title,
107144
+ ...xml ? { rawXml: xml } : {}
107145
+ };
107146
+ }
107147
+ const section = await this.qtiSectionItems(client2, qtiTestIdentifier, undefined, undefined, input.test);
107148
+ const href = qtiItemHref(client2, itemIdentifier);
107149
+ let item;
107150
+ let itemCreationAttempted = false;
107151
+ let referenceCreationAttempted = false;
107152
+ try {
107153
+ itemCreationAttempted = true;
107154
+ item = xml ? await client2.course.createAssessmentItemXml({ xml, metadata: metadata2 }) : await client2.qtiApi.assessmentItems.create({
107155
+ ...structuredInput,
107156
+ identifier: itemIdentifier,
107157
+ metadata: metadata2
107158
+ });
107159
+ referenceCreationAttempted = true;
107160
+ await section.items.add({
107161
+ identifier: itemIdentifier,
107162
+ href
107163
+ });
107164
+ } catch (error88) {
107165
+ let referenceRemoved = !referenceCreationAttempted;
107166
+ const creationStatus = isApiError(error88) ? error88.statusCode : undefined;
107167
+ const itemCreationDefinitelyRejected = !referenceCreationAttempted && typeof creationStatus === "number" && creationStatus >= 400 && creationStatus < 500 && creationStatus !== 408;
107168
+ if (referenceCreationAttempted) {
107169
+ try {
107170
+ await section.items.remove(itemIdentifier);
107171
+ referenceRemoved = true;
107172
+ } catch (cleanupError) {
107173
+ referenceRemoved = isApiError(cleanupError) && cleanupError.statusCode === 404;
107174
+ addEvent("assessment.qti_question_reference_cleanup_failed", {
107175
+ "app.assessment.qti_test_identifier": qtiTestIdentifier,
107176
+ "app.assessment.qti_item_identifier": itemIdentifier,
107177
+ "exception.type": errorType(cleanupError),
107178
+ "app.error.message": errorMessage(cleanupError)
107179
+ });
107180
+ }
107181
+ }
107182
+ if (itemCreationAttempted && !itemCreationDefinitelyRejected && referenceRemoved) {
107183
+ try {
107184
+ await client2.qtiApi.assessmentItems.delete(itemIdentifier);
107185
+ } catch (cleanupError) {
107186
+ addEvent("assessment.qti_question_cleanup_failed", {
107187
+ "app.assessment.qti_test_identifier": qtiTestIdentifier,
107188
+ "app.assessment.qti_item_identifier": itemIdentifier,
107189
+ "exception.type": errorType(cleanupError),
107190
+ "app.error.message": errorMessage(cleanupError)
107191
+ });
107192
+ }
107193
+ }
107194
+ throw error88;
107195
+ }
107196
+ return buildCreatedQtiQuestionReference({
107197
+ item,
107198
+ fallbackItem,
107199
+ itemIdentifier,
107200
+ href,
107201
+ partIdentifier: section.partIdentifier,
107202
+ sectionIdentifier: section.sectionIdentifier,
107203
+ metadata: metadata2
106975
107204
  });
106976
107205
  }
106977
107206
  async updateQuestion(integrationId, qtiTestIdentifier, itemIdentifier, input) {
@@ -107219,6 +107448,22 @@ class TimebackAssessmentsService {
107219
107448
  }
107220
107449
  return row;
107221
107450
  }
107451
+ async requireCurrentRevisionRow(integrationId, qtiTestIdentifier) {
107452
+ const row = await this.deps.db.query.gameTimebackAssessmentTests.findFirst({
107453
+ where: and(eq(gameTimebackAssessmentTests.integrationId, integrationId), eq(gameTimebackAssessmentTests.qtiTestIdentifier, qtiTestIdentifier))
107454
+ });
107455
+ if (row) {
107456
+ return row;
107457
+ }
107458
+ const assessmentKey = assessmentKeyFromManagedQtiIdentifier(qtiTestIdentifier);
107459
+ const successor = assessmentKey ? await this.deps.db.query.gameTimebackAssessmentTests.findFirst({
107460
+ where: and(eq(gameTimebackAssessmentTests.integrationId, integrationId), eq(gameTimebackAssessmentTests.assessmentKey, assessmentKey))
107461
+ }) : undefined;
107462
+ if (successor) {
107463
+ throw revisionConflict(qtiTestIdentifier, successor.qtiTestIdentifier);
107464
+ }
107465
+ throw new NotFoundError(`Assessment not found: ${qtiTestIdentifier}`);
107466
+ }
107222
107467
  async requireDraftAssessmentRow(integrationId, qtiTestIdentifier, lockedRow) {
107223
107468
  const row = lockedRow ?? await this.requireAssessmentRow(integrationId, qtiTestIdentifier);
107224
107469
  assertDraftAssessment(row);
@@ -107366,9 +107611,21 @@ class TimebackAssessmentsService {
107366
107611
  itemHref: (identifier) => qtiItemHref(client2, identifier)
107367
107612
  });
107368
107613
  const artifactSource = await this.ensureManagedQtiPublication(client2, plan);
107369
- const artifactsAvailable = await this.storeManagedArtifacts(artifactSource, managedAssessmentDiagnosticRoutingManifest(artifactSource.test, plan.assessmentKey));
107614
+ const artifactsAvailable = await this.managedArtifactsStored(plan.qtiTestIdentifier) || await this.storeManagedArtifacts(artifactSource, managedAssessmentDiagnosticRoutingManifest(artifactSource.test, plan.assessmentKey));
107370
107615
  const warning = assessmentArtifactPublicationWarning(artifactsAvailable);
107371
- return { ...plan, ...warning ? { warning } : {} };
107616
+ return { ...plan, artifactsAvailable, ...warning ? { warning } : {} };
107617
+ }
107618
+ async managedArtifactsStored(qtiTestIdentifier) {
107619
+ try {
107620
+ return await this.deps.assessmentArtifacts.loadManagedPlayable(qtiTestIdentifier) !== null;
107621
+ } catch (error88) {
107622
+ addEvent("assessment.artifact_lookup_failed", {
107623
+ "app.assessment.qti_test_identifier": qtiTestIdentifier,
107624
+ "exception.type": errorType(error88),
107625
+ "app.error.message": errorMessage(error88)
107626
+ });
107627
+ return false;
107628
+ }
107372
107629
  }
107373
107630
  async cleanupQtiAssessmentCopy(client2, testIdentifier, itemIdentifiers) {
107374
107631
  let testDeleted = !testIdentifier;
@@ -107495,6 +107752,7 @@ var init_timeback_assessments_service = __esm(async () => {
107495
107752
  init_timeback_assessment_diagnostic_util();
107496
107753
  init_timeback_assessment_import_util();
107497
107754
  init_timeback_assessment_publication_util();
107755
+ init_timeback_assessment_revision_util();
107498
107756
  init_timeback_assessment_rules_util();
107499
107757
  init_timeback_qti_authoring_util();
107500
107758
  init_timeback_qti_hydration_util();
@@ -167504,7 +167762,7 @@ function parseQtiLibraryParams(searchParams) {
167504
167762
  limit
167505
167763
  };
167506
167764
  }
167507
- 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;
167765
+ 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;
167508
167766
  var init_timeback_controller = __esm(() => {
167509
167767
  init_esm();
167510
167768
  init_src();
@@ -168249,6 +168507,20 @@ var init_timeback_controller = __esm(() => {
168249
168507
  const body2 = await parseRequestBody(ctx.request, UpdateAssessmentRequestSchema);
168250
168508
  return ctx.services.timebackAssessments.updateAssessment(integrationId, testIdentifier, body2);
168251
168509
  });
168510
+ reviseAssessment = requireDeveloper(async (ctx) => {
168511
+ const { gameId, courseId } = ctx.params;
168512
+ if (!gameId || !courseId) {
168513
+ throw ApiError.badRequest("Missing gameId or courseId parameter");
168514
+ }
168515
+ const body2 = await parseRequestBody(ctx.request, ReviseAssessmentRequestSchema);
168516
+ const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
168517
+ return ctx.services.timebackAssessments.reviseAssessment(integrationId, {
168518
+ currentQtiTestIdentifier: body2.currentQtiTestIdentifier,
168519
+ sourceTestIdentifier: newPlaycademyTestIdentifier(),
168520
+ title: body2.title,
168521
+ questions: body2.questions
168522
+ });
168523
+ });
168252
168524
  synchronizeReviewBank = requireDeveloper(async (ctx) => {
168253
168525
  const { gameId, courseId } = ctx.params;
168254
168526
  if (!gameId || !courseId) {
@@ -168399,6 +168671,7 @@ var init_timeback_controller = __esm(() => {
168399
168671
  createAssessment,
168400
168672
  attachExistingAssessments,
168401
168673
  updateAssessment,
168674
+ reviseAssessment,
168402
168675
  synchronizeReviewBank,
168403
168676
  removeAssessment,
168404
168677
  reorderQuestions,