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