@playcademy/sandbox 0.8.1-beta.5 → 0.8.1-beta.7

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_ITEM_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 = 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
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.8.1-beta.5",
1130
+ version: "0.8.1-beta.7",
1131
1131
  description: "Local development server for Playcademy game development",
1132
1132
  type: "module",
1133
1133
  exports: {
@@ -9239,26 +9239,6 @@ var init_v3 = __esm(() => {
9239
9239
  var init_esm = __esm(() => {
9240
9240
  init_v3();
9241
9241
  });
9242
- // ../types/src/timeback/assessment-runtime.ts
9243
- var PLAYABLE_ASSESSMENT_SHAPES;
9244
- var init_assessment_runtime = __esm(() => {
9245
- PLAYABLE_ASSESSMENT_SHAPES = ["circle", "ellipse", "rect", "poly", "default"];
9246
- });
9247
- // ../types/src/timeback/diagnostic-routing.ts
9248
- var DIAGNOSTIC_ROUTING_MANIFEST_VERSION = 1, DIAGNOSTIC_ROUTING_STATE_VERSION = 1, DIAGNOSTIC_TRACK_OUTCOMES;
9249
- var init_diagnostic_routing = __esm(() => {
9250
- DIAGNOSTIC_TRACK_OUTCOMES = [
9251
- "provisional",
9252
- "unresolved",
9253
- "route-to-instruction",
9254
- "not-assessed"
9255
- ];
9256
- });
9257
- // ../types/src/timeback/index.ts
9258
- var init_timeback4 = __esm(() => {
9259
- init_assessment_runtime();
9260
- init_diagnostic_routing();
9261
- });
9262
9242
 
9263
9243
  // ../utils/src/uuid.ts
9264
9244
  function isValidUUID(value) {
@@ -9282,6 +9262,26 @@ var UUID_REGEX;
9282
9262
  var init_uuid = __esm(() => {
9283
9263
  UUID_REGEX = /^[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12}$/;
9284
9264
  });
9265
+ // ../types/src/timeback/assessment-runtime.ts
9266
+ var PLAYABLE_ASSESSMENT_SHAPES;
9267
+ var init_assessment_runtime = __esm(() => {
9268
+ PLAYABLE_ASSESSMENT_SHAPES = ["circle", "ellipse", "rect", "poly", "default"];
9269
+ });
9270
+ // ../types/src/timeback/diagnostic-routing.ts
9271
+ var DIAGNOSTIC_ROUTING_MANIFEST_VERSION = 1, DIAGNOSTIC_ROUTING_STATE_VERSION = 1, DIAGNOSTIC_TRACK_OUTCOMES;
9272
+ var init_diagnostic_routing = __esm(() => {
9273
+ DIAGNOSTIC_TRACK_OUTCOMES = [
9274
+ "provisional",
9275
+ "unresolved",
9276
+ "route-to-instruction",
9277
+ "not-assessed"
9278
+ ];
9279
+ });
9280
+ // ../types/src/timeback/index.ts
9281
+ var init_timeback4 = __esm(() => {
9282
+ init_assessment_runtime();
9283
+ init_diagnostic_routing();
9284
+ });
9285
9285
 
9286
9286
  // ../timeback/dist/engines/assessment-runtime/index.js
9287
9287
  function qtiDirectedPairValidationIssue(values, contract) {
@@ -9561,6 +9561,25 @@ function assessmentReviewBankShortage(fulfillment) {
9561
9561
  function assessmentFlowForPurpose(purpose) {
9562
9562
  return purpose === "review" ? "item-submit" : "attempt-submit";
9563
9563
  }
9564
+ function isObject(value) {
9565
+ return typeof value === "object" && value !== null;
9566
+ }
9567
+ function isTimebackGrade(value) {
9568
+ return typeof value === "number" && Number.isInteger(value) && GRADE_VALUES.includes(value);
9569
+ }
9570
+ function isAssessmentSessionTiming(value) {
9571
+ return isObject(value) && typeof value.runId === "string" && isValidUUID(value.runId) && typeof value.resumeId === "string" && isValidUUID(value.resumeId) && isNonNegativeDuration(value.activeSeconds) && (value.inactiveSeconds === undefined || isNonNegativeDuration(value.inactiveSeconds));
9572
+ }
9573
+ function isNonNegativeDuration(value) {
9574
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
9575
+ }
9576
+ function isAssessmentPurpose(value) {
9577
+ return ASSESSMENT_PURPOSES.includes(value);
9578
+ }
9579
+ function isAssessmentResponseValue(value) {
9580
+ const entries = Array.isArray(value) ? value : [value];
9581
+ return entries.length > 0 && entries.every((entry) => typeof entry === "string" && entry.length > 0 && entry.length <= TIMEBACK_ASSESSMENT_RESPONSE_VALUE_MAX_LENGTH);
9582
+ }
9564
9583
  function compareCodeUnits(left, right) {
9565
9584
  if (left < right) {
9566
9585
  return -1;
@@ -10414,48 +10433,6 @@ function projectDiagnosticRoutingSnapshot(revision, state) {
10414
10433
  }))
10415
10434
  };
10416
10435
  }
10417
- function isObject(value) {
10418
- return typeof value === "object" && value !== null;
10419
- }
10420
- function isTimebackGrade(value) {
10421
- return typeof value === "number" && Number.isInteger(value) && GRADE_VALUES.includes(value);
10422
- }
10423
- function isAssessmentSessionTiming(value) {
10424
- return isObject(value) && typeof value.runId === "string" && isValidUUID(value.runId) && typeof value.resumeId === "string" && isValidUUID(value.resumeId) && isNonNegativeDuration(value.activeSeconds) && (value.inactiveSeconds === undefined || isNonNegativeDuration(value.inactiveSeconds));
10425
- }
10426
- function isNonNegativeDuration(value) {
10427
- return typeof value === "number" && Number.isFinite(value) && value >= 0;
10428
- }
10429
- function isAssessmentPurpose(value) {
10430
- return ASSESSMENT_PURPOSES.includes(value);
10431
- }
10432
- function isAssessmentResponseValue(value) {
10433
- const entries = Array.isArray(value) ? value : [value];
10434
- return entries.length > 0 && entries.every((entry) => typeof entry === "string" && entry.length > 0 && entry.length <= TIMEBACK_ASSESSMENT_RESPONSE_VALUE_MAX_LENGTH);
10435
- }
10436
- function applyAssessmentItemResponseUpdate(current, update) {
10437
- const next = { ...current };
10438
- for (const [responseId, value] of Object.entries(update)) {
10439
- if (value === null) {
10440
- delete next[responseId];
10441
- } else {
10442
- next[responseId] = value;
10443
- }
10444
- }
10445
- return Object.keys(next).length === 0 ? undefined : next;
10446
- }
10447
- function applyAssessmentResponseUpdate(current, update) {
10448
- const next = structuredClone(current);
10449
- for (const [questionId, questionUpdate] of Object.entries(update)) {
10450
- const questionResponses = applyAssessmentItemResponseUpdate(next[questionId], questionUpdate);
10451
- if (questionResponses === undefined) {
10452
- delete next[questionId];
10453
- } else {
10454
- next[questionId] = questionResponses;
10455
- }
10456
- }
10457
- return next;
10458
- }
10459
10436
  function assessmentResponseUpdateValidationMessage(assessment, update) {
10460
10437
  const items = new Map(assessment.items.map((item) => [item.identifier, item]));
10461
10438
  for (const [itemIdentifier, itemUpdate] of Object.entries(update)) {
@@ -10617,158 +10594,163 @@ function interactionResponseValidationMessage(itemIdentifier, interaction, respo
10617
10594
  }
10618
10595
  return null;
10619
10596
  }
10620
- function invalidResponse(message) {
10621
- return { code: "INVALID_RESPONSE", message };
10622
- }
10623
- function administeredItemIdentifiers(itemSubmissions) {
10624
- return new Set(itemSubmissions.map((submission) => submission.itemIdentifier));
10625
- }
10626
- function itemAdministration(itemSubmissions, itemIdentifier) {
10627
- return itemSubmissions.find((submission) => submission.itemIdentifier === itemIdentifier);
10628
- }
10629
- function responseValuesEqual(left, right) {
10630
- if (left === undefined || right === undefined) {
10631
- return left === right;
10632
- }
10633
- if (Array.isArray(left) || Array.isArray(right)) {
10634
- return Array.isArray(left) && Array.isArray(right) && left.length === right.length && left.every((value, index) => value === right[index]);
10635
- }
10636
- return left === right;
10637
- }
10638
- function itemResponsesEqual(left, right) {
10639
- const leftEntries = Object.entries(left ?? {});
10640
- const rightEntries = Object.entries(right ?? {});
10641
- return leftEntries.length === rightEntries.length && leftEntries.every(([identifier, value]) => responseValuesEqual(value, right?.[identifier]));
10597
+ function emptyAssessmentTranscript(attemptId, contentRevision) {
10598
+ return { version: 1, attemptId, contentRevision, administrations: [] };
10642
10599
  }
10643
- function itemResponseUpdateMatches(current, update) {
10644
- return itemResponsesEqual(current, applyAssessmentItemResponseUpdate(current, update));
10645
- }
10646
- function resolveAssessmentItemReplay(state, input) {
10647
- const flow = assessmentFlowForPurpose(state.purpose);
10648
- if (flow !== "item-submit") {
10600
+ function assessmentProgressAdministration(purpose, input) {
10601
+ if (purpose !== "mastery") {
10649
10602
  return {
10650
- action: "conflict",
10651
- failure: assessmentFlowViolation("Items cannot be submitted individually in this flow.", { flow })
10603
+ ok: false,
10604
+ failure: assessmentFlowViolation("Background progress is only available for mastery.")
10652
10605
  };
10653
10606
  }
10654
- const priorSubmission = state.itemSubmissions.find((submission) => submission.submissionId === input.submissionId);
10655
- if (priorSubmission) {
10656
- const sameRequest = priorSubmission.itemIdentifier === input.itemIdentifier && itemResponseUpdateMatches(state.responses[input.itemIdentifier], input.responses);
10657
- if (!sameRequest) {
10658
- return {
10659
- action: "conflict",
10660
- failure: assessmentFlowViolation("This item submission ID was already used for a different request.", {
10661
- submissionId: input.submissionId,
10662
- itemIdentifier: input.itemIdentifier,
10663
- committedItemIdentifier: priorSubmission.itemIdentifier,
10664
- flow
10665
- })
10666
- };
10667
- }
10607
+ const { administrations } = input.transcript;
10608
+ if (input.expectedResponseVersion !== administrations.length) {
10668
10609
  return {
10669
- action: "replay",
10670
- responseVersion: state.responseVersion,
10671
- responses: state.responses,
10672
- submission: priorSubmission
10610
+ ok: false,
10611
+ failure: assessmentResponseVersionConflict(input.expectedResponseVersion, administrations.length)
10673
10612
  };
10674
10613
  }
10675
- return { action: "missing" };
10614
+ const administration = administrations.find((entry) => entry.itemIdentifier === input.itemIdentifier);
10615
+ return administration ? { ok: true, submissionId: administration.submissionId } : {
10616
+ ok: false,
10617
+ failure: assessmentFlowViolation("The progress item must be present in the transcript.")
10618
+ };
10676
10619
  }
10677
- function prepareAssessmentItemSubmission(state, input) {
10678
- const replay = resolveAssessmentItemReplay(state, input);
10679
- if (replay.action === "conflict") {
10680
- return { action: "reject", failure: replay.failure };
10681
- }
10682
- if (replay.action === "replay") {
10683
- return replay;
10684
- }
10685
- if (state.responseVersion !== input.expectedResponseVersion) {
10620
+ function appendAssessmentAdministration(transcript, input) {
10621
+ const entry = {
10622
+ submissionId: input.submissionId,
10623
+ itemIdentifier: input.itemIdentifier,
10624
+ ...input.routingNodeKey === undefined ? {} : { routingNodeKey: input.routingNodeKey },
10625
+ responses: Object.fromEntries(Object.entries(input.responses).filter((pair) => pair[1] !== null))
10626
+ };
10627
+ const priorIndex = transcript.administrations.findIndex((item) => item.submissionId === input.submissionId);
10628
+ const index = priorIndex === -1 ? transcript.administrations.length : priorIndex;
10629
+ if (input.expectedResponseVersion !== index) {
10686
10630
  return {
10687
- action: "reject",
10688
- failure: assessmentResponseVersionConflict(input.expectedResponseVersion, state.responseVersion)
10631
+ ok: false,
10632
+ failure: assessmentResponseVersionConflict(input.expectedResponseVersion, index)
10689
10633
  };
10690
10634
  }
10691
- const itemIndex = state.assessment.items.findIndex((item) => item.identifier === input.itemIdentifier);
10692
- const previousItemIdentifier = state.itemSubmissions.at(-1)?.itemIdentifier;
10693
- const previousItemIndex = state.assessment.items.findIndex((item) => item.identifier === previousItemIdentifier);
10694
- if (itemIndex === -1 || itemIndex <= previousItemIndex) {
10635
+ if (priorIndex !== -1 && canonicalJson(transcript.administrations[priorIndex]) !== canonicalJson(entry)) {
10695
10636
  return {
10696
- action: "reject",
10697
- failure: assessmentFlowViolation("Items must be submitted once and in candidate order.", {
10698
- itemIdentifier: input.itemIdentifier,
10699
- previousItemIdentifier: previousItemIdentifier ?? null,
10700
- flow: assessmentFlowForPurpose(state.purpose)
10701
- })
10637
+ ok: false,
10638
+ failure: assessmentFlowViolation("This administration identity was reused with different answers.")
10702
10639
  };
10703
10640
  }
10704
- const update = { [input.itemIdentifier]: input.responses };
10705
- const updateMessage = assessmentResponseUpdateValidationMessage(state.assessment, update);
10706
- if (updateMessage) {
10707
- return { action: "reject", failure: invalidResponse(updateMessage) };
10708
- }
10709
- const responses = applyAssessmentResponseUpdate(state.responses, update);
10710
- const validationMessage = assessmentResponseValidationMessage(state.assessment, responses);
10711
- if (validationMessage) {
10712
- return { action: "reject", failure: invalidResponse(validationMessage) };
10713
- }
10714
10641
  return {
10715
- action: "commit",
10716
- itemIndex,
10717
- responseVersion: state.responseVersion + 1,
10718
- responses,
10719
- answered: Object.keys(responses[input.itemIdentifier] ?? {}).length > 0
10642
+ ok: true,
10643
+ index,
10644
+ transcript: priorIndex === -1 ? { ...transcript, administrations: [...transcript.administrations, entry] } : transcript
10720
10645
  };
10721
10646
  }
10722
- function completeAssessmentItemSubmission(state, input, prepared, scoring, submittedAt) {
10723
- const submission = {
10724
- submissionId: input.submissionId,
10725
- itemIdentifier: input.itemIdentifier,
10726
- submittedAt,
10727
- responseVersion: prepared.responseVersion,
10728
- answered: prepared.answered,
10729
- score: scoring.score,
10730
- isCorrect: scoring.isCorrect,
10731
- ...scoring.grading ? { grading: scoring.grading } : {}
10732
- };
10733
- return {
10734
- responseVersion: prepared.responseVersion,
10735
- responses: prepared.responses,
10736
- itemSubmissions: [...state.itemSubmissions, submission],
10737
- submission
10738
- };
10647
+ function prepareAssessmentTranscript(input) {
10648
+ const parsed = AssessmentTranscriptSchema.safeParse(input.transcript);
10649
+ if (!parsed.success) {
10650
+ throw new AssessmentTranscriptError("Invalid assessment transcript", 0, "INVALID_RESPONSE");
10651
+ }
10652
+ const transcript = parsed.data;
10653
+ if (transcript.attemptId !== input.attemptId || transcript.contentRevision !== input.contentRevision) {
10654
+ throw new AssessmentTranscriptError("Transcript does not belong to this attempt and revision");
10655
+ }
10656
+ const responses = {};
10657
+ const submissions = new Set;
10658
+ const items = new Set;
10659
+ let previousItemIndex = -1;
10660
+ for (const [index, entry] of transcript.administrations.entries()) {
10661
+ const itemIndex = input.assessment.items.findIndex((item) => item.identifier === entry.itemIdentifier);
10662
+ if (submissions.has(entry.submissionId) || items.has(entry.itemIdentifier)) {
10663
+ throw new AssessmentTranscriptError("Transcript contains a repeated administration or item", index);
10664
+ }
10665
+ if (itemIndex === -1) {
10666
+ throw new AssessmentTranscriptError("Transcript references an unknown assessment item", index, "INVALID_RESPONSE");
10667
+ }
10668
+ if (!input.routed && itemIndex <= previousItemIndex) {
10669
+ throw new AssessmentTranscriptError("Transcript items must follow the selected candidate order", index);
10670
+ }
10671
+ if (input.routed !== (entry.routingNodeKey !== undefined)) {
10672
+ throw new AssessmentTranscriptError("Transcript routing identity does not match the assessment flow", index);
10673
+ }
10674
+ const itemResponses = { [entry.itemIdentifier]: entry.responses };
10675
+ const validate2 = input.routed || input.allowIncomplete ? assessmentDraftResponseValidationMessage : assessmentResponseValidationMessage;
10676
+ const invalid = Object.keys(entry.responses).length === 0 ? null : validate2(input.assessment, itemResponses);
10677
+ if (invalid) {
10678
+ throw new AssessmentTranscriptError(invalid, index, "INVALID_RESPONSE");
10679
+ }
10680
+ submissions.add(entry.submissionId);
10681
+ items.add(entry.itemIdentifier);
10682
+ previousItemIndex = itemIndex;
10683
+ Object.assign(responses, itemResponses);
10684
+ }
10685
+ return { transcript, responses };
10686
+ }
10687
+ function completeAssessmentTranscript(input) {
10688
+ const itemSubmissions = [];
10689
+ let state;
10690
+ const ledger = [];
10691
+ if (input.manifest) {
10692
+ const initial = initializeDiagnosticRouting(input.manifest);
10693
+ if (!initial.ok) {
10694
+ throw new AssessmentTranscriptError("The pinned diagnostic routing manifest is invalid");
10695
+ }
10696
+ state = initial.state;
10697
+ }
10698
+ const scores = new Map(input.scores.map((score) => [score.itemIdentifier, score]));
10699
+ for (const [index, entry] of input.transcript.administrations.entries()) {
10700
+ const scoring = scores.get(entry.itemIdentifier);
10701
+ if (!scoring) {
10702
+ throw new AssessmentTranscriptError("Transcript item has no trusted grade", index);
10703
+ }
10704
+ if (input.manifest) {
10705
+ if (typeof scoring.isCorrect !== "boolean" || !entry.routingNodeKey) {
10706
+ throw new AssessmentTranscriptError("Diagnostic grading must be determinate", index);
10707
+ }
10708
+ const advanced = advanceDiagnosticRouting(input.manifest, ledger, {
10709
+ routingNodeKey: entry.routingNodeKey,
10710
+ itemIdentifier: entry.itemIdentifier,
10711
+ isCorrect: scoring.isCorrect
10712
+ });
10713
+ if (!advanced.ok) {
10714
+ throw new AssessmentTranscriptError("Transcript does not follow the pinned diagnostic route", index);
10715
+ }
10716
+ ledger.push(advanced.ledgerEntry);
10717
+ state = advanced.state;
10718
+ }
10719
+ itemSubmissions.push({
10720
+ submissionId: entry.submissionId,
10721
+ itemIdentifier: entry.itemIdentifier,
10722
+ responseVersion: index + 1,
10723
+ submittedAt: input.submittedAt,
10724
+ answered: Object.keys(entry.responses).length > 0,
10725
+ score: scoring.score,
10726
+ isCorrect: scoring.isCorrect,
10727
+ ...scoring.grading ? { grading: scoring.grading } : {}
10728
+ });
10729
+ }
10730
+ if (input.terminal) {
10731
+ const count = itemSubmissions.length;
10732
+ const conventionalComplete = assessmentFlowForPurpose(input.purpose) === "item-submit" ? count > 0 : count === input.itemCount;
10733
+ const complete = state ? state.status === "ready-to-complete" && count > 0 : conventionalComplete;
10734
+ if (!complete) {
10735
+ throw new AssessmentTranscriptError("A complete administered transcript is required for submission", count);
10736
+ }
10737
+ }
10738
+ return { itemSubmissions, ledger, state };
10739
10739
  }
10740
- function assessmentResponseUpdateFlowFailure(purpose, itemSubmissions, update) {
10741
- const administered = administeredItemIdentifiers(itemSubmissions);
10742
- const committedUpdate = Object.keys(update).find((identifier) => administered.has(identifier));
10743
- return committedUpdate ? assessmentFlowViolation("A submitted item response cannot be edited.", {
10744
- itemIdentifier: committedUpdate,
10745
- flow: assessmentFlowForPurpose(purpose)
10746
- }) : null;
10740
+ function administeredItemIdentifiers(itemSubmissions) {
10741
+ return new Set(itemSubmissions.map((submission) => submission.itemIdentifier));
10747
10742
  }
10748
- function assessmentCompletionFlowFailure(purpose, itemSubmissions, itemCount) {
10749
- const flow = assessmentFlowForPurpose(purpose);
10750
- if (flow !== "item-submit") {
10751
- return null;
10752
- }
10753
- if (itemSubmissions.length > 0 && itemSubmissions.length <= itemCount) {
10754
- return null;
10755
- }
10756
- return assessmentFlowViolation("At least one candidate must be administered before the review can be completed.", {
10757
- flow,
10758
- submittedItemCount: itemSubmissions.length,
10759
- itemCount
10760
- });
10743
+ function itemAdministration(itemSubmissions, itemIdentifier) {
10744
+ return itemSubmissions.find((submission) => submission.itemIdentifier === itemIdentifier);
10761
10745
  }
10762
10746
  function selectAssessmentCourseCandidate(candidates, subjectSpecified) {
10763
10747
  if (candidates.length === 0) {
10764
10748
  return { outcome: "none" };
10765
10749
  }
10766
- const serviceable = candidates.filter((candidate) => candidate.hasLiveTests || candidate.hasOpenAttempt);
10767
- if (serviceable.length === 0) {
10750
+ const eligible = candidates.filter((candidate) => candidate.hasLiveTests);
10751
+ if (eligible.length === 0) {
10768
10752
  return { outcome: "no_eligible_tests" };
10769
10753
  }
10770
- const withOpenAttempt = serviceable.filter((candidate) => candidate.hasOpenAttempt);
10771
- const eligible = withOpenAttempt.length > 0 ? withOpenAttempt : serviceable;
10772
10754
  const subjects = new Set(eligible.map((candidate) => candidate.subject)).size;
10773
10755
  if (!subjectSpecified && subjects > 1) {
10774
10756
  return { outcome: "ambiguous", subjects };
@@ -10780,12 +10762,9 @@ function assessmentAttemptTimestamp(value) {
10780
10762
  const parsed = Date.parse(value);
10781
10763
  return Number.isNaN(parsed) ? 0 : parsed;
10782
10764
  }
10783
- function newestAttempt(left, right, field) {
10784
- const dateDifference = assessmentAttemptTimestamp(right[field]) - assessmentAttemptTimestamp(left[field]);
10785
- return dateDifference || left.attemptId.localeCompare(right.attemptId);
10786
- }
10787
10765
  function compareNewestCompletedAssessmentAttempt(left, right) {
10788
- return newestAttempt(left, right, "scoreDate");
10766
+ const dateDifference = assessmentAttemptTimestamp(right.scoreDate) - assessmentAttemptTimestamp(left.scoreDate);
10767
+ return dateDifference || left.attemptId.localeCompare(right.attemptId);
10789
10768
  }
10790
10769
  function compareAssessmentCatalogOrder(left, right) {
10791
10770
  if (left.assessmentKey !== null && right.assessmentKey !== null) {
@@ -10808,31 +10787,22 @@ function orderRuntimeAssessmentTests(tests) {
10808
10787
  return [...tests].toSorted(compareAssessmentCatalogOrder);
10809
10788
  }
10810
10789
  function selectRuntimeAssessment(liveTests, attempts) {
10811
- const resumable = attempts.filter((attempt) => attempt.resumable !== false && (isAssessmentAttemptOpen(attempt) || isAssessmentAttemptAwaitingAward(attempt))).toSorted((left, right) => newestAttempt(left, right, "updatedAt"));
10812
- if (resumable.length > 0) {
10813
- return {
10814
- kind: "resume",
10815
- attempt: resumable[0],
10816
- additionalResumableAttemptIds: resumable.slice(1).map((candidate) => candidate.attemptId)
10817
- };
10818
- }
10819
10790
  const orderedTests = orderRuntimeAssessmentTests(liveTests);
10820
10791
  if (orderedTests.length === 0) {
10821
10792
  return null;
10822
10793
  }
10823
- const completed = attempts.filter(isAssessmentAttemptCompleted);
10824
- const exposedAssessmentKeys = new Set(attempts.filter((attempt) => isAssessmentAttemptOpen(attempt) || isAssessmentAttemptAwaitingAward(attempt) || isAssessmentAttemptCompleted(attempt)).map((attempt) => attempt.selectedAssessmentKey));
10794
+ const history = attempts.filter((attempt) => isAssessmentAttemptOpen(attempt) || isAssessmentAttemptAwaitingAward(attempt) || isAssessmentAttemptCompleted(attempt) || isAssessmentAttemptSuperseded(attempt));
10795
+ const exposedAssessmentKeys = new Set(history.map((attempt) => attempt.selectedAssessmentKey));
10825
10796
  const unattempted = orderedTests.find((test) => !exposedAssessmentKeys.has(test.assessmentKey));
10826
10797
  if (unattempted) {
10827
- return { kind: "start", test: unattempted, reason: "unattempted" };
10798
+ return { test: unattempted, reason: "unattempted" };
10828
10799
  }
10829
- const latestCompleted = completed.toSorted(compareNewestCompletedAssessmentAttempt)[0];
10830
- const latestTestIndex = latestCompleted ? orderedTests.findIndex((test) => test.assessmentKey === latestCompleted.selectedAssessmentKey) : -1;
10800
+ const latest = history.toSorted(compareNewestCompletedAssessmentAttempt)[0];
10801
+ const latestTestIndex = latest ? orderedTests.findIndex((test) => test.assessmentKey === latest.selectedAssessmentKey) : -1;
10831
10802
  if (latestTestIndex === -1) {
10832
- return { kind: "start", test: orderedTests[0], reason: "first_live" };
10803
+ return { test: orderedTests[0], reason: "first_live" };
10833
10804
  }
10834
10805
  return {
10835
- kind: "start",
10836
10806
  test: orderedTests[(latestTestIndex + 1) % orderedTests.length],
10837
10807
  reason: "round_robin"
10838
10808
  };
@@ -11090,9 +11060,6 @@ function reviewRequestFingerprint(request, policy) {
11090
11060
  function reviewQuestionLineItemId(parentLineItemId, itemIdentifier) {
11091
11061
  return deterministicUUID(["playcademy:review-question-line-item:v1", parentLineItemId, itemIdentifier].join("\x00"));
11092
11062
  }
11093
- function reviewQuestionResultId(parentAttemptId, childQuestionLineItemId) {
11094
- return deterministicUUID(["playcademy:review-question-result:v1", parentAttemptId, childQuestionLineItemId].join("\x00"));
11095
- }
11096
11063
  function qtiQuestionByIdentifier(questions) {
11097
11064
  const byIdentifier = new Map;
11098
11065
  for (const entry of questions.questions) {
@@ -11652,11 +11619,8 @@ function isDiagnosticRoutingState(value) {
11652
11619
  const nextValid = value.next === null || isRecord(value.next) && isNonEmptyString(value.next.stageKey) && isNonEmptyString(value.next.trackKey) && isNonEmptyString(value.next.nodeKey) && isNonEmptyString(value.next.itemIdentifier);
11653
11620
  return stagesValid && tracksValid && nextValid && (value.status === "in-progress" && value.next !== null || value.status === "ready-to-complete" && value.next === null);
11654
11621
  }
11655
- function isDiagnosticRoutingSnapshot(value, revision) {
11656
- return isRecord(value) && value.kind === "platform-routed-diagnostic" && value.revision === revision && (value.status === "in-progress" || value.status === "ready-to-complete") && (value.next === null || isRecord(value.next) && isNonEmptyString(value.next.stageKey) && isNonEmptyString(value.next.trackKey) && isNonEmptyString(value.next.nodeKey) && isNonEmptyString(value.next.itemIdentifier)) && Array.isArray(value.tracks) && value.tracks.every((track) => isRecord(track) && isNonEmptyString(track.trackKey) && (track.groupKey === undefined || isNonEmptyString(track.groupKey)) && ["pending", "active", "completed", "skipped"].includes(String(track.status)) && Number.isInteger(track.administeredCount) && Number(track.administeredCount) >= 0);
11657
- }
11658
11622
  function isRoutedDiagnosticAttemptMetadata(value, itemSubmissions, responseVersion) {
11659
- if (!isRecord(value) || !isNonEmptyString(value.definitionId) || !isNonEmptyString(value.diagnosticKey) || !isNonEmptyString(value.testName) || !isNonEmptyString(value.routingRevision) || !Array.isArray(value.ledger) || !value.ledger.every(isDiagnosticRoutingLedgerEntry) || value.routingSnapshots !== undefined && (!Array.isArray(value.routingSnapshots) || value.routingSnapshots.length !== value.ledger.length || !value.routingSnapshots.every((snapshot) => isDiagnosticRoutingSnapshot(snapshot, String(value.routingRevision)))) || !isDiagnosticRoutingState(value.state) || value.ledger.length !== itemSubmissions.length || responseVersion !== itemSubmissions.length) {
11623
+ if (!isRecord(value) || !isNonEmptyString(value.definitionId) || !isNonEmptyString(value.diagnosticKey) || !isNonEmptyString(value.testName) || !isNonEmptyString(value.routingRevision) || !Array.isArray(value.ledger) || !value.ledger.every(isDiagnosticRoutingLedgerEntry) || !isDiagnosticRoutingState(value.state) || value.ledger.length !== itemSubmissions.length || responseVersion !== itemSubmissions.length) {
11660
11624
  return false;
11661
11625
  }
11662
11626
  const itemIdentifiers = new Set;
@@ -11710,19 +11674,7 @@ function isPlaycademyAssessmentResultMetadataV1(value) {
11710
11674
  const review = metadata2.review;
11711
11675
  const diagnostic = metadata2.diagnostic;
11712
11676
  const routedDiagnostic = metadata2.purpose === "diagnostic" && diagnostic !== undefined && Array.isArray(metadata2.itemSubmissions) && Number.isInteger(metadata2.responseVersion) && isRoutedDiagnosticAttemptMetadata(diagnostic, metadata2.itemSubmissions, metadata2.responseVersion);
11713
- return metadata2.version === 1 && typeof metadata2.activityId === "string" && isAssessmentPurpose(metadata2.purpose) && typeof metadata2.courseId === "string" && typeof metadata2.integrationId === "string" && typeof metadata2.enrollmentId === "string" && Boolean(selectedTest) && (selectedTest?.assessmentKey === undefined || isNonEmptyString(selectedTest.assessmentKey)) && typeof selectedTest?.identifier === "string" && typeof selectedTest.contentRevision === "string" && Number.isInteger(metadata2.attemptNumber) && Number.isInteger(metadata2.responseVersion) && Boolean(metadata2.responses) && typeof metadata2.responses === "object" && Array.isArray(metadata2.itemSubmissions) && metadata2.itemSubmissions.every(isAssessmentItemSubmission) && typeof metadata2.startedAt === "string" && typeof metadata2.updatedAt === "string" && (metadata2.finalizedAt === undefined || typeof metadata2.finalizedAt === "string") && isAssessmentAwardRecord(metadata2.award) && (metadata2.purpose === "mastery" ? isMasteryAttemptMetadata(mastery) : mastery === undefined) && (metadata2.purpose === "review" ? isReviewAttemptMetadata(review) : review === undefined) && (metadata2.purpose === "diagnostic" ? diagnostic === undefined || routedDiagnostic : diagnostic === undefined) && (metadata2.submissionId === undefined || typeof metadata2.submissionId === "string") && (metadata2.score === undefined || isAssessmentScore(metadata2.score)) && isCompletionMetadata(metadata2.completion, metadata2.purpose, routedDiagnostic);
11714
- }
11715
- function isPlaycademyReviewAssessmentItemResultMetadataV1(value) {
11716
- if (!isRecord(value) || !isRecord(value.responses)) {
11717
- return false;
11718
- }
11719
- return value.version === 1 && typeof value.parentAttemptId === "string" && typeof value.activityId === "string" && typeof value.integrationId === "string" && typeof value.enrollmentId === "string" && typeof value.bankRevision === "string" && typeof value.itemIdentifier === "string" && isAssessmentStandardRef(value.standard) && typeof value.deliveredAt === "string" && Number.isInteger(value.responseVersion) && Object.values(value.responses).every(isAssessmentResponseValue) && (value.submissionId === undefined || typeof value.submissionId === "string") && (value.isCorrect === undefined || value.isCorrect === null || typeof value.isCorrect === "boolean");
11720
- }
11721
- function isPlaycademyDiagnosticAssessmentItemResultMetadataV1(value) {
11722
- if (!isRecord(value) || !isRecord(value.responses)) {
11723
- return false;
11724
- }
11725
- return value.version === 1 && isNonEmptyString(value.parentAttemptId) && isNonEmptyString(value.activityId) && isNonEmptyString(value.integrationId) && isNonEmptyString(value.enrollmentId) && isNonEmptyString(value.definitionId) && isNonEmptyString(value.diagnosticKey) && isNonEmptyString(value.routingRevision) && isNonEmptyString(value.contentRevision) && isNonEmptyString(value.stageKey) && isNonEmptyString(value.trackKey) && isNonEmptyString(value.routingNodeKey) && isNonEmptyString(value.itemIdentifier) && isNonEmptyString(value.deliveredAt) && Number.isInteger(value.responseVersion) && Number(value.responseVersion) > 0 && Object.values(value.responses).every(isAssessmentResponseValue) && isNonEmptyString(value.submissionId) && typeof value.isCorrect === "boolean";
11677
+ return metadata2.version === 1 && typeof metadata2.activityId === "string" && isAssessmentPurpose(metadata2.purpose) && typeof metadata2.courseId === "string" && typeof metadata2.integrationId === "string" && typeof metadata2.enrollmentId === "string" && Boolean(selectedTest) && (selectedTest?.assessmentKey === undefined || isNonEmptyString(selectedTest.assessmentKey)) && typeof selectedTest?.identifier === "string" && typeof selectedTest.contentRevision === "string" && Number.isInteger(metadata2.attemptNumber) && Number.isInteger(metadata2.responseVersion) && Boolean(metadata2.responses) && typeof metadata2.responses === "object" && Array.isArray(metadata2.itemSubmissions) && metadata2.itemSubmissions.every(isAssessmentItemSubmission) && typeof metadata2.startedAt === "string" && typeof metadata2.updatedAt === "string" && (metadata2.finalizedAt === undefined || typeof metadata2.finalizedAt === "string") && isAssessmentAwardRecord(metadata2.award) && (metadata2.purpose === "mastery" ? isMasteryAttemptMetadata(mastery) : mastery === undefined) && (metadata2.purpose === "review" ? isReviewAttemptMetadata(review) : review === undefined) && (metadata2.purpose === "diagnostic" ? diagnostic === undefined || routedDiagnostic : diagnostic === undefined) && (metadata2.submissionId === undefined || typeof metadata2.submissionId === "string") && (metadata2.score === undefined || isAssessmentScore(metadata2.score)) && (metadata2.transcript === undefined || AssessmentTranscriptSchema.safeParse(metadata2.transcript).success) && isCompletionMetadata(metadata2.completion, metadata2.purpose, routedDiagnostic);
11726
11678
  }
11727
11679
  function playcademyAssessmentResultMetadata(value) {
11728
11680
  if (!isRecord(value)) {
@@ -11742,12 +11694,6 @@ function playcademyAssessmentResultMetadata(value) {
11742
11694
  diagnostic: { ...normalized.diagnostic, ledger: [] }
11743
11695
  };
11744
11696
  }
11745
- if (isRecord(normalized.diagnostic) && Array.isArray(normalized.diagnostic.ledger) && normalized.diagnostic.ledger.length === 0 && normalized.diagnostic.routingSnapshots === undefined) {
11746
- normalized = {
11747
- ...normalized,
11748
- diagnostic: { ...normalized.diagnostic, routingSnapshots: [] }
11749
- };
11750
- }
11751
11697
  if (isRecord(normalized.diagnostic) && isRecord(normalized.diagnostic.state)) {
11752
11698
  const state = normalized.diagnostic.state;
11753
11699
  const tracks = Array.isArray(state.tracks) ? state.tracks.map((track) => isRecord(track) ? {
@@ -11755,12 +11701,10 @@ function playcademyAssessmentResultMetadata(value) {
11755
11701
  currentNodeKey: track.currentNodeKey ?? null,
11756
11702
  terminal: track.terminal ?? null
11757
11703
  } : track) : state.tracks;
11758
- const routingSnapshots = Array.isArray(normalized.diagnostic.routingSnapshots) ? normalized.diagnostic.routingSnapshots.map((snapshot) => isRecord(snapshot) ? { ...snapshot, next: snapshot.next ?? null } : snapshot) : normalized.diagnostic.routingSnapshots;
11759
11704
  normalized = {
11760
11705
  ...normalized,
11761
11706
  diagnostic: {
11762
11707
  ...normalized.diagnostic,
11763
- ...routingSnapshots === undefined ? {} : { routingSnapshots },
11764
11708
  state: {
11765
11709
  ...state,
11766
11710
  ...tracks === undefined ? {} : { tracks },
@@ -11770,6 +11714,16 @@ function playcademyAssessmentResultMetadata(value) {
11770
11714
  };
11771
11715
  }
11772
11716
  const review = normalized.review;
11717
+ if (isRecord(normalized.transcript)) {
11718
+ const transcript = normalized.transcript;
11719
+ normalized = {
11720
+ ...normalized,
11721
+ transcript: {
11722
+ ...transcript,
11723
+ administrations: Array.isArray(transcript.administrations) ? transcript.administrations.map((entry) => isRecord(entry) ? { ...entry, responses: entry.responses ?? {} } : entry) : transcript.administrations ?? []
11724
+ }
11725
+ };
11726
+ }
11773
11727
  if (isRecord(review) && isRecord(review.fulfillment) && review.fulfillment.status === "complete" && review.fulfillment.shortages === undefined) {
11774
11728
  normalized = {
11775
11729
  ...normalized,
@@ -11784,29 +11738,7 @@ function playcademyAssessmentResultMetadata(value) {
11784
11738
  }
11785
11739
  return isPlaycademyAssessmentResultMetadataV1(normalized) ? normalized : null;
11786
11740
  }
11787
- function playcademyAssessmentItemResultMetadata(value) {
11788
- if (!isRecord(value)) {
11789
- return null;
11790
- }
11791
- const metadata2 = value[PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY];
11792
- if (!isRecord(metadata2)) {
11793
- return null;
11794
- }
11795
- const normalized = metadata2.responses === undefined ? { ...metadata2, responses: {} } : metadata2;
11796
- return isPlaycademyReviewAssessmentItemResultMetadataV1(normalized) ? normalized : null;
11797
- }
11798
- function playcademyDiagnosticAssessmentItemResultMetadata(value) {
11799
- if (!isRecord(value)) {
11800
- return null;
11801
- }
11802
- const metadata2 = value[PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY];
11803
- if (!isRecord(metadata2)) {
11804
- return null;
11805
- }
11806
- const normalized = metadata2.responses === undefined ? { ...metadata2, responses: {} } : metadata2;
11807
- return isPlaycademyDiagnosticAssessmentItemResultMetadataV1(normalized) ? normalized : null;
11808
- }
11809
- var SCRIPT_SCHEMES, DATA_RASTER_IMAGE_PATTERN, QTI_MATHML_ALLOWED_TAGS, metadataSymbol, parser, SUPPORTED_QTI_INTERACTION_TYPES, URL_ATTRIBUTES, IMAGE_URL_ATTRIBUTES, INTERACTION_TYPES, EMPHASIS_TAGS, STRONG_TAGS, CONTENT_SKIPPED_TAGS, BLOCK_CONTENT_KINDS, GAP_MATCH_TOKEN_TAGS, STAGE_GRAPHIC_EXCLUDED_CONTAINERS, BLANK_SENTINEL = "￿", MARKED_BLANK_PATTERN, MARKED_BLANK_GLOBAL_PATTERN, MATCH_CORRECT_TEMPLATE = "http://www.imsglobal.org/question/qti_v3p0/rptemplates/match_correct", MAP_RESPONSE_TEMPLATE = "https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/map_response", MAP_RESPONSE_TEMPLATES, RESPONSE_PROCESSING_TEMPLATES, VOID_BODY_ELEMENTS, URL_ATTRIBUTES2, IMAGE_URL_ATTRIBUTES2, VOID_CHOICE_ELEMENTS, NUMERIC_COMPARISON_ATTRIBUTES, RESPONSE_CARDINALITIES, RESPONSE_BASE_TYPES, POINT_INTERACTION_TYPES, INDEPENDENT_PROCESSING_TAGS, PRIVATE_PLAYABLE_KEYS, trimmedNonEmptyString, finiteNumber2, positiveFiniteNumber, IDENTIFIER_VALUE, DIRECTED_PAIR_VALUE, scoringMatchComparisonSchema, scoringNumericEqualComparisonSchema, scoringNumericEqualRoundedComparisonSchema, scoringRuleSchema, scoringRulesSchema, scoringItemSchema, assessmentScoringArtifactItemsSchema, assessmentScoringArtifactSchema, SUPPORTED_BASE_TYPES, SUPPORTED_CARDINALITIES, MATCH_CORRECT_TEMPLATES, UUID_PATTERN, COMMON_CORE_MATH_FRAMEWORK_ALIASES, COMMON_CORE_ELA_FRAMEWORK_ALIASES, COMMON_CORE_FRAMEWORK_ALIASES, ASSESSMENT_ATTEMPT_OPEN, ASSESSMENT_ATTEMPT_COMPLETED, ASSESSMENT_ATTEMPT_AWAITING_AWARD, ASSESSMENT_ATTEMPT_SUPERSEDED, ASSESSMENT_RUNTIME_ERROR_STATUS, ROUTING_KEY_MAX_LENGTH = 128, ROUTING_RESULT_KEY_MAX_LENGTH = 256, ROUTING_STAGE_LIMIT = 32, ROUTING_TRACK_LIMIT = 128, ROUTING_NODE_LIMIT = 1000, ROUTING_TERMINAL_LIMIT = 1000, ROUTING_GROUP_LIMIT = 128, ROUTING_PREDICATE_DEPTH_LIMIT = 8, ROUTING_EXPLORATION_STATE_LIMIT = 200000, RoutingKeySchema, RoutingResultKeySchema, DiagnosticTrackOutcomeSchema, DiagnosticActivationPredicateSchema, DiagnosticRoutingTransitionSchema, DiagnosticRoutingManifestV1Schema, GRADE_VALUES, POINT_RESPONSE_PATTERN, ZERO_WIDTH_CHARACTERS, CURLY_SINGLE_QUOTES, CURLY_DOUBLE_QUOTES, UNICODE_DASHES, ARITHMETIC_OPERATOR_SPACING, THOUSANDS_GROUP, REVIEW_SELECTION_POLICY_VERSION = "unseen-first-least-recently-delivered-v1", PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY = "playcademyReviewBank", PLAYCADEMY_REVIEW_BANK_MANIFEST_VERSION = 1, DEFAULT_REVIEW_SELECTION_POLICY, ASSESSMENT_RUNTIME_FIXTURE_BUNDLE_VERSION = 8, ASSESSMENT_CHILD_RESULT_KIND = "assessment-child-result", ASSESSMENT_CHILD_RESULT_VERSION = 1, ASSESSMENT_CHILD_RESULT_ID_NAMESPACE = "playcademy:assessment-child-result:v1", IdentitySchema, RecordedAtSchema, ResponseKeySchema, AssessmentResponseValueSchema, AssessmentScoreSchema, AssessmentStandardRefSchema, DiagnosticRoutingLedgerEntryV1Schema, AssessmentChildResultAttemptV1Schema, AssessmentChildResultAdministrationV1Schema, AssessmentChildResultGradingV1Schema, ReviewChildResultContextV1Schema, DiagnosticChildResultContextV1Schema, CommonPayloadShape, ReviewChildResultPayloadV1Schema, DiagnosticChildResultPayloadV1Schema, AssessmentChildResultPayloadV1Schema, AssessmentChildResultCommandV1Schema, RuntimeSubjectSchema, RuntimeGradeSchema, OptionalQueryGradeSchema, AssessmentResponseValueSchema2, AssessmentRuntimeIdentitySchema, ResponseKeySchema2, StartAssessmentBaseSchema, AssessmentStandardRefSchema2, LatestAssessmentFilterBaseSchema, LatestAssessmentFiltersSchema, LatestRuntimeAssessmentQuerySchema, StartAssessmentBodySchema, AssessmentPreparationReceiptSchema, StartAssessmentRequestBodySchema, SaveAssessmentBodySchema, SubmitAssessmentItemBodySchema, SubmitAssessmentBodySchema, FinalizeAssessmentBodySchema, PrepareRuntimeAssessmentRequestSchema, StartRuntimeAssessmentRequestSchema, SaveRuntimeAssessmentRequestSchema, SubmitAssessmentItemRuntimeRequestSchema, SubmitRuntimeAssessmentRequestSchema, FinalizeRuntimeAssessmentRequestSchema;
11741
+ var SCRIPT_SCHEMES, DATA_RASTER_IMAGE_PATTERN, QTI_MATHML_ALLOWED_TAGS, metadataSymbol, parser, SUPPORTED_QTI_INTERACTION_TYPES, URL_ATTRIBUTES, IMAGE_URL_ATTRIBUTES, INTERACTION_TYPES, EMPHASIS_TAGS, STRONG_TAGS, CONTENT_SKIPPED_TAGS, BLOCK_CONTENT_KINDS, GAP_MATCH_TOKEN_TAGS, STAGE_GRAPHIC_EXCLUDED_CONTAINERS, BLANK_SENTINEL = "￿", MARKED_BLANK_PATTERN, MARKED_BLANK_GLOBAL_PATTERN, MATCH_CORRECT_TEMPLATE = "http://www.imsglobal.org/question/qti_v3p0/rptemplates/match_correct", MAP_RESPONSE_TEMPLATE = "https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/map_response", MAP_RESPONSE_TEMPLATES, RESPONSE_PROCESSING_TEMPLATES, VOID_BODY_ELEMENTS, URL_ATTRIBUTES2, IMAGE_URL_ATTRIBUTES2, VOID_CHOICE_ELEMENTS, NUMERIC_COMPARISON_ATTRIBUTES, RESPONSE_CARDINALITIES, RESPONSE_BASE_TYPES, POINT_INTERACTION_TYPES, INDEPENDENT_PROCESSING_TAGS, PRIVATE_PLAYABLE_KEYS, trimmedNonEmptyString, finiteNumber2, positiveFiniteNumber, IDENTIFIER_VALUE, DIRECTED_PAIR_VALUE, scoringMatchComparisonSchema, scoringNumericEqualComparisonSchema, scoringNumericEqualRoundedComparisonSchema, scoringRuleSchema, scoringRulesSchema, scoringItemSchema, assessmentScoringArtifactItemsSchema, assessmentScoringArtifactSchema, SUPPORTED_BASE_TYPES, SUPPORTED_CARDINALITIES, MATCH_CORRECT_TEMPLATES, UUID_PATTERN, COMMON_CORE_MATH_FRAMEWORK_ALIASES, COMMON_CORE_ELA_FRAMEWORK_ALIASES, COMMON_CORE_FRAMEWORK_ALIASES, ASSESSMENT_ATTEMPT_OPEN, ASSESSMENT_ATTEMPT_COMPLETED, ASSESSMENT_ATTEMPT_AWAITING_AWARD, ASSESSMENT_ATTEMPT_SUPERSEDED, ASSESSMENT_RUNTIME_ERROR_STATUS, GRADE_VALUES, ROUTING_KEY_MAX_LENGTH = 128, ROUTING_RESULT_KEY_MAX_LENGTH = 256, ROUTING_STAGE_LIMIT = 32, ROUTING_TRACK_LIMIT = 128, ROUTING_NODE_LIMIT = 1000, ROUTING_TERMINAL_LIMIT = 1000, ROUTING_GROUP_LIMIT = 128, ROUTING_PREDICATE_DEPTH_LIMIT = 8, ROUTING_EXPLORATION_STATE_LIMIT = 200000, RoutingKeySchema, RoutingResultKeySchema, DiagnosticTrackOutcomeSchema, DiagnosticActivationPredicateSchema, DiagnosticRoutingTransitionSchema, DiagnosticRoutingManifestV1Schema, POINT_RESPONSE_PATTERN, Identity, AssessmentTranscriptSchema, AssessmentTranscriptError, ZERO_WIDTH_CHARACTERS, CURLY_SINGLE_QUOTES, CURLY_DOUBLE_QUOTES, UNICODE_DASHES, ARITHMETIC_OPERATOR_SPACING, THOUSANDS_GROUP, REVIEW_SELECTION_POLICY_VERSION = "unseen-first-least-recently-delivered-v1", PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY = "playcademyReviewBank", PLAYCADEMY_REVIEW_BANK_MANIFEST_VERSION = 1, DEFAULT_REVIEW_SELECTION_POLICY, ASSESSMENT_RUNTIME_FIXTURE_BUNDLE_VERSION = 8, ASSESSMENT_CHILD_RESULT_KIND = "assessment-child-result", ASSESSMENT_CHILD_RESULT_VERSION = 1, ASSESSMENT_CHILD_RESULT_ID_NAMESPACE = "playcademy:assessment-child-result:v1", IdentitySchema, RecordedAtSchema, ResponseKeySchema, AssessmentResponseValueSchema, AssessmentScoreSchema, AssessmentStandardRefSchema, DiagnosticRoutingLedgerEntryV1Schema, AssessmentChildResultAttemptV1Schema, AssessmentChildResultAdministrationV1Schema, AssessmentChildResultGradingV1Schema, ReviewChildResultContextV1Schema, FixedChildResultContextV1Schema, DiagnosticChildResultContextV1Schema, CommonPayloadShape, ReviewChildResultPayloadV1Schema, FixedChildResultPayloadV1Schema, DiagnosticChildResultPayloadV1Schema, AssessmentChildResultPayloadV1Schema, AssessmentChildResultCommandV1Schema, RuntimeSubjectSchema, RuntimeGradeSchema, OptionalQueryGradeSchema, AssessmentResponseValueSchema2, AssessmentRuntimeIdentitySchema, ResponseKeySchema2, StartAssessmentBaseSchema, AssessmentStandardRefSchema2, LatestAssessmentFilterBaseSchema, LatestAssessmentFiltersSchema, LatestRuntimeAssessmentQuerySchema, StartAssessmentBodySchema, AssessmentPreparationReceiptSchema, StartAssessmentRequestBodySchema, SaveAssessmentBodySchema, RecordAssessmentProgressBodySchema, ResumeAssessmentBodySchema, ResumeRuntimeAssessmentRequestSchema, SubmitAssessmentItemBodySchema, SubmitAssessmentBodySchema, FinalizeAssessmentBodySchema, PrepareRuntimeAssessmentRequestSchema, StartRuntimeAssessmentRequestSchema, SaveRuntimeAssessmentRequestSchema, RecordRuntimeAssessmentProgressRequestSchema, SubmitAssessmentItemRuntimeRequestSchema, SubmitRuntimeAssessmentRequestSchema, FinalizeRuntimeAssessmentRequestSchema;
11810
11742
  var init_assessment_runtime2 = __esm(() => {
11811
11743
  init_timeback3();
11812
11744
  init_timeback3();
@@ -11816,13 +11748,14 @@ var init_assessment_runtime2 = __esm(() => {
11816
11748
  init_timeback3();
11817
11749
  init_esm();
11818
11750
  init_esm();
11751
+ init_src();
11752
+ init_uuid();
11753
+ init_esm();
11819
11754
  init_timeback4();
11820
11755
  init_src();
11821
11756
  init_timeback3();
11822
11757
  init_src();
11823
11758
  init_uuid();
11824
- init_src();
11825
- init_uuid();
11826
11759
  init_esm();
11827
11760
  init_src();
11828
11761
  init_uuid();
@@ -12118,6 +12051,7 @@ var init_assessment_runtime2 = __esm(() => {
12118
12051
  ATTEMPT_ALREADY_SUBMITTED: 409,
12119
12052
  ASSESSMENT_FLOW_VIOLATION: 409
12120
12053
  };
12054
+ GRADE_VALUES = TIMEBACK_GRADES;
12121
12055
  RoutingKeySchema = exports_external.string().trim().min(1).max(ROUTING_KEY_MAX_LENGTH);
12122
12056
  RoutingResultKeySchema = exports_external.string().trim().min(1).max(ROUTING_RESULT_KEY_MAX_LENGTH);
12123
12057
  DiagnosticTrackOutcomeSchema = exports_external.enum(DIAGNOSTIC_TRACK_OUTCOMES);
@@ -12175,8 +12109,31 @@ var init_assessment_runtime2 = __esm(() => {
12175
12109
  resultKey: RoutingResultKeySchema
12176
12110
  }).strict()).min(1).max(ROUTING_TERMINAL_LIMIT)
12177
12111
  }).strict();
12178
- GRADE_VALUES = TIMEBACK_GRADES;
12179
12112
  POINT_RESPONSE_PATTERN = /^-?\d+(?:\.\d+)? -?\d+(?:\.\d+)?$/;
12113
+ Identity = exports_external.string().min(1).max(512).refine((value) => value.trim() === value);
12114
+ AssessmentTranscriptSchema = exports_external.object({
12115
+ version: exports_external.literal(1),
12116
+ attemptId: Identity,
12117
+ contentRevision: Identity,
12118
+ administrations: exports_external.array(exports_external.object({
12119
+ submissionId: Identity,
12120
+ itemIdentifier: Identity,
12121
+ routingNodeKey: Identity.optional(),
12122
+ responses: exports_external.record(Identity, exports_external.custom(isAssessmentResponseValue))
12123
+ }).strict()).max(1000)
12124
+ }).strict().refine((value) => new TextEncoder().encode(JSON.stringify(value)).length <= 256000, {
12125
+ message: "Assessment transcript exceeds 256 KB"
12126
+ });
12127
+ AssessmentTranscriptError = class AssessmentTranscriptError extends Error {
12128
+ index;
12129
+ code;
12130
+ constructor(message, index = 0, code = "ASSESSMENT_FLOW_VIOLATION") {
12131
+ super(message);
12132
+ this.name = "AssessmentTranscriptError";
12133
+ this.index = index;
12134
+ this.code = code;
12135
+ }
12136
+ };
12180
12137
  ZERO_WIDTH_CHARACTERS = /[\u200B\uFEFF\u200C\u200D]/g;
12181
12138
  CURLY_SINGLE_QUOTES = /[\u2018\u2019\u201A\u201B]/g;
12182
12139
  CURLY_DOUBLE_QUOTES = /[\u201C\u201D\u201E\u201F]/g;
@@ -12268,6 +12225,10 @@ var init_assessment_runtime2 = __esm(() => {
12268
12225
  bankRevision: IdentitySchema,
12269
12226
  standard: AssessmentStandardRefSchema
12270
12227
  }).strict();
12228
+ FixedChildResultContextV1Schema = exports_external.object({
12229
+ kind: exports_external.literal("fixed"),
12230
+ purpose: exports_external.enum(["mastery", "end_of_course", "diagnostic"])
12231
+ }).strict();
12271
12232
  DiagnosticChildResultContextV1Schema = exports_external.object({
12272
12233
  kind: exports_external.literal("platform-routed-diagnostic"),
12273
12234
  definitionId: IdentitySchema,
@@ -12287,6 +12248,11 @@ var init_assessment_runtime2 = __esm(() => {
12287
12248
  isCorrect: exports_external.boolean().optional(),
12288
12249
  context: ReviewChildResultContextV1Schema
12289
12250
  }).strict();
12251
+ FixedChildResultPayloadV1Schema = exports_external.object({
12252
+ ...CommonPayloadShape,
12253
+ isCorrect: exports_external.boolean().optional(),
12254
+ context: FixedChildResultContextV1Schema
12255
+ }).strict();
12290
12256
  DiagnosticChildResultPayloadV1Schema = exports_external.object({
12291
12257
  ...CommonPayloadShape,
12292
12258
  isCorrect: exports_external.boolean(),
@@ -12302,7 +12268,8 @@ var init_assessment_runtime2 = __esm(() => {
12302
12268
  });
12303
12269
  AssessmentChildResultPayloadV1Schema = exports_external.union([
12304
12270
  ReviewChildResultPayloadV1Schema,
12305
- DiagnosticChildResultPayloadV1Schema
12271
+ DiagnosticChildResultPayloadV1Schema,
12272
+ FixedChildResultPayloadV1Schema
12306
12273
  ]);
12307
12274
  AssessmentChildResultCommandV1Schema = exports_external.object({
12308
12275
  kind: exports_external.literal(ASSESSMENT_CHILD_RESULT_KIND),
@@ -12379,9 +12346,19 @@ var init_assessment_runtime2 = __esm(() => {
12379
12346
  StartAssessmentRequestBodySchema = exports_external.intersection(StartAssessmentBodySchema, exports_external.object({ preparationReceipt: AssessmentPreparationReceiptSchema.optional() }));
12380
12347
  SaveAssessmentBodySchema = exports_external.object({
12381
12348
  expectedResponseVersion: exports_external.number().int().nonnegative(),
12382
- responses: exports_external.record(ResponseKeySchema2, exports_external.record(ResponseKeySchema2, AssessmentResponseValueSchema2.nullable()))
12349
+ transcript: AssessmentTranscriptSchema
12350
+ });
12351
+ RecordAssessmentProgressBodySchema = SaveAssessmentBodySchema.extend({
12352
+ assessmentToken: exports_external.string().min(1).max(32000),
12353
+ itemIdentifier: ResponseKeySchema2
12383
12354
  });
12355
+ ResumeAssessmentBodySchema = exports_external.object({
12356
+ transcript: AssessmentTranscriptSchema.optional()
12357
+ });
12358
+ ResumeRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.extend(ResumeAssessmentBodySchema.shape);
12384
12359
  SubmitAssessmentItemBodySchema = exports_external.object({
12360
+ assessmentToken: exports_external.string().min(1).max(32000),
12361
+ transcript: AssessmentTranscriptSchema,
12385
12362
  expectedResponseVersion: exports_external.number().int().nonnegative(),
12386
12363
  submissionId: exports_external.string().trim().min(1),
12387
12364
  itemIdentifier: ResponseKeySchema2,
@@ -12389,6 +12366,7 @@ var init_assessment_runtime2 = __esm(() => {
12389
12366
  responses: exports_external.record(ResponseKeySchema2, AssessmentResponseValueSchema2.nullable())
12390
12367
  });
12391
12368
  SubmitAssessmentBodySchema = exports_external.object({
12369
+ transcript: AssessmentTranscriptSchema,
12392
12370
  expectedResponseVersion: exports_external.number().int().nonnegative(),
12393
12371
  submissionId: exports_external.string().trim().min(1),
12394
12372
  session: exports_external.custom(isAssessmentSessionTiming, {
@@ -12407,6 +12385,7 @@ var init_assessment_runtime2 = __esm(() => {
12407
12385
  PrepareRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.and(StartAssessmentBodySchema);
12408
12386
  StartRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.and(StartAssessmentRequestBodySchema);
12409
12387
  SaveRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.extend(SaveAssessmentBodySchema.shape);
12388
+ RecordRuntimeAssessmentProgressRequestSchema = AssessmentRuntimeIdentitySchema.extend(RecordAssessmentProgressBodySchema.shape);
12410
12389
  SubmitAssessmentItemRuntimeRequestSchema = AssessmentRuntimeIdentitySchema.extend(SubmitAssessmentItemBodySchema.shape);
12411
12390
  SubmitRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.extend(SubmitAssessmentBodySchema.shape).extend({
12412
12391
  appName: exports_external.string().trim().min(1),
@@ -100206,22 +100185,6 @@ var init_locks = __esm(() => {
100206
100185
  });
100207
100186
 
100208
100187
  // ../api-core/src/utils/assessment-runtime-lock.util.ts
100209
- async function crossAssessmentAttemptLockBarrier(lock, db2, attemptId) {
100210
- const maxAttempts = 5;
100211
- for (let attempt = 1;attempt <= maxAttempts; attempt += 1) {
100212
- try {
100213
- await lock(db2, [assessmentRuntimeAttemptLockKey(attemptId)], async () => {
100214
- return;
100215
- });
100216
- return;
100217
- } catch (error88) {
100218
- if (!(error88 instanceof ServiceUnavailableError) || attempt === maxAttempts) {
100219
- throw error88;
100220
- }
100221
- await sleep(25);
100222
- }
100223
- }
100224
- }
100225
100188
  function normalizeKeys(keys) {
100226
100189
  return [...new Set(keys)].toSorted();
100227
100190
  }
@@ -100294,17 +100257,6 @@ var init_assessment_runtime_lock_util = __esm(() => {
100294
100257
  });
100295
100258
 
100296
100259
  // ../api-core/src/utils/timeback-assessment-runtime.util.ts
100297
- function assessmentPreparationContentionError(expectedResponseVersion, responseVersion, kind) {
100298
- if (expectedResponseVersion === responseVersion) {
100299
- return new ServiceUnavailableError(`The ${kind} response state changed repeatedly while this item was being prepared. Try again shortly.`, {
100300
- retryable: true,
100301
- reason: "PREPARATION_CONTENTION",
100302
- expectedResponseVersion,
100303
- responseVersion
100304
- });
100305
- }
100306
- return AssessmentRuntimeError.from(assessmentResponseVersionConflict(expectedResponseVersion, responseVersion));
100307
- }
100308
100260
  function assessmentTrackableCourse(integration) {
100309
100261
  if (!isTimebackGrade2(integration.grade) || !isTimebackSubject(integration.subject)) {
100310
100262
  return null;
@@ -100370,7 +100322,7 @@ function buildConventionalAssessmentSnapshot(input) {
100370
100322
  ...input.activityData ? { activityData: input.activityData } : {},
100371
100323
  assessment: assessmentPresentationForAttempt(input.assessment, input.attemptId),
100372
100324
  responses: metadata2.responses,
100373
- itemSubmissions: metadata2.itemSubmissions,
100325
+ itemSubmissions: assessmentFlowForPurpose(metadata2.purpose) === "item-submit" ? metadata2.itemSubmissions : [],
100374
100326
  score: null,
100375
100327
  selection
100376
100328
  };
@@ -100385,13 +100337,6 @@ function buildAssessmentAwardRecord(input, mastery) {
100385
100337
  ...mastery.warnings ? { warnings: mastery.warnings } : {}
100386
100338
  };
100387
100339
  }
100388
- function stageAssessmentAttemptSupersession(attempt) {
100389
- Object.assign(attempt.result, ASSESSMENT_ATTEMPT_SUPERSEDED);
100390
- Object.assign(attempt.selection, ASSESSMENT_ATTEMPT_SUPERSEDED);
100391
- }
100392
- function shouldConfirmAssessmentVersionUnavailable(details) {
100393
- return typeof details === "object" && details !== null && "expectedRevision" in details && typeof details.expectedRevision === "string" && !("currentRevision" in details);
100394
- }
100395
100340
  function buildAssessmentAttemptSupersessionUpdate(result) {
100396
100341
  if (result.status !== "active" || !isAssessmentAttemptOpen({
100397
100342
  inProgress: result.inProgress ?? "",
@@ -100409,162 +100354,9 @@ function buildAssessmentAttemptSupersessionUpdate(result) {
100409
100354
  metadata: result.metadata ?? {}
100410
100355
  };
100411
100356
  }
100412
- async function resolveAvailableAssessmentAttempt(initialDecision, select3, resume) {
100413
- let decision = initialDecision;
100414
- while (decision?.kind === "resume") {
100415
- const snapshot = await resume(decision);
100416
- if (snapshot !== null) {
100417
- return { kind: "resumed", snapshot };
100418
- }
100419
- decision = select3();
100420
- }
100421
- return { kind: "continue", decision };
100422
- }
100423
- function buildReviewItemResultMetadata(input) {
100424
- const administration = itemAdministration(input.metadata.itemSubmissions, input.selection.itemIdentifier);
100425
- if (!administration) {
100426
- throw new Error(`Cannot create a review result for unadministered candidate ${input.selection.itemIdentifier}`);
100427
- }
100428
- return {
100429
- version: PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_VERSION,
100430
- parentAttemptId: input.parentAttemptId,
100431
- activityId: input.metadata.activityId,
100432
- integrationId: input.metadata.integrationId,
100433
- enrollmentId: input.metadata.enrollmentId,
100434
- bankRevision: input.metadata.review.bankRevision,
100435
- itemIdentifier: input.selection.itemIdentifier,
100436
- standard: input.selection.standard,
100437
- deliveredAt: administration.submittedAt,
100438
- responseVersion: input.responseVersion ?? input.metadata.responseVersion,
100439
- responses: input.metadata.responses[input.selection.itemIdentifier] ?? {},
100440
- ...input.submissionId ? { submissionId: input.submissionId } : {},
100441
- ...input.isCorrect === undefined ? {} : { isCorrect: input.isCorrect }
100442
- };
100443
- }
100444
- function buildOpenReviewChildResult(input) {
100445
- return {
100446
- status: "active",
100447
- assessmentLineItem: { sourcedId: input.childLineItemId },
100448
- student: input.student,
100449
- score: 0,
100450
- scoreDate: input.metadata.deliveredAt,
100451
- ...ASSESSMENT_ATTEMPT_OPEN,
100452
- metadata: {
100453
- [PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY]: input.metadata
100454
- }
100455
- };
100456
- }
100457
100357
  function diagnosticQuestionLineItemId(parentLineItemId, itemIdentifier) {
100458
100358
  return deterministicUUID(["playcademy:diagnostic-question-line-item:v1", parentLineItemId, itemIdentifier].join("\x00"));
100459
100359
  }
100460
- function diagnosticQuestionResultId(parentAttemptId, childLineItemId) {
100461
- return deterministicUUID(["playcademy:diagnostic-question-result:v1", parentAttemptId, childLineItemId].join("\x00"));
100462
- }
100463
- function diagnosticAdministration(input) {
100464
- const ledgerIndex = input.metadata.diagnostic.ledger.findIndex((entry) => entry.routingNodeKey === input.ledgerEntry.routingNodeKey);
100465
- const persistedEntry = input.metadata.diagnostic.ledger[ledgerIndex];
100466
- const administration = input.metadata.itemSubmissions[ledgerIndex];
100467
- if (ledgerIndex === -1 || !persistedEntry || !administration || persistedEntry.stageKey !== input.ledgerEntry.stageKey || persistedEntry.trackKey !== input.ledgerEntry.trackKey || persistedEntry.itemIdentifier !== input.ledgerEntry.itemIdentifier || persistedEntry.isCorrect !== input.ledgerEntry.isCorrect || administration.itemIdentifier !== input.ledgerEntry.itemIdentifier || administration.isCorrect !== input.ledgerEntry.isCorrect) {
100468
- throw new Error(`Cannot create a diagnostic result for uncommitted routing node ${input.ledgerEntry.routingNodeKey}`);
100469
- }
100470
- return administration;
100471
- }
100472
- function buildDiagnosticItemResultMetadata(input) {
100473
- const administration = diagnosticAdministration(input);
100474
- return {
100475
- version: PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_VERSION,
100476
- parentAttemptId: input.parentAttemptId,
100477
- activityId: input.metadata.activityId,
100478
- integrationId: input.metadata.integrationId,
100479
- enrollmentId: input.metadata.enrollmentId,
100480
- definitionId: input.metadata.diagnostic.definitionId,
100481
- diagnosticKey: input.metadata.diagnostic.diagnosticKey,
100482
- routingRevision: input.metadata.diagnostic.routingRevision,
100483
- contentRevision: input.metadata.selectedTest.contentRevision,
100484
- stageKey: input.ledgerEntry.stageKey,
100485
- trackKey: input.ledgerEntry.trackKey,
100486
- routingNodeKey: input.ledgerEntry.routingNodeKey,
100487
- itemIdentifier: input.ledgerEntry.itemIdentifier,
100488
- deliveredAt: administration.submittedAt,
100489
- responseVersion: administration.responseVersion,
100490
- responses: input.metadata.responses[input.ledgerEntry.itemIdentifier] ?? {},
100491
- submissionId: administration.submissionId,
100492
- isCorrect: input.ledgerEntry.isCorrect
100493
- };
100494
- }
100495
- function resolveDiagnosticChildResult(input) {
100496
- if (!input.childResult) {
100497
- return { action: "create" };
100498
- }
100499
- const childMetadata = playcademyDiagnosticAssessmentItemResultMetadata(input.childResult.metadata);
100500
- const expectedMetadata = buildDiagnosticItemResultMetadata({
100501
- metadata: input.parentMetadata,
100502
- ledgerEntry: input.ledgerEntry,
100503
- parentAttemptId: input.parentResult.sourcedId
100504
- });
100505
- if (input.childResult.assessmentLineItem.sourcedId !== input.childLineItemId || input.childResult.student.sourcedId !== input.parentResult.student.sourcedId || !childMetadata || canonicalJson2(childMetadata) !== canonicalJson2(expectedMetadata)) {
100506
- return { action: "restore", reason: "incompatible" };
100507
- }
100508
- return input.childResult.status === "active" ? { action: "reuse", metadata: expectedMetadata } : { action: "restore", reason: "inactive" };
100509
- }
100510
- function buildFinalizedDiagnosticChildResult(input) {
100511
- const administration = diagnosticAdministration(input);
100512
- const metadata2 = buildDiagnosticItemResultMetadata(input);
100513
- return {
100514
- resultUpdate: {
100515
- status: "active",
100516
- assessmentLineItem: { sourcedId: input.childLineItemId },
100517
- student: input.student,
100518
- score: administration.score.earned,
100519
- scoreDate: administration.submittedAt,
100520
- ...ASSESSMENT_ATTEMPT_COMPLETED,
100521
- metadata: {
100522
- [PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY]: metadata2
100523
- }
100524
- }
100525
- };
100526
- }
100527
- function buildFinalizedReviewChildResult(input) {
100528
- const metadata2 = buildReviewItemResultMetadata({
100529
- metadata: input.metadata,
100530
- selection: input.selection,
100531
- parentAttemptId: input.parentAttemptId,
100532
- submissionId: input.submissionId,
100533
- isCorrect: input.itemResult.isCorrect
100534
- });
100535
- return {
100536
- resultUpdate: {
100537
- status: "active",
100538
- assessmentLineItem: { sourcedId: input.childLineItemId },
100539
- student: input.student,
100540
- score: input.itemResult.score.earned,
100541
- scoreDate: input.timestamp,
100542
- ...ASSESSMENT_ATTEMPT_COMPLETED,
100543
- metadata: {
100544
- [PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY]: metadata2
100545
- }
100546
- },
100547
- outcome: {
100548
- itemIdentifier: input.selection.itemIdentifier,
100549
- standard: input.selection.standard,
100550
- answered: Object.keys(input.metadata.responses[input.selection.itemIdentifier] ?? {}).length > 0,
100551
- score: input.itemResult.score,
100552
- isCorrect: input.itemResult.isCorrect
100553
- }
100554
- };
100555
- }
100556
- function resolveReviewChildResult(input) {
100557
- if (!input.childResult) {
100558
- return { action: "create" };
100559
- }
100560
- const childMetadata = playcademyAssessmentItemResultMetadata(input.childResult.metadata);
100561
- const expectedResponses = input.parentMetadata.responses[input.selection.itemIdentifier] ?? {};
100562
- const administration = itemAdministration(input.parentMetadata.itemSubmissions, input.selection.itemIdentifier);
100563
- if (input.childResult.assessmentLineItem.sourcedId !== input.childLineItemId || input.childResult.student.sourcedId !== input.parentResult.student.sourcedId || !childMetadata || childMetadata.parentAttemptId !== input.parentResult.sourcedId || childMetadata.activityId !== input.parentMetadata.activityId || childMetadata.integrationId !== input.parentMetadata.integrationId || childMetadata.enrollmentId !== input.parentMetadata.enrollmentId || childMetadata.bankRevision !== input.parentMetadata.review.bankRevision || childMetadata.itemIdentifier !== input.selection.itemIdentifier || childMetadata.standard.framework !== input.selection.standard.framework || childMetadata.standard.identifier !== input.selection.standard.identifier || childMetadata.deliveredAt !== administration?.submittedAt || childMetadata.responseVersion > input.parentMetadata.responseVersion || canonicalJson2(childMetadata.responses) !== canonicalJson2(expectedResponses)) {
100564
- return { action: "restore", reason: "incompatible" };
100565
- }
100566
- return input.childResult.status === "active" ? { action: "reuse", metadata: childMetadata } : { action: "restore", reason: "inactive" };
100567
- }
100568
100360
  function buildAssessmentSubmitResult(input) {
100569
100361
  if (!input.metadata.score || !input.metadata.submissionId) {
100570
100362
  return null;
@@ -100950,32 +100742,12 @@ function assessmentFixtureScoringKeys(assessment, questions) {
100950
100742
  ...Object.keys(responseAreas).length > 0 ? { responseAreas } : {}
100951
100743
  };
100952
100744
  }
100953
- function prepareDiagnosticAssessmentResponses(assessment, current, input) {
100954
- const update2 = { [input.itemIdentifier]: input.responses };
100955
- validateAssessmentResponseUpdate(assessment, update2);
100956
- const responses = applyAssessmentResponseUpdate(current, update2);
100957
- const itemResponses = responses[input.itemIdentifier];
100958
- validateAssessmentDraftResponses(assessment, itemResponses ? { [input.itemIdentifier]: itemResponses } : {});
100959
- return responses;
100960
- }
100961
100745
  function validateAssessmentResponseUpdate(assessment, update2) {
100962
100746
  const message = assessmentResponseUpdateValidationMessage(assessment, update2);
100963
100747
  if (message) {
100964
100748
  throw new AssessmentRuntimeError("INVALID_RESPONSE", message);
100965
100749
  }
100966
100750
  }
100967
- function validateAssessmentResponses(assessment, responses) {
100968
- const message = assessmentResponseValidationMessage(assessment, responses);
100969
- if (message) {
100970
- throw new AssessmentRuntimeError("INVALID_RESPONSE", message);
100971
- }
100972
- }
100973
- function validateAssessmentDraftResponses(assessment, responses) {
100974
- const message = assessmentDraftResponseValidationMessage(assessment, responses);
100975
- if (message) {
100976
- throw new AssessmentRuntimeError("INVALID_RESPONSE", message);
100977
- }
100978
- }
100979
100751
  function assessmentScoringRequests(assessment, responses) {
100980
100752
  return assessment.items.map((item) => ({
100981
100753
  itemIdentifier: item.identifier,
@@ -101015,6 +100787,21 @@ function assessmentScoreFromPercentage(percentage, possible) {
101015
100787
  const earned = Math.round(normalized * possible * 1e6) / 1e6;
101016
100788
  return { earned, possible, normalized };
101017
100789
  }
100790
+ function buildAssessmentResultCheckpoint(input) {
100791
+ return {
100792
+ status: input.result.status,
100793
+ assessmentLineItem: input.result.assessmentLineItem,
100794
+ student: input.result.student,
100795
+ score: input.result.score ?? 0,
100796
+ scoreDate: input.result.scoreDate,
100797
+ ...ASSESSMENT_ATTEMPT_OPEN,
100798
+ metadata: mergeAssessmentRuntimeMetadata(input.result.metadata, {
100799
+ ...input.metadata,
100800
+ transcript: input.transcript,
100801
+ updatedAt: input.timestamp
100802
+ })
100803
+ };
100804
+ }
101018
100805
  function buildAssessmentResultSubmission(input) {
101019
100806
  const metadata2 = {
101020
100807
  ...input.metadata,
@@ -102419,6 +102206,183 @@ var init_timeback_assessment_artifacts_util = __esm(() => {
102419
102206
  init_timeback_review_mapping_util();
102420
102207
  });
102421
102208
 
102209
+ // ../api-core/src/utils/timeback-assessment-scoring.util.ts
102210
+ async function scoreCachedAssessmentItems(input) {
102211
+ const selected = input.assessment.items.filter((item) => input.itemIdentifiers.has(item.identifier));
102212
+ const keys = new Map(await Promise.all(selected.map(async (item) => [
102213
+ item.identifier,
102214
+ await sha256Hex(canonicalJson2({
102215
+ identity: input.artifactIdentity,
102216
+ purpose: input.purpose,
102217
+ itemIdentifier: item.identifier,
102218
+ responses: input.responses[item.identifier] ?? {},
102219
+ strategy: input.artifact?.items.find((candidate) => candidate.itemIdentifier === item.identifier)?.strategy ?? "qti"
102220
+ }))
102221
+ ])));
102222
+ const scored = new Map;
102223
+ for (const item of selected) {
102224
+ const cached3 = input.cache.get(keys.get(item.identifier));
102225
+ if (cached3) {
102226
+ scored.set(item.identifier, cached3);
102227
+ }
102228
+ }
102229
+ const missing = new Set(selected.filter((item) => !scored.has(item.identifier)).map((item) => item.identifier));
102230
+ const fresh = await scoreAssessmentItems({ ...input, itemIdentifiers: missing });
102231
+ for (const result of fresh) {
102232
+ input.cache.set(keys.get(result.itemIdentifier), result);
102233
+ scored.set(result.itemIdentifier, result);
102234
+ }
102235
+ return selected.map((item) => scored.get(item.identifier));
102236
+ }
102237
+ async function scoreAssessmentItems({
102238
+ assessment,
102239
+ responses,
102240
+ artifactIdentity,
102241
+ purpose,
102242
+ itemIdentifiers,
102243
+ artifact,
102244
+ processResponse,
102245
+ onEvent
102246
+ }) {
102247
+ const scoringItems = assessmentScoringRequests(assessment, responses).filter((item) => !itemIdentifiers || itemIdentifiers.has(item.itemIdentifier));
102248
+ const artifactItems = new Map(artifact?.items.map((item) => [item.itemIdentifier, item]));
102249
+ const scored = new Map;
102250
+ const qti2 = [];
102251
+ for (const item of scoringItems) {
102252
+ const artifactItem = artifactItems.get(item.itemIdentifier);
102253
+ let fallback2 = null;
102254
+ if (!artifact) {
102255
+ fallback2 = { reason: "artifact-unavailable" };
102256
+ } else if (!artifactItem) {
102257
+ fallback2 = { reason: "artifact-item-missing" };
102258
+ } else if (artifactItem.strategy === "qti") {
102259
+ fallback2 = { reason: "artifact-qti", artifactReason: artifactItem.reason };
102260
+ } else {
102261
+ const startedAt = Date.now();
102262
+ try {
102263
+ scored.set(item.itemIdentifier, {
102264
+ ...scorePlatformAssessmentItem(artifactItem, responses[item.itemIdentifier]),
102265
+ grading: PLATFORM_CHILD_GRADING
102266
+ });
102267
+ onEvent("assessment.item_scoring", {
102268
+ "app.assessment.qti_item_identifier": item.itemIdentifier,
102269
+ "app.assessment.purpose": purpose,
102270
+ "app.assessment.artifact_identity_kind": artifactIdentity.kind,
102271
+ "app.assessment.grading_source": "platform-artifact",
102272
+ "app.assessment.grading_duration_ms": Date.now() - startedAt
102273
+ });
102274
+ } catch (error88) {
102275
+ fallback2 = { reason: "platform-grader-error", error: error88 };
102276
+ }
102277
+ }
102278
+ if (fallback2) {
102279
+ qti2.push({ item, ...fallback2 });
102280
+ }
102281
+ }
102282
+ if (qti2.length > 0) {
102283
+ const fallbackTasks = qti2.flatMap(({ item, reason, artifactReason, error: graderError }) => {
102284
+ const interactionResults = [];
102285
+ let startedAt;
102286
+ let completedInteractions = 0;
102287
+ let failed = false;
102288
+ const omittedResponseCount = item.requests.filter((request) => responses[item.itemIdentifier]?.[request.identifier] === undefined).length;
102289
+ let qtiRemoteInteractionCount = 0;
102290
+ let syntheticIncorrectInteractionCount = 0;
102291
+ function fallbackEvent(outcome) {
102292
+ onEvent("assessment.item_scoring_fallback", {
102293
+ "app.assessment.qti_item_identifier": item.itemIdentifier,
102294
+ "app.assessment.purpose": purpose,
102295
+ "app.assessment.artifact_identity_kind": artifactIdentity.kind,
102296
+ "app.assessment.grading_source": omittedResponseCount > 0 ? "platform-qti-adapter" : "timeback-qti",
102297
+ "app.assessment.fallback_reason": reason,
102298
+ "app.assessment.fallback_duration_ms": startedAt === undefined ? 0 : Date.now() - startedAt,
102299
+ "app.assessment.qti_remote_interaction_count": qtiRemoteInteractionCount,
102300
+ "app.assessment.synthetic_incorrect_interaction_count": syntheticIncorrectInteractionCount,
102301
+ ...artifactReason ? { "app.assessment.artifact_fallback_reason": artifactReason } : {},
102302
+ ...graderError ? {
102303
+ "app.assessment.platform_grader_exception_type": errorType(graderError)
102304
+ } : {},
102305
+ ...outcome
102306
+ });
102307
+ }
102308
+ function completeItem() {
102309
+ const earned = assessmentItemEarnedScore(interactionResults.map((result) => Number.isFinite(result.score) ? result.score : 0), item.maxScore);
102310
+ const possible = item.maxScore;
102311
+ fallbackEvent({ "app.assessment.fallback_outcome": "succeeded" });
102312
+ scored.set(item.itemIdentifier, {
102313
+ itemIdentifier: item.itemIdentifier,
102314
+ score: {
102315
+ earned,
102316
+ possible,
102317
+ normalized: possible === 0 ? 0 : earned / possible
102318
+ },
102319
+ isCorrect: assessmentItemCorrectness(interactionResults.map((result) => result.isCorrect)),
102320
+ grading: omittedResponseCount > 0 ? PLATFORM_QTI_ADAPTER_CHILD_GRADING : QTI_CHILD_GRADING
102321
+ });
102322
+ }
102323
+ if (item.requests.length === 0) {
102324
+ completeItem();
102325
+ return [];
102326
+ }
102327
+ return item.requests.map((request, index2) => async () => {
102328
+ startedAt ??= Date.now();
102329
+ try {
102330
+ const response = responses[item.itemIdentifier]?.[request.identifier];
102331
+ if (response === undefined) {
102332
+ syntheticIncorrectInteractionCount += 1;
102333
+ interactionResults[index2] = { score: 0, isCorrect: false };
102334
+ } else {
102335
+ qtiRemoteInteractionCount += 1;
102336
+ interactionResults[index2] = await processResponse(item.itemIdentifier, request);
102337
+ }
102338
+ completedInteractions += 1;
102339
+ if (completedInteractions === item.requests.length && !failed) {
102340
+ completeItem();
102341
+ }
102342
+ } catch (error88) {
102343
+ if (!failed) {
102344
+ failed = true;
102345
+ fallbackEvent({
102346
+ "app.assessment.fallback_outcome": "failed",
102347
+ "exception.type": errorType(error88),
102348
+ "app.error.message": errorMessage(error88)
102349
+ });
102350
+ }
102351
+ throw error88;
102352
+ }
102353
+ });
102354
+ });
102355
+ await runWithConcurrency(fallbackTasks, SCORING_CONCURRENCY, (task) => task());
102356
+ }
102357
+ return scoringItems.map((item) => {
102358
+ const result = scored.get(item.itemIdentifier);
102359
+ if (!result) {
102360
+ throw new Error(`Assessment item ${item.itemIdentifier} was not scored`);
102361
+ }
102362
+ return result;
102363
+ });
102364
+ }
102365
+ var QTI_CHILD_GRADING, PLATFORM_QTI_ADAPTER_CHILD_GRADING, PLATFORM_CHILD_GRADING, SCORING_CONCURRENCY = 4;
102366
+ var init_timeback_assessment_scoring_util = __esm(() => {
102367
+ init_assessment_runtime2();
102368
+ init_assessment_artifact_envelope_util();
102369
+ init_timeback_assessment_runtime_util();
102370
+ QTI_CHILD_GRADING = {
102371
+ source: "timeback-qti",
102372
+ graderVersion: "timeback-qti-process-response-v1"
102373
+ };
102374
+ PLATFORM_QTI_ADAPTER_CHILD_GRADING = {
102375
+ source: "platform-qti-adapter",
102376
+ graderVersion: "platform-qti-adapter-v1",
102377
+ qtiGraderVersion: "timeback-qti-process-response-v1"
102378
+ };
102379
+ PLATFORM_CHILD_GRADING = {
102380
+ source: "platform-artifact",
102381
+ artifactVersion: `assessment-artifact-v${ASSESSMENT_ARTIFACT_SCHEMA_VERSION}`,
102382
+ graderVersion: `platform-grader-v${ASSESSMENT_GRADER_PROTOCOL_VERSION}`
102383
+ };
102384
+ });
102385
+
102422
102386
  // ../api-core/src/utils/timeback-assessment-child-result.util.ts
102423
102387
  async function buildSubmissionChildResultCommand(input) {
102424
102388
  const { submission, context: context2 } = input;
@@ -102434,12 +102398,9 @@ async function buildSubmissionChildResultCommand(input) {
102434
102398
  score: submission.score,
102435
102399
  grading: input.grading
102436
102400
  };
102437
- if (context2.kind === "review") {
102438
- return buildAssessmentChildResultCommand({
102439
- ...common2,
102440
- ...typeof isCorrect === "boolean" ? { isCorrect } : {},
102441
- context: context2
102442
- }, submission.submittedAt);
102401
+ if (context2.kind !== "platform-routed-diagnostic") {
102402
+ const payload = { ...common2, ...typeof isCorrect === "boolean" ? { isCorrect } : {} };
102403
+ return context2.kind === "review" ? buildAssessmentChildResultCommand({ ...payload, context: context2 }, submission.submittedAt) : buildAssessmentChildResultCommand({ ...payload, context: context2 }, submission.submittedAt);
102443
102404
  }
102444
102405
  if (typeof isCorrect !== "boolean") {
102445
102406
  throw new Error("Diagnostic child results require determinate correctness");
@@ -102460,11 +102421,7 @@ function assessmentChildLineItemMetadataMatches(key, stored, expected) {
102460
102421
  if (!Array.isArray(storedStandards) || !Array.isArray(expectedStandards)) {
102461
102422
  return false;
102462
102423
  }
102463
- const sameDefinition = canonicalJson2(reviewDefinitionWithoutStandards(storedDefinition)) === canonicalJson2(reviewDefinitionWithoutStandards(expectedDefinition));
102464
- function contains(standards, candidate) {
102465
- return standards.some((standard) => canonicalJson2(standard) === canonicalJson2(candidate));
102466
- }
102467
- return sameDefinition && (expectedStandards.every((standard) => contains(storedStandards, standard)) || storedStandards.every((standard) => contains(expectedStandards, standard)));
102424
+ return canonicalJson2(reviewDefinitionWithoutStandards(storedDefinition)) === canonicalJson2(reviewDefinitionWithoutStandards(expectedDefinition));
102468
102425
  }
102469
102426
  function assessmentChildLineItemMatches(stored, expected) {
102470
102427
  if (stored.status !== expected.status || stored.parentAssessmentLineItem?.sourcedId !== expected.parentAssessmentLineItem.sourcedId || stored.course?.sourcedId !== expected.course.sourcedId || stored.resultValueMin !== expected.resultValueMin || stored.resultValueMax !== expected.resultValueMax || typeof stored.metadata !== "object" || stored.metadata === null) {
@@ -102522,19 +102479,14 @@ async function classifyAssessmentChildResult(input) {
102522
102479
  result.scoreScale,
102523
102480
  result.scorePercentile,
102524
102481
  result.comment,
102525
- result.learningObjectiveSet,
102526
- result.incomplete,
102527
- result.late,
102528
- result.missing
102482
+ result.learningObjectiveSet
102529
102483
  ];
102530
- if (auxiliaryFields.some((value) => value !== undefined && value !== null)) {
102484
+ const flags2 = [result.incomplete, result.late, result.missing];
102485
+ if (auxiliaryFields.some((value) => value !== undefined && value !== null) || flags2.some((value) => value !== undefined && value !== null && value !== "false")) {
102531
102486
  return { outcome: "conflict", reason: "auxiliary-field-mismatch" };
102532
102487
  }
102533
102488
  return { outcome: "replay" };
102534
102489
  }
102535
- function hasApiStatus(error88, statusCode) {
102536
- return isApiError(error88) && error88.statusCode === statusCode;
102537
- }
102538
102490
  async function persistAssessmentChildResult(input) {
102539
102491
  const { assessmentResults, childLineItemId } = input;
102540
102492
  const verification2 = await verifyAssessmentChildResultCommand(input.command);
@@ -102543,38 +102495,127 @@ async function persistAssessmentChildResult(input) {
102543
102495
  }
102544
102496
  const { command } = verification2;
102545
102497
  const resultId = await assessmentChildResultId(command.payload.attempt.attemptId, command.payload.administration.submissionId);
102546
- async function classifyOccupant() {
102547
- const resolution = await classifyAssessmentChildResult({
102548
- result: await assessmentResults.get(resultId),
102498
+ await assessmentResults.upsert(resultId, buildAssessmentChildResultUpdate(command, childLineItemId));
102499
+ }
102500
+ function assessmentTranscriptChildLineItemId(context2, itemIdentifier) {
102501
+ return context2.metadata.purpose === "review" ? reviewQuestionLineItemId(context2.parentLineItemId, itemIdentifier) : diagnosticQuestionLineItemId(context2.parentLineItemId, itemIdentifier);
102502
+ }
102503
+ async function reconcileAssessmentTranscriptChildren({
102504
+ context: context2,
102505
+ graded,
102506
+ only,
102507
+ assessmentResults,
102508
+ ensureLineItems,
102509
+ onPersisted
102510
+ }) {
102511
+ const metadata2 = graded.metadata;
102512
+ const assessmentKey = metadata2.selectedTest.assessmentKey;
102513
+ if (!assessmentKey) {
102514
+ throw new Error("Assessment context requires a selected assessment key");
102515
+ }
102516
+ const submissions = metadata2.itemSubmissions.filter((submission) => !only || only.has(submission.submissionId));
102517
+ const administered = new Set(submissions.map((submission) => submission.itemIdentifier));
102518
+ await ensureLineItems({
102519
+ items: graded.assessment.items.filter((item) => administered.has(item.identifier))
102520
+ });
102521
+ await runWithConcurrency(submissions, 4, async (submission) => {
102522
+ const index2 = submission.responseVersion - 1;
102523
+ let flow;
102524
+ if (metadata2.purpose === "review") {
102525
+ const selection = metadata2.review.selections.find((item) => item.itemIdentifier === submission.itemIdentifier);
102526
+ flow = {
102527
+ kind: "review",
102528
+ bankRevision: metadata2.review.bankRevision,
102529
+ standard: selection.standard
102530
+ };
102531
+ } else if (metadata2.purpose === "diagnostic" && metadata2.diagnostic) {
102532
+ flow = {
102533
+ kind: "platform-routed-diagnostic",
102534
+ definitionId: metadata2.diagnostic.definitionId,
102535
+ diagnosticKey: metadata2.diagnostic.diagnosticKey,
102536
+ routingRevision: metadata2.diagnostic.routingRevision,
102537
+ transition: metadata2.diagnostic.ledger[index2]
102538
+ };
102539
+ } else {
102540
+ flow = { kind: "fixed", purpose: metadata2.purpose };
102541
+ }
102542
+ const command = await buildSubmissionChildResultCommand({
102543
+ attempt: {
102544
+ attemptId: context2.attemptId,
102545
+ gameId: context2.gameId,
102546
+ studentId: context2.studentId,
102547
+ activityId: metadata2.activityId,
102548
+ courseId: metadata2.courseId,
102549
+ integrationId: metadata2.integrationId,
102550
+ enrollmentId: metadata2.enrollmentId,
102551
+ selectedTest: { ...metadata2.selectedTest, assessmentKey }
102552
+ },
102553
+ submission,
102554
+ responses: graded.transcript.administrations[index2].responses,
102555
+ grading: submission.grading ?? QTI_CHILD_GRADING,
102556
+ context: flow
102557
+ });
102558
+ await persistAssessmentChildResult({
102559
+ assessmentResults,
102549
102560
  command,
102550
- childLineItemId
102561
+ childLineItemId: await assessmentTranscriptChildLineItemId(context2, submission.itemIdentifier)
102551
102562
  });
102552
- return resolution.outcome === "replay" ? { outcome: "replay", resultId } : { outcome: "conflict", resultId, reason: resolution.reason };
102563
+ onPersisted?.(submission.submissionId);
102564
+ });
102565
+ }
102566
+ async function recoveredAssessmentAdministration(context2, result) {
102567
+ const verified = await verifyAssessmentChildResultCommand(result.metadata);
102568
+ if (!verified.ok) {
102569
+ return null;
102553
102570
  }
102554
- try {
102555
- return await classifyOccupant();
102556
- } catch (error88) {
102557
- if (!hasApiStatus(error88, 404)) {
102558
- throw error88;
102571
+ const { command } = verified;
102572
+ const { attempt, administration, responses, context: flow } = command.payload;
102573
+ if (attempt.attemptId !== context2.attemptId || attempt.gameId !== context2.gameId || attempt.studentId !== context2.studentId || attempt.integrationId !== context2.metadata.integrationId || attempt.enrollmentId !== context2.metadata.enrollmentId || attempt.courseId !== context2.metadata.courseId || attempt.activityId !== context2.metadata.activityId || canonicalJson2(attempt.selectedTest) !== canonicalJson2(context2.metadata.selectedTest) || result.sourcedId !== await assessmentChildResultId(context2.attemptId, administration.submissionId)) {
102574
+ return null;
102575
+ }
102576
+ const disposition = await classifyAssessmentChildResult({
102577
+ result,
102578
+ command,
102579
+ childLineItemId: await assessmentTranscriptChildLineItemId(context2, administration.itemIdentifier)
102580
+ });
102581
+ if (disposition.outcome !== "replay") {
102582
+ return null;
102583
+ }
102584
+ return {
102585
+ sequence: administration.responseVersion,
102586
+ entry: {
102587
+ submissionId: administration.submissionId,
102588
+ itemIdentifier: administration.itemIdentifier,
102589
+ responses,
102590
+ ...flow.kind === "platform-routed-diagnostic" ? { routingNodeKey: flow.transition.routingNodeKey } : {}
102591
+ }
102592
+ };
102593
+ }
102594
+ async function recoverAssessmentTranscript(context2, results) {
102595
+ const entries = new Map;
102596
+ for (const result of results) {
102597
+ const recovered = await recoveredAssessmentAdministration(context2, result);
102598
+ if (recovered) {
102599
+ const atPosition = entries.get(recovered.sequence) ?? new Map;
102600
+ atPosition.set(recovered.entry.submissionId, recovered.entry);
102601
+ entries.set(recovered.sequence, atPosition);
102559
102602
  }
102560
102603
  }
102561
- try {
102562
- await assessmentResults.create({
102563
- sourcedId: resultId,
102564
- ...buildAssessmentChildResultUpdate(command, childLineItemId)
102565
- });
102566
- return { outcome: "created", resultId };
102567
- } catch (error88) {
102568
- if (!hasApiStatus(error88, 409)) {
102569
- throw error88;
102604
+ const transcript = emptyAssessmentTranscript(context2.attemptId, context2.metadata.selectedTest.contentRevision);
102605
+ for (let sequence2 = 1;sequence2 <= entries.size; sequence2 += 1) {
102606
+ const candidates = entries.get(sequence2);
102607
+ if (!candidates || candidates.size !== 1) {
102608
+ break;
102570
102609
  }
102610
+ transcript.administrations.push(candidates.values().next().value);
102571
102611
  }
102572
- return classifyOccupant();
102612
+ return transcript;
102573
102613
  }
102574
- var init_timeback_assessment_child_result_util = __esm(async () => {
102614
+ var init_timeback_assessment_child_result_util = __esm(() => {
102575
102615
  init_src();
102576
102616
  init_assessment_runtime2();
102577
- await init_errors8();
102617
+ init_timeback_assessment_runtime_util();
102618
+ init_timeback_assessment_scoring_util();
102578
102619
  });
102579
102620
 
102580
102621
  // ../api-core/src/utils/timeback-assessment-preparation.util.ts
@@ -102613,21 +102654,6 @@ function assessmentPreparationFlightKey(input) {
102613
102654
  async function assessmentPreparationReceiptMatches(plan, request) {
102614
102655
  return plan.gameId === request.gameId && plan.studentId === request.studentId && plan.activityId === request.input.activityId && plan.purpose === request.input.purpose && plan.requestHash === await assessmentPreparationRequestHash(request.input);
102615
102656
  }
102616
- function resolveAssessmentPreparationSelection(decision, candidateCourseCount) {
102617
- if (decision?.kind === "resume") {
102618
- return {
102619
- status: "blocked",
102620
- result: { status: "canonical_required", reason: "resumable_attempt" }
102621
- };
102622
- }
102623
- if (candidateCourseCount !== 1) {
102624
- return {
102625
- status: "blocked",
102626
- result: { status: "unprepared", reason: "prerequisite_unavailable" }
102627
- };
102628
- }
102629
- return decision ? { status: "ready", decision } : { status: "unavailable" };
102630
- }
102631
102657
  function preparedAssessmentIntegration(plan, candidates) {
102632
102658
  if (candidates.rows.length !== 1) {
102633
102659
  return null;
@@ -102679,6 +102705,7 @@ function buildPreparedAssessmentMetadata(plan, timestamp6, testName) {
102679
102705
  version: PLAYCADEMY_ASSESSMENT_RESULT_METADATA_VERSION,
102680
102706
  activityId: plan.activityId,
102681
102707
  courseId: plan.courseId,
102708
+ courseName: plan.courseName,
102682
102709
  integrationId: plan.integrationId,
102683
102710
  enrollmentId: plan.enrollmentId,
102684
102711
  selectedTest: plan.selectedTest,
@@ -102709,7 +102736,6 @@ function buildPreparedAssessmentMetadata(plan, timestamp6, testName) {
102709
102736
  testName,
102710
102737
  routingRevision: plan.diagnostic.routingRevision,
102711
102738
  ledger: [],
102712
- routingSnapshots: [],
102713
102739
  state: plan.diagnostic.initialState
102714
102740
  }
102715
102741
  };
@@ -103004,127 +103030,6 @@ var init_timeback_assessment_publication_util = __esm(() => {
103004
103030
  MANAGED_ASSET_URL = new RegExp(`https?://[^\\s"')]+/(${ASSESSMENT_ASSET_KEY_PREFIX}v1/sha256/[a-f0-9]{64}\\.[a-z0-9]+)`, "gi");
103005
103031
  });
103006
103032
 
103007
- // ../api-core/src/utils/timeback-assessment-reconciliation.util.ts
103008
- function assessmentChildProjectionIsTerminal(result) {
103009
- const state = { inProgress: result.inProgress ?? "", scoreStatus: result.scoreStatus };
103010
- return isAssessmentAttemptAwaitingAward(state) || isAssessmentAttemptCompleted(state);
103011
- }
103012
- async function reconcileReviewChildResults({
103013
- initial,
103014
- writeOpen,
103015
- crossBarrier,
103016
- readLatest,
103017
- restoreTerminal
103018
- }) {
103019
- if (initial.metadata.purpose !== "review") {
103020
- throw new Error("A standards-review request selected a non-review attempt");
103021
- }
103022
- if (assessmentChildProjectionIsTerminal(initial.result)) {
103023
- const settled = { result: initial.result, metadata: initial.metadata };
103024
- await restoreTerminal(settled);
103025
- return settled;
103026
- }
103027
- await writeOpen();
103028
- await crossBarrier();
103029
- const current = await readLatest();
103030
- if (current.metadata.purpose !== "review") {
103031
- throw new Error("A standards-review request selected a non-review attempt");
103032
- }
103033
- const reviewed = { result: current.result, metadata: current.metadata };
103034
- if (assessmentChildProjectionIsTerminal(current.result)) {
103035
- await restoreTerminal(reviewed);
103036
- }
103037
- return reviewed;
103038
- }
103039
- var init_timeback_assessment_reconciliation_util = __esm(() => {
103040
- init_assessment_runtime2();
103041
- });
103042
-
103043
- // ../api-core/src/utils/timeback-assessment-replay.util.ts
103044
- function resolveDiagnosticItemReplay(metadata2, input) {
103045
- const priorIndex = metadata2.itemSubmissions.findIndex((submission) => submission.submissionId === input.submissionId);
103046
- if (priorIndex === -1) {
103047
- return { action: "missing" };
103048
- }
103049
- const prior = metadata2.itemSubmissions[priorIndex];
103050
- const ledgerEntry = metadata2.diagnostic.ledger[priorIndex];
103051
- if (!prior || !ledgerEntry || prior.itemIdentifier !== input.itemIdentifier || ledgerEntry.routingNodeKey !== input.routingNodeKey || !itemResponseUpdateMatches(metadata2.responses[input.itemIdentifier], input.responses)) {
103052
- return {
103053
- action: "conflict",
103054
- failure: assessmentFlowViolation("This diagnostic submission ID was already used for a different request.", {
103055
- submissionId: input.submissionId,
103056
- routingNodeKey: input.routingNodeKey,
103057
- itemIdentifier: input.itemIdentifier
103058
- })
103059
- };
103060
- }
103061
- return {
103062
- action: "replay",
103063
- submission: prior,
103064
- ledgerEntry,
103065
- priorIndex,
103066
- routing: metadata2.diagnostic.routingSnapshots?.[priorIndex]
103067
- };
103068
- }
103069
- function buildAssessmentItemReplayResult(attemptId, metadata2, submission) {
103070
- return {
103071
- attemptId,
103072
- responseVersion: metadata2.responseVersion,
103073
- status: "in_progress",
103074
- responses: metadata2.responses,
103075
- itemSubmissions: metadata2.itemSubmissions,
103076
- submission
103077
- };
103078
- }
103079
- function diagnosticReceipt(submission, ledgerEntry) {
103080
- return {
103081
- submissionId: submission.submissionId,
103082
- routingNodeKey: ledgerEntry.routingNodeKey,
103083
- itemIdentifier: submission.itemIdentifier,
103084
- submittedAt: submission.submittedAt,
103085
- responseVersion: submission.responseVersion,
103086
- answered: submission.answered,
103087
- score: submission.score,
103088
- isCorrect: ledgerEntry.isCorrect
103089
- };
103090
- }
103091
- function buildDiagnosticItemReplayResult(attemptId, submission, ledgerEntry, routing) {
103092
- return {
103093
- attemptId,
103094
- responseVersion: submission.responseVersion,
103095
- status: "in_progress",
103096
- routing,
103097
- submission: diagnosticReceipt(submission, ledgerEntry)
103098
- };
103099
- }
103100
- function requireAssessmentMutationAccess(attemptId, integrationStatus, enrollment) {
103101
- if (!enrollment || integrationStatus === "deactivated") {
103102
- throw AssessmentRuntimeError.from(assessmentAttemptUnauthorized(attemptId));
103103
- }
103104
- return enrollment;
103105
- }
103106
- function committedReviewItemResults(metadata2, assessment) {
103107
- const available = new Set(assessment.items.map((item) => item.identifier));
103108
- const seen = new Set;
103109
- return metadata2.itemSubmissions.map((submission) => {
103110
- if (!available.has(submission.itemIdentifier) || seen.has(submission.itemIdentifier)) {
103111
- throw AssessmentRuntimeError.from(assessmentFlowViolation("The persisted review item-submission ledger is corrupt.", {
103112
- itemIdentifier: submission.itemIdentifier
103113
- }));
103114
- }
103115
- seen.add(submission.itemIdentifier);
103116
- return {
103117
- itemIdentifier: submission.itemIdentifier,
103118
- score: submission.score,
103119
- isCorrect: submission.isCorrect
103120
- };
103121
- });
103122
- }
103123
- var init_timeback_assessment_replay_util = __esm(() => {
103124
- init_assessment_runtime2();
103125
- init_errors2();
103126
- });
103127
-
103128
103033
  // ../api-core/src/utils/timeback-assessment-routing.util.ts
103129
103034
  function diagnosticRoutingFallbackReason(artifact, routingRevision) {
103130
103035
  if (!artifact) {
@@ -103274,154 +103179,256 @@ var init_timeback_assessment_rules_util = __esm(() => {
103274
103179
  init_errors2();
103275
103180
  });
103276
103181
 
103277
- // ../api-core/src/utils/timeback-assessment-scoring.util.ts
103278
- async function scoreAssessmentItems({
103279
- assessment,
103280
- responses,
103281
- artifactIdentity,
103282
- purpose,
103283
- itemIdentifiers,
103284
- artifact,
103285
- processResponse,
103286
- onEvent
103287
- }) {
103288
- const scoringItems = assessmentScoringRequests(assessment, responses).filter((item) => !itemIdentifiers || itemIdentifiers.has(item.itemIdentifier));
103289
- const artifactItems = new Map(artifact?.items.map((item) => [item.itemIdentifier, item]));
103290
- const scored = new Map;
103291
- const qti2 = [];
103292
- for (const item of scoringItems) {
103293
- const artifactItem = artifactItems.get(item.itemIdentifier);
103294
- let fallback2 = null;
103295
- if (!artifact) {
103296
- fallback2 = { reason: "artifact-unavailable" };
103297
- } else if (!artifactItem) {
103298
- fallback2 = { reason: "artifact-item-missing" };
103299
- } else if (artifactItem.strategy === "qti") {
103300
- fallback2 = { reason: "artifact-qti", artifactReason: artifactItem.reason };
103301
- } else {
103302
- const startedAt = Date.now();
103303
- try {
103304
- scored.set(item.itemIdentifier, {
103305
- ...scorePlatformAssessmentItem(artifactItem, responses[item.itemIdentifier]),
103306
- grading: PLATFORM_CHILD_GRADING
103307
- });
103308
- onEvent("assessment.item_scoring", {
103309
- "app.assessment.qti_item_identifier": item.itemIdentifier,
103310
- "app.assessment.purpose": purpose,
103311
- "app.assessment.artifact_identity_kind": artifactIdentity.kind,
103312
- "app.assessment.grading_source": "platform-artifact",
103313
- "app.assessment.grading_duration_ms": Date.now() - startedAt
103314
- });
103315
- } catch (error88) {
103316
- fallback2 = { reason: "platform-grader-error", error: error88 };
103182
+ // ../api-core/src/utils/timeback-assessment-replay.util.ts
103183
+ function diagnosticReceipt(submission, ledgerEntry) {
103184
+ return {
103185
+ submissionId: submission.submissionId,
103186
+ routingNodeKey: ledgerEntry.routingNodeKey,
103187
+ itemIdentifier: submission.itemIdentifier,
103188
+ submittedAt: submission.submittedAt,
103189
+ responseVersion: submission.responseVersion,
103190
+ answered: submission.answered,
103191
+ score: submission.score,
103192
+ isCorrect: ledgerEntry.isCorrect
103193
+ };
103194
+ }
103195
+
103196
+ // ../api-core/src/utils/timeback-assessment-transcript.util.ts
103197
+ function parseAssessmentTranscript(value) {
103198
+ const parsed = AssessmentTranscriptSchema.safeParse(value);
103199
+ if (!parsed.success) {
103200
+ throw new ValidationError("Invalid assessment transcript");
103201
+ }
103202
+ return parsed.data;
103203
+ }
103204
+ function assessmentTokenMetadata(metadata2) {
103205
+ const context2 = { ...metadata2, updatedAt: metadata2.startedAt };
103206
+ delete context2.transcript;
103207
+ return context2;
103208
+ }
103209
+ function requireAssessmentTokenContext(context2, scope) {
103210
+ if (!context2 || context2.gameId !== scope.gameId || context2.studentId !== scope.studentId || context2.attemptId !== scope.attemptId) {
103211
+ throw AssessmentRuntimeError.from(assessmentAttemptUnauthorized(scope.attemptId));
103212
+ }
103213
+ return context2;
103214
+ }
103215
+ function requireAssessmentFeedbackContext(tokenContext, scope) {
103216
+ const context2 = requireAssessmentTokenContext(tokenContext, scope);
103217
+ const metadata2 = context2.metadata;
103218
+ if (assessmentFlowForPurpose(metadata2.purpose) === "attempt-submit" && !(metadata2.purpose === "diagnostic" && metadata2.diagnostic)) {
103219
+ throw AssessmentRuntimeError.from(assessmentFlowViolation("This assessment accepts a complete attempt, not per-item feedback."));
103220
+ }
103221
+ return context2;
103222
+ }
103223
+ function prepareAssessmentProgress(tokenContext, scope, input) {
103224
+ const context2 = requireAssessmentTokenContext(tokenContext, scope);
103225
+ const transcript = parseAssessmentTranscript(input.transcript);
103226
+ const progress = assessmentProgressAdministration(context2.metadata.purpose, {
103227
+ ...input,
103228
+ transcript
103229
+ });
103230
+ if (!progress.ok) {
103231
+ throw AssessmentRuntimeError.from(progress.failure);
103232
+ }
103233
+ return { context: context2, transcript, submissionId: progress.submissionId };
103234
+ }
103235
+ function prepareAssessmentItemTranscript(assessment, input) {
103236
+ validateAssessmentResponseUpdate(assessment, { [input.itemIdentifier]: input.responses });
103237
+ const appended = appendAssessmentAdministration(input.transcript, input);
103238
+ if (!appended.ok) {
103239
+ throw AssessmentRuntimeError.from(appended.failure);
103240
+ }
103241
+ return appended;
103242
+ }
103243
+ function prepareAttemptAssessmentTranscript(context2, assessment, transcript, terminal = false) {
103244
+ return prepareAssessmentTranscript({
103245
+ transcript,
103246
+ attemptId: context2.attemptId,
103247
+ contentRevision: context2.metadata.selectedTest.contentRevision,
103248
+ assessment,
103249
+ routed: context2.metadata.purpose === "diagnostic" && context2.metadata.diagnostic !== undefined,
103250
+ allowIncomplete: !terminal && assessmentFlowForPurpose(context2.metadata.purpose) === "attempt-submit"
103251
+ });
103252
+ }
103253
+ function requireTranscriptWriteAccess(attemptId, integrationStatus) {
103254
+ if (integrationStatus === "deactivated") {
103255
+ throw AssessmentRuntimeError.from(assessmentAttemptUnauthorized(attemptId));
103256
+ }
103257
+ }
103258
+ function requireTranscriptVersion(transcript, expectedResponseVersion) {
103259
+ if (expectedResponseVersion !== transcript.administrations.length) {
103260
+ throw AssessmentRuntimeError.from(assessmentResponseVersionConflict(expectedResponseVersion, transcript.administrations.length));
103261
+ }
103262
+ }
103263
+ function requireAssessmentTranscriptCheckpoint(attempt, integrationStatus, input) {
103264
+ requireTranscriptWriteAccess(attempt.sourcedId, integrationStatus);
103265
+ requireTranscriptVersion(input.transcript, input.expectedResponseVersion);
103266
+ if (!isAssessmentAttemptOpen({ ...attempt, inProgress: attempt.inProgress ?? "" })) {
103267
+ throw AssessmentRuntimeError.from(assessmentAttemptNotOpen(attempt.sourcedId));
103268
+ }
103269
+ }
103270
+ function assessmentTranscriptRequestHash(input) {
103271
+ return sha256Hex(canonicalJson2({
103272
+ transcript: input.transcript,
103273
+ ...input.session ? { session: input.session } : {}
103274
+ }));
103275
+ }
103276
+ function resolveAssessmentTranscriptSubmission(attempt, metadata2, integrationStatus, input, requestHash) {
103277
+ const disposition = classifyAssessmentSubmission({
103278
+ inProgress: attempt.inProgress ?? "",
103279
+ scoreStatus: attempt.scoreStatus,
103280
+ submissionId: metadata2.submissionId
103281
+ }, input.submissionId);
103282
+ if (disposition === "replay" && metadata2.completion?.requestHash === requestHash) {
103283
+ return "replay";
103284
+ }
103285
+ if (disposition === "reject") {
103286
+ throw AssessmentRuntimeError.from(assessmentAlreadySubmitted(attempt.sourcedId));
103287
+ }
103288
+ if (disposition !== "submit") {
103289
+ throw AssessmentRuntimeError.from(assessmentFlowViolation("A different completion was already accepted for this attempt."));
103290
+ }
103291
+ requireTranscriptWriteAccess(attempt.sourcedId, integrationStatus);
103292
+ if (input.session && input.session.runId !== attempt.sourcedId) {
103293
+ throw new ValidationError("Assessment session runId must match the attemptId.");
103294
+ }
103295
+ requireTranscriptVersion(input.transcript, input.expectedResponseVersion);
103296
+ return "submit";
103297
+ }
103298
+ function buildGradedAssessmentTranscript(input) {
103299
+ const { context: context2, prepared, itemResults, assessment, submittedAt, manifest } = input;
103300
+ const completed = completeAssessmentTranscript({
103301
+ transcript: prepared.transcript,
103302
+ scores: itemResults,
103303
+ purpose: context2.metadata.purpose,
103304
+ itemCount: assessment.items.length,
103305
+ submittedAt,
103306
+ manifest,
103307
+ terminal: input.terminal
103308
+ });
103309
+ const common2 = {
103310
+ ...context2.metadata,
103311
+ responses: prepared.responses,
103312
+ responseVersion: completed.itemSubmissions.length,
103313
+ itemSubmissions: completed.itemSubmissions,
103314
+ updatedAt: submittedAt,
103315
+ transcript: prepared.transcript
103316
+ };
103317
+ const diagnostic = context2.metadata.purpose === "diagnostic" ? context2.metadata.diagnostic : undefined;
103318
+ const metadata2 = diagnostic ? {
103319
+ ...common2,
103320
+ purpose: "diagnostic",
103321
+ diagnostic: { ...diagnostic, ledger: completed.ledger, state: completed.state }
103322
+ } : common2;
103323
+ return {
103324
+ metadata: metadata2,
103325
+ transcript: prepared.transcript,
103326
+ assessment,
103327
+ itemResults,
103328
+ manifest,
103329
+ score: aggregateAssessmentScore(itemResults.map((item) => item.score.earned), itemResults.reduce((sum, item) => sum + item.score.possible, 0)),
103330
+ correctQuestions: countCorrectQuestions(itemResults.map((item) => item.isCorrect))
103331
+ };
103332
+ }
103333
+ function assessmentTranscriptRuntimeError(error88) {
103334
+ return error88 instanceof AssessmentTranscriptError ? AssessmentRuntimeError.from({
103335
+ code: error88.code,
103336
+ message: error88.message,
103337
+ details: { transcriptIndex: error88.index }
103338
+ }) : error88;
103339
+ }
103340
+ function buildAssessmentTranscriptFeedback(attemptId, graded, index2) {
103341
+ const { metadata: metadata2 } = graded;
103342
+ const submission = metadata2.itemSubmissions[index2];
103343
+ const common2 = {
103344
+ attemptId,
103345
+ responseVersion: metadata2.responseVersion,
103346
+ status: "in_progress",
103347
+ transcript: graded.transcript
103348
+ };
103349
+ return metadata2.purpose === "diagnostic" && metadata2.diagnostic ? {
103350
+ ...common2,
103351
+ routing: projectDiagnosticRoutingSnapshot(metadata2.diagnostic.routingRevision, metadata2.diagnostic.state),
103352
+ submission: diagnosticReceipt(submission, metadata2.diagnostic.ledger[index2])
103353
+ } : {
103354
+ ...common2,
103355
+ responses: metadata2.responses,
103356
+ itemSubmissions: metadata2.itemSubmissions,
103357
+ submission
103358
+ };
103359
+ }
103360
+ async function resumeAssessmentTranscript(input) {
103361
+ const retained = input.suppliedTranscript ?? input.checkpointTranscript;
103362
+ let transcript = retained ?? await input.recover();
103363
+ while (true) {
103364
+ try {
103365
+ return await input.grade(transcript);
103366
+ } catch (error88) {
103367
+ const index2 = error88 instanceof AssessmentRuntimeError && isRecord(error88.details) ? error88.details.transcriptIndex : undefined;
103368
+ if (retained !== undefined || typeof index2 !== "number" || index2 < 0 || index2 >= transcript.administrations.length) {
103369
+ throw error88;
103317
103370
  }
103318
- }
103319
- if (fallback2) {
103320
- qti2.push({ item, ...fallback2 });
103371
+ transcript = {
103372
+ ...transcript,
103373
+ administrations: transcript.administrations.slice(0, index2)
103374
+ };
103321
103375
  }
103322
103376
  }
103323
- if (qti2.length > 0) {
103324
- const fallbackTasks = qti2.flatMap(({ item, reason, artifactReason, error: graderError }) => {
103325
- const interactionResults = [];
103326
- let startedAt;
103327
- let completedInteractions = 0;
103328
- let failed = false;
103329
- const omittedResponseCount = item.requests.filter((request) => responses[item.itemIdentifier]?.[request.identifier] === undefined).length;
103330
- let qtiRemoteInteractionCount = 0;
103331
- let syntheticIncorrectInteractionCount = 0;
103332
- function fallbackEvent(outcome) {
103333
- onEvent("assessment.item_scoring_fallback", {
103334
- "app.assessment.qti_item_identifier": item.itemIdentifier,
103335
- "app.assessment.purpose": purpose,
103336
- "app.assessment.artifact_identity_kind": artifactIdentity.kind,
103337
- "app.assessment.grading_source": omittedResponseCount > 0 ? "platform-qti-adapter" : "timeback-qti",
103338
- "app.assessment.fallback_reason": reason,
103339
- "app.assessment.fallback_duration_ms": startedAt === undefined ? 0 : Date.now() - startedAt,
103340
- "app.assessment.qti_remote_interaction_count": qtiRemoteInteractionCount,
103341
- "app.assessment.synthetic_incorrect_interaction_count": syntheticIncorrectInteractionCount,
103342
- ...artifactReason ? { "app.assessment.artifact_fallback_reason": artifactReason } : {},
103343
- ...graderError ? {
103344
- "app.assessment.platform_grader_exception_type": errorType(graderError)
103345
- } : {},
103346
- ...outcome
103347
- });
103348
- }
103349
- function completeItem() {
103350
- const earned = assessmentItemEarnedScore(interactionResults.map((result) => Number.isFinite(result.score) ? result.score : 0), item.maxScore);
103351
- const possible = item.maxScore;
103352
- fallbackEvent({ "app.assessment.fallback_outcome": "succeeded" });
103353
- scored.set(item.itemIdentifier, {
103354
- itemIdentifier: item.itemIdentifier,
103355
- score: {
103356
- earned,
103357
- possible,
103358
- normalized: possible === 0 ? 0 : earned / possible
103359
- },
103360
- isCorrect: assessmentItemCorrectness(interactionResults.map((result) => result.isCorrect)),
103361
- grading: omittedResponseCount > 0 ? PLATFORM_QTI_ADAPTER_CHILD_GRADING : QTI_CHILD_GRADING
103362
- });
103363
- }
103364
- if (item.requests.length === 0) {
103365
- completeItem();
103366
- return [];
103377
+ }
103378
+ function assessmentReviewTranscriptOutcomes(input) {
103379
+ const resultByItem = new Map(input.itemResults.map((item) => [item.itemIdentifier, item]));
103380
+ const administered = administeredItemIdentifiers(input.metadata.itemSubmissions);
103381
+ return input.metadata.review.selections.filter((selection) => administered.has(selection.itemIdentifier)).map((selection) => {
103382
+ const result = resultByItem.get(selection.itemIdentifier);
103383
+ const submission = itemAdministration(input.metadata.itemSubmissions, selection.itemIdentifier);
103384
+ if (!result || !submission) {
103385
+ throw new Error(`Scoring omitted selected review item ${selection.itemIdentifier}`);
103386
+ }
103387
+ return {
103388
+ itemIdentifier: selection.itemIdentifier,
103389
+ standard: selection.standard,
103390
+ answered: submission.answered,
103391
+ score: result.score,
103392
+ isCorrect: result.isCorrect
103393
+ };
103394
+ });
103395
+ }
103396
+ function assessmentReviewTranscriptExposures(input) {
103397
+ const exposures = [];
103398
+ const authoritative = new Set;
103399
+ for (const { result, metadata: metadata2 } of input.attempts) {
103400
+ const state = { ...result, inProgress: result.inProgress ?? "" };
103401
+ const settled = isAssessmentAttemptAwaitingAward(state) || isAssessmentAttemptCompleted(state);
103402
+ const useTranscript = !settled && metadata2.transcript !== undefined;
103403
+ if (metadata2.purpose === "review" && (useTranscript || settled)) {
103404
+ authoritative.add(result.sourcedId);
103405
+ if (metadata2.review.bankRevision === input.bankRevision) {
103406
+ const accepted = useTranscript ? metadata2.transcript.administrations.map((entry) => ({
103407
+ itemIdentifier: entry.itemIdentifier,
103408
+ deliveredAt: metadata2.updatedAt
103409
+ })) : metadata2.itemSubmissions.map((submission) => ({
103410
+ itemIdentifier: submission.itemIdentifier,
103411
+ deliveredAt: submission.submittedAt
103412
+ }));
103413
+ exposures.push(...accepted);
103367
103414
  }
103368
- return item.requests.map((request, index2) => async () => {
103369
- startedAt ??= Date.now();
103370
- try {
103371
- const response = responses[item.itemIdentifier]?.[request.identifier];
103372
- if (response === undefined) {
103373
- syntheticIncorrectInteractionCount += 1;
103374
- interactionResults[index2] = { score: 0, isCorrect: false };
103375
- } else {
103376
- qtiRemoteInteractionCount += 1;
103377
- interactionResults[index2] = await processResponse(item.itemIdentifier, request);
103378
- }
103379
- completedInteractions += 1;
103380
- if (completedInteractions === item.requests.length && !failed) {
103381
- completeItem();
103382
- }
103383
- } catch (error88) {
103384
- if (!failed) {
103385
- failed = true;
103386
- fallbackEvent({
103387
- "app.assessment.fallback_outcome": "failed",
103388
- "exception.type": errorType(error88),
103389
- "app.error.message": errorMessage(error88)
103390
- });
103391
- }
103392
- throw error88;
103393
- }
103394
- });
103395
- });
103396
- await runWithConcurrency(fallbackTasks, SCORING_CONCURRENCY, (task) => task());
103415
+ }
103397
103416
  }
103398
- return scoringItems.map((item) => {
103399
- const result = scored.get(item.itemIdentifier);
103400
- if (!result) {
103401
- throw new Error(`Assessment item ${item.itemIdentifier} was not scored`);
103417
+ for (const command of input.children) {
103418
+ const { attempt, administration, context: context2 } = command.payload;
103419
+ if (!authoritative.has(attempt.attemptId) && context2.kind === "review" && context2.bankRevision === input.bankRevision) {
103420
+ exposures.push({
103421
+ itemIdentifier: administration.itemIdentifier,
103422
+ deliveredAt: command.recordedAt
103423
+ });
103402
103424
  }
103403
- return result;
103404
- });
103425
+ }
103426
+ return exposures;
103405
103427
  }
103406
- var QTI_CHILD_GRADING, PLATFORM_QTI_ADAPTER_CHILD_GRADING, PLATFORM_CHILD_GRADING, SCORING_CONCURRENCY = 4;
103407
- var init_timeback_assessment_scoring_util = __esm(() => {
103428
+ var init_timeback_assessment_transcript_util = __esm(() => {
103408
103429
  init_assessment_runtime2();
103409
- init_assessment_artifact_envelope_util();
103430
+ init_errors2();
103410
103431
  init_timeback_assessment_runtime_util();
103411
- QTI_CHILD_GRADING = {
103412
- source: "timeback-qti",
103413
- graderVersion: "timeback-qti-process-response-v1"
103414
- };
103415
- PLATFORM_QTI_ADAPTER_CHILD_GRADING = {
103416
- source: "platform-qti-adapter",
103417
- graderVersion: "platform-qti-adapter-v1",
103418
- qtiGraderVersion: "timeback-qti-process-response-v1"
103419
- };
103420
- PLATFORM_CHILD_GRADING = {
103421
- source: "platform-artifact",
103422
- artifactVersion: `assessment-artifact-v${ASSESSMENT_ARTIFACT_SCHEMA_VERSION}`,
103423
- graderVersion: `platform-grader-v${ASSESSMENT_GRADER_PROTOCOL_VERSION}`
103424
- };
103425
103432
  });
103426
103433
 
103427
103434
  // ../api-core/src/utils/timeback-qti-hydration.util.ts
@@ -103508,20 +103515,19 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
103508
103515
  init_assessment_artifact_runtime_util();
103509
103516
  init_assessment_runtime_lock_util();
103510
103517
  init_timeback_assessment_artifacts_util();
103518
+ init_timeback_assessment_child_result_util();
103511
103519
  init_timeback_assessment_preparation_util();
103512
103520
  init_timeback_assessment_publication_util();
103513
- init_timeback_assessment_reconciliation_util();
103514
- init_timeback_assessment_replay_util();
103515
103521
  init_timeback_assessment_routing_util();
103516
103522
  init_timeback_assessment_rules_util();
103517
103523
  init_timeback_assessment_runtime_util();
103518
103524
  init_timeback_assessment_scoring_util();
103525
+ init_timeback_assessment_transcript_util();
103519
103526
  init_timeback_qti_hydration_util();
103520
103527
  await __promiseAll([
103521
103528
  init_dist5(),
103522
103529
  init_errors8(),
103523
- init_assessment_artifact_loader_util(),
103524
- init_timeback_assessment_child_result_util()
103530
+ init_assessment_artifact_loader_util()
103525
103531
  ]);
103526
103532
  AssessmentPreparationPrerequisiteError = class AssessmentPreparationPrerequisiteError extends Error {
103527
103533
  original;
@@ -103536,8 +103542,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
103536
103542
  static PREPARATION_IN_FLIGHT_LIMIT = 32;
103537
103543
  static ASSESSMENT_CACHE_TTL_MS = 60000;
103538
103544
  static EXPORT_CONCURRENCY = 4;
103539
- static ITEM_SUBMISSION_MAX_PREPARATION_ATTEMPTS = 3;
103540
- static ASSESSMENT_VERSION_CONFIRMATION_DELAY_MS = 25;
103541
103545
  static SCORING_CONCURRENCY = 4;
103542
103546
  static STALE_AWARD_AGE_MS = 15 * 60 * 1000;
103543
103547
  deps;
@@ -103554,6 +103558,14 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
103554
103558
  maxSize: TimebackAssessmentRuntimeService.ASSESSMENT_CACHE_LIMIT
103555
103559
  });
103556
103560
  artifactLoader;
103561
+ itemScoreCache = new TimebackCache({
103562
+ defaultTTL: 5 * 60000,
103563
+ maxSize: 512
103564
+ });
103565
+ childLineItemCache = new TimebackCache({
103566
+ defaultTTL: 5 * 60000,
103567
+ maxSize: 512
103568
+ });
103557
103569
  preparations = new AssessmentPreparationFlights(TimebackAssessmentRuntimeService.PREPARATION_IN_FLIGHT_LIMIT);
103558
103570
  constructor(deps) {
103559
103571
  this.deps = deps;
@@ -103619,8 +103631,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
103619
103631
  return this.startReview({ gameId, studentId, input, user });
103620
103632
  }
103621
103633
  const masteryStandard = input.purpose === "mastery" ? normalizeMasteryStandard(input.standard) : undefined;
103622
- const requestFingerprint = this.startRequestFingerprint(input);
103623
- const pendingSupersessions = new Set;
103624
103634
  const candidates = await this.candidateIntegrations(gameId, studentId, input);
103625
103635
  const lockKeys = candidates.rows.map((row) => assessmentRuntimeStartLockKey({
103626
103636
  studentId,
@@ -103629,7 +103639,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
103629
103639
  courseId: row.courseId
103630
103640
  }));
103631
103641
  return this.deps.assessmentRuntimeLock(this.deps.db, lockKeys, async (db2) => {
103632
- const context2 = await this.selectIntegration(candidates, studentId, input);
103642
+ const context2 = await this.selectIntegration(candidates, input);
103633
103643
  const attemptScope = {
103634
103644
  studentId,
103635
103645
  activityId: input.activityId,
@@ -103642,44 +103652,34 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
103642
103652
  this.liveTests(context2.integration.id, input.purpose, db2, masteryStandard),
103643
103653
  this.listAttempts(attemptScope)
103644
103654
  ]);
103645
- const attempts = listing.attempts;
103646
- const initialResolution = await this.resumeAvailableAttempt(tests, attempts, requestFingerprint, pendingSupersessions, (resumeDecision) => this.resumeSnapshot(resumeDecision, attempts, context2.integration));
103647
- if (initialResolution.kind === "resumed") {
103648
- await this.persistPendingSupersessionsBestEffort(db2, pendingSupersessions);
103649
- return initialResolution.snapshot;
103650
- }
103651
- let decision = this.requireDecision(initialResolution.decision, input.purpose);
103652
- let assessment = await this.loadAssessment(decision.test.qtiTestIdentifier);
103653
- const refreshedListing = await this.listAttempts(attemptScope);
103654
- const refreshed = refreshedListing.attempts;
103655
- const refreshedResolution = await this.resumeAvailableAttempt(tests, refreshed, requestFingerprint, pendingSupersessions, (resumeDecision) => this.resumeSnapshot(resumeDecision, refreshed, context2.integration));
103656
- if (refreshedResolution.kind === "resumed") {
103657
- await this.persistPendingSupersessionsBestEffort(db2, pendingSupersessions);
103658
- return refreshedResolution.snapshot;
103659
- }
103660
- decision = this.requireDecision(refreshedResolution.decision, input.purpose);
103661
- if (assessment.identifier !== decision.test.qtiTestIdentifier) {
103662
- assessment = await this.loadAssessment(decision.test.qtiTestIdentifier);
103663
- }
103664
- const classId = await this.resolveClassId(context2.integration.courseId, context2.enrollment, studentId);
103655
+ const pending = await this.pendingAwardSnapshot(listing, context2);
103656
+ if (pending) {
103657
+ return pending;
103658
+ }
103659
+ const decision = this.selectFreshStart(tests, listing, input.purpose);
103660
+ const [assessment, classId, { attemptNumber, attemptId }] = await Promise.all([
103661
+ this.loadAssessment(decision.test.qtiTestIdentifier),
103662
+ this.resolveClassId(context2.integration.courseId, context2.enrollment, studentId),
103663
+ this.claimAttemptId({
103664
+ gameId,
103665
+ studentId,
103666
+ courseId: context2.integration.courseId,
103667
+ activityId: input.activityId,
103668
+ purpose: input.purpose
103669
+ }, nextAssessmentAttemptNumber(listing.attempts.values()))
103670
+ ]);
103665
103671
  const lineItemId = await this.ensureLineItem({
103666
103672
  classId,
103667
103673
  integration: context2.integration,
103668
103674
  purpose: input.purpose,
103669
103675
  assessment
103670
103676
  });
103671
- const { attemptNumber, attemptId } = await this.claimAttemptId({
103672
- gameId,
103673
- studentId,
103674
- courseId: context2.integration.courseId,
103675
- activityId: input.activityId,
103676
- purpose: input.purpose
103677
- }, Math.max(0, ...[...refreshed.values()].map((entry) => entry.metadata.attemptNumber)) + 1);
103678
103677
  const timestamp6 = new Date().toISOString();
103679
103678
  const metadataBase = {
103680
103679
  version: PLAYCADEMY_ASSESSMENT_RESULT_METADATA_VERSION,
103681
103680
  activityId: input.activityId,
103682
103681
  courseId: context2.integration.courseId,
103682
+ courseName: context2.enrollment.course.title,
103683
103683
  integrationId: context2.integration.id,
103684
103684
  enrollmentId: context2.enrollment.id,
103685
103685
  selectedTest: {
@@ -103698,7 +103698,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
103698
103698
  ...metadataBase,
103699
103699
  purpose: input.purpose,
103700
103700
  mastery: {
103701
- requestFingerprint,
103701
+ requestFingerprint: masteryRequestFingerprint(input),
103702
103702
  standard: masteryStandard
103703
103703
  }
103704
103704
  } : { ...metadataBase, purpose: input.purpose };
@@ -103716,8 +103716,11 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
103716
103716
  grade: context2.integration.grade
103717
103717
  })
103718
103718
  });
103719
- await this.persistPendingSupersessionsBestEffort(db2, pendingSupersessions);
103720
- return this.snapshot(result, metadata2, assessment, context2.integration);
103719
+ const [snapshot] = await Promise.all([
103720
+ this.snapshot(result, metadata2, assessment, context2.integration),
103721
+ this.supersedeReplacedAttempts(db2, listing.attempts, context2.enrollment.id)
103722
+ ]);
103723
+ return snapshot;
103721
103724
  });
103722
103725
  }
103723
103726
  async prepareNewAttempt(params, requestHash) {
@@ -103726,7 +103729,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
103726
103729
  this.candidateIntegrations(params.gameId, params.studentId, params.input),
103727
103730
  this.studentAssessmentResults(params.studentId)
103728
103731
  ]);
103729
- const context2 = await this.selectIntegration(candidates, params.studentId, params.input, results);
103732
+ const context2 = await this.selectIntegration(candidates, params.input);
103730
103733
  const attemptScope = {
103731
103734
  studentId: params.studentId,
103732
103735
  activityId: params.input.activityId,
@@ -103736,29 +103739,31 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
103736
103739
  enrollmentId: context2.enrollment.id
103737
103740
  };
103738
103741
  const tests = await this.liveTests(context2.integration.id, params.input.purpose, this.deps.db, params.input.purpose === "mastery" ? params.input.standard : undefined, params.input.purpose === "diagnostic" ? params.input.diagnosticKey : undefined);
103739
- const listing = this.attemptListing(results, attemptScope);
103740
- const decision = selectRuntimeAssessment(tests, [
103741
- ...this.attemptsMatchingRequest(listing.attempts, this.startRequestFingerprint(params.input)).values()
103742
- ].map((entry) => entry.selection));
103743
- const selection = resolveAssessmentPreparationSelection(decision, candidates.rows.length);
103744
- if (selection.status === "blocked") {
103745
- this.recordPreparation(selection.result.status, params.input.purpose, startedAt, {
103746
- reason: selection.result.status === "canonical_required" ? selection.result.reason : "multiple_candidate_courses"
103742
+ const listing = await this.attemptListing(results, attemptScope);
103743
+ if (this.pendingAwardAttempt(listing, context2.enrollment.id)) {
103744
+ this.recordPreparation("unprepared", params.input.purpose, startedAt, {
103745
+ reason: "pending_award"
103747
103746
  });
103748
- return selection.result;
103747
+ return { status: "unprepared", reason: "prerequisite_unavailable" };
103749
103748
  }
103750
- if (selection.status === "unavailable") {
103751
- throw AssessmentRuntimeError.from(assessmentNoEligibleTests(params.input.purpose));
103749
+ if (candidates.rows.length !== 1) {
103750
+ this.recordPreparation("unprepared", params.input.purpose, startedAt, {
103751
+ reason: "multiple_candidate_courses"
103752
+ });
103753
+ return { status: "unprepared", reason: "prerequisite_unavailable" };
103752
103754
  }
103753
103755
  const preparation = {
103754
103756
  context: context2,
103755
103757
  tests,
103756
103758
  listing,
103757
- decision: selection.decision
103759
+ decision: this.selectFreshStart(tests, listing, params.input.purpose)
103758
103760
  };
103759
103761
  let prepared;
103760
103762
  try {
103761
103763
  prepared = await this.buildPreparationPlan(params, requestHash, preparation);
103764
+ if (!await this.loadPreparedPlanAssessment(prepared.plan)) {
103765
+ throw new Error("Prepared assessment artifact is unavailable");
103766
+ }
103762
103767
  } catch (error88) {
103763
103768
  if (error88 instanceof ValidationError || error88 instanceof AssessmentRuntimeError) {
103764
103769
  throw error88;
@@ -103844,8 +103849,10 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
103844
103849
  integrationId: context2.integration.id,
103845
103850
  enrollmentId: context2.enrollment.id,
103846
103851
  courseId: context2.integration.courseId,
103852
+ courseName: context2.enrollment.course.title,
103847
103853
  ...attempt,
103848
103854
  parentLineItemId,
103855
+ supersedes: [...this.replacedAttemptIds(listing.attempts, context2.enrollment.id)],
103849
103856
  selectedTest: {
103850
103857
  assessmentKey: decision.test.assessmentKey,
103851
103858
  identifier: assessment.identifier,
@@ -103884,7 +103891,9 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
103884
103891
  const selected = selectReviewItems({
103885
103892
  request: params.input,
103886
103893
  bank: catalog.bank,
103887
- exposures: this.reviewExposures(listing.reviewChildren.values(), {
103894
+ exposures: assessmentReviewTranscriptExposures({
103895
+ attempts: listing.attempts.values(),
103896
+ children: listing.reviewChildren.values(),
103888
103897
  bankRevision: catalog.bank.bankRevision
103889
103898
  })
103890
103899
  });
@@ -103895,26 +103904,12 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
103895
103904
  throw AssessmentRuntimeError.from(assessmentReviewBankShortage(selected.fulfillment));
103896
103905
  }
103897
103906
  const artifact = await this.loadOrCompileReviewBankArtifact(catalog);
103898
- const assessment = projectReviewAssessment(artifact.assessment, catalog.bank, selected.selections);
103899
103907
  const source = { assessment: artifact.assessment, bank: catalog.bank };
103900
103908
  const parentLineItemId = await this.provisionPreparationPrerequisites(() => this.ensureReviewBankLineItem({
103901
103909
  integration: context2.integration,
103902
103910
  activityId: params.input.activityId,
103903
103911
  source
103904
103912
  }));
103905
- await this.provisionPreparationPrerequisites(() => runWithConcurrency(assessment.items, TimebackAssessmentRuntimeService.SCORING_CONCURRENCY, (item) => {
103906
- const indexed = catalog.bank.items.find((candidate) => candidate.itemIdentifier === item.identifier);
103907
- if (!indexed) {
103908
- throw new Error(`Review-bank index is missing item ${item.identifier}`);
103909
- }
103910
- return this.ensureReviewQuestionLineItem({
103911
- parentLineItemId,
103912
- integration: context2.integration,
103913
- bankRevision: catalog.bank.bankRevision,
103914
- standards: [...indexed.standards],
103915
- item
103916
- });
103917
- }));
103918
103913
  const attempt = await this.provisionPreparationPrerequisites(() => this.claimAttemptId({
103919
103914
  gameId: params.gameId,
103920
103915
  studentId: params.studentId,
@@ -103933,8 +103928,10 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
103933
103928
  integrationId: context2.integration.id,
103934
103929
  enrollmentId: context2.enrollment.id,
103935
103930
  courseId: context2.integration.courseId,
103931
+ courseName: context2.enrollment.course.title,
103936
103932
  ...attempt,
103937
103933
  parentLineItemId,
103934
+ supersedes: [...this.replacedAttemptIds(listing.attempts, context2.enrollment.id)],
103938
103935
  selectedTest: {
103939
103936
  assessmentKey: decision.test.assessmentKey,
103940
103937
  identifier: artifact.assessment.identifier,
@@ -103983,10 +103980,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
103983
103980
  if (!integration || !await this.preparedPlanTestIsLive(verified, db2)) {
103984
103981
  return null;
103985
103982
  }
103986
- if (await this.attemptIdOccupied(verified.attemptId)) {
103987
- return null;
103988
- }
103989
- return this.createPreparedAttempt(verified, assessment, integration);
103983
+ return this.claimPreparedAttempt(db2, verified, assessment, integration);
103990
103984
  });
103991
103985
  if (!outcome) {
103992
103986
  this.recordPreparedStart("fallback", params.input.purpose, startedAt, {
@@ -104011,8 +104005,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
104011
104005
  studentId,
104012
104006
  input
104013
104007
  }) {
104014
- const requestFingerprint = this.startRequestFingerprint(input);
104015
- const pendingSupersessions = new Set;
104016
104008
  const candidates = await this.candidateIntegrations(gameId, studentId, input);
104017
104009
  const lockKeys = candidates.rows.map((row) => assessmentRuntimeStartLockKey({
104018
104010
  studentId,
@@ -104021,7 +104013,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
104021
104013
  courseId: row.courseId
104022
104014
  }));
104023
104015
  return this.deps.assessmentRuntimeLock(this.deps.db, lockKeys, async (db2) => {
104024
- const context2 = await this.selectIntegration(candidates, studentId, input);
104016
+ const context2 = await this.selectIntegration(candidates, input);
104025
104017
  const attemptScope = {
104026
104018
  studentId,
104027
104019
  activityId: input.activityId,
@@ -104034,32 +104026,25 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
104034
104026
  this.liveTests(context2.integration.id, input.purpose, db2, undefined, input.diagnosticKey),
104035
104027
  this.listAttempts(attemptScope)
104036
104028
  ]);
104037
- const initialResolution = await this.resumeAvailableAttempt(tests, listing.attempts, requestFingerprint, pendingSupersessions, (resumeDecision) => this.resumeDiagnosticSnapshot(resumeDecision, listing.attempts, context2.integration, db2));
104038
- if (initialResolution.kind === "resumed") {
104039
- await this.persistPendingSupersessionsBestEffort(db2, pendingSupersessions);
104040
- return initialResolution.snapshot;
104029
+ const pending = await this.pendingAwardSnapshot(listing, context2);
104030
+ if (pending) {
104031
+ return pending;
104041
104032
  }
104042
- let decision = this.requireDecision(initialResolution.decision, input.purpose);
104033
+ const decision = this.selectFreshStart(tests, listing, input.purpose);
104043
104034
  let definition = await this.requireDiagnosticDefinition(decision.test.id, context2.integration.id, db2, true);
104044
- let assessment = await this.loadAssessment(definition.qtiTestIdentifier);
104045
- let routing = await this.initializeHostedDiagnostic(definition, assessment);
104046
- const refreshedListing = await this.listAttempts(attemptScope);
104047
- const refreshed = refreshedListing.attempts;
104048
- const refreshedResolution = await this.resumeAvailableAttempt(tests, refreshed, requestFingerprint, pendingSupersessions, (resumeDecision) => this.resumeDiagnosticSnapshot(resumeDecision, refreshed, context2.integration, db2));
104049
- if (refreshedResolution.kind === "resumed") {
104050
- await this.persistPendingSupersessionsBestEffort(db2, pendingSupersessions);
104051
- return refreshedResolution.snapshot;
104052
- }
104053
- decision = this.requireDecision(refreshedResolution.decision, input.purpose);
104054
- if (definition.id !== decision.test.id) {
104055
- definition = await this.requireDiagnosticDefinition(decision.test.id, context2.integration.id, db2, true);
104056
- assessment = await this.loadAssessment(definition.qtiTestIdentifier);
104057
- routing = await this.initializeHostedDiagnostic(definition, assessment);
104058
- } else {
104059
- definition = await this.requireDiagnosticDefinition(definition.id, context2.integration.id, db2, true);
104060
- routing = await this.initializeHostedDiagnostic(definition, assessment);
104061
- }
104062
- const classId = await this.resolveClassId(context2.integration.courseId, context2.enrollment, studentId);
104035
+ const [assessment, classId, { attemptNumber, attemptId }] = await Promise.all([
104036
+ this.loadAssessment(definition.qtiTestIdentifier),
104037
+ this.resolveClassId(context2.integration.courseId, context2.enrollment, studentId),
104038
+ this.claimAttemptId({
104039
+ gameId,
104040
+ studentId,
104041
+ courseId: context2.integration.courseId,
104042
+ activityId: input.activityId,
104043
+ purpose: input.purpose
104044
+ }, nextAssessmentAttemptNumber(listing.attempts.values()))
104045
+ ]);
104046
+ definition = await this.requireDiagnosticDefinition(definition.id, context2.integration.id, db2, true);
104047
+ const routing = await this.initializeHostedDiagnostic(definition, assessment);
104063
104048
  const lineItemId = await this.ensureLineItem({
104064
104049
  classId,
104065
104050
  integration: context2.integration,
@@ -104071,19 +104056,13 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
104071
104056
  routingRevision: routing.routingRevision
104072
104057
  }
104073
104058
  });
104074
- const { attemptNumber, attemptId } = await this.claimAttemptId({
104075
- gameId,
104076
- studentId,
104077
- courseId: context2.integration.courseId,
104078
- activityId: input.activityId,
104079
- purpose: input.purpose
104080
- }, Math.max(0, ...[...refreshed.values()].map((entry) => entry.metadata.attemptNumber)) + 1);
104081
104059
  const timestamp6 = new Date().toISOString();
104082
104060
  const metadata2 = {
104083
104061
  version: PLAYCADEMY_ASSESSMENT_RESULT_METADATA_VERSION,
104084
104062
  activityId: input.activityId,
104085
104063
  purpose: "diagnostic",
104086
104064
  courseId: context2.integration.courseId,
104065
+ courseName: context2.enrollment.course.title,
104087
104066
  integrationId: context2.integration.id,
104088
104067
  enrollmentId: context2.enrollment.id,
104089
104068
  selectedTest: {
@@ -104103,7 +104082,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
104103
104082
  testName: assessment.title,
104104
104083
  routingRevision: routing.routingRevision,
104105
104084
  ledger: [],
104106
- routingSnapshots: [],
104107
104085
  state: routing.state
104108
104086
  }
104109
104087
  };
@@ -104121,8 +104099,13 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
104121
104099
  grade: context2.integration.grade
104122
104100
  })
104123
104101
  });
104124
- await this.persistPendingSupersessionsBestEffort(db2, pendingSupersessions);
104125
- return this.diagnosticSnapshot(result, metadata2, assessment, context2.integration, routing.state);
104102
+ const [snapshot] = await Promise.all([
104103
+ this.diagnosticSnapshot(result, metadata2, assessment, context2.integration, {
104104
+ state: routing.state
104105
+ }),
104106
+ this.supersedeReplacedAttempts(db2, listing.attempts, context2.enrollment.id)
104107
+ ]);
104108
+ return snapshot;
104126
104109
  });
104127
104110
  }
104128
104111
  async startReview({
@@ -104132,7 +104115,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
104132
104115
  }) {
104133
104116
  const preparationStartedAt = Date.now();
104134
104117
  const requestFingerprint = reviewRequestFingerprint(input, DEFAULT_REVIEW_SELECTION_POLICY);
104135
- const pendingSupersessions = new Set;
104136
104118
  const normalizedRequest = normalizeReviewRequest(input);
104137
104119
  const candidates = await this.candidateIntegrations(gameId, studentId, input);
104138
104120
  const lockKeys = candidates.rows.map((row) => assessmentRuntimeStartLockKey({
@@ -104142,7 +104124,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
104142
104124
  courseId: row.courseId
104143
104125
  }));
104144
104126
  return this.deps.assessmentRuntimeLock(this.deps.db, lockKeys, async (db2) => {
104145
- const context2 = await this.selectIntegration(candidates, studentId, input);
104127
+ const context2 = await this.selectIntegration(candidates, input);
104146
104128
  const attemptScope = {
104147
104129
  studentId,
104148
104130
  activityId: input.activityId,
@@ -104155,40 +104137,22 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
104155
104137
  this.liveTests(context2.integration.id, input.purpose, db2),
104156
104138
  this.listAttempts(attemptScope)
104157
104139
  ]);
104158
- const attempts = listing.attempts;
104159
- const initialResolution = await this.resumeAvailableAttempt(tests, attempts, requestFingerprint, pendingSupersessions, (resumeDecision) => this.resumeReviewSnapshot(resumeDecision, attempts, listing.reviewChildren, context2));
104160
- if (initialResolution.kind === "resumed") {
104161
- await this.persistPendingSupersessionsBestEffort(db2, pendingSupersessions);
104162
- return initialResolution.snapshot;
104163
- }
104164
- let decision = initialResolution.decision;
104165
- if (tests.length === 0 || !decision) {
104166
- throw AssessmentRuntimeError.from(assessmentNoEligibleTests(input.purpose));
104140
+ const pending = await this.pendingAwardSnapshot(listing, context2);
104141
+ if (pending) {
104142
+ return pending;
104167
104143
  }
104144
+ const decision = this.selectFreshStart(tests, listing, input.purpose);
104168
104145
  if (tests.length > 1) {
104169
104146
  throw new ValidationError("Standards review requires exactly one live review-bank assessment.");
104170
104147
  }
104171
104148
  const manifestStartedAt = Date.now();
104172
- let catalog = await this.loadReviewBankCatalog(decision.test.qtiTestIdentifier);
104149
+ const catalog = await this.loadReviewBankCatalog(decision.test.qtiTestIdentifier);
104173
104150
  this.recordReviewPreparationPhase("manifest_load", manifestStartedAt, {
104174
104151
  candidateCount: catalog.bank.items.length
104175
104152
  });
104176
- const refreshedListing = await this.listAttempts(attemptScope);
104177
- const refreshed = refreshedListing.attempts;
104178
- const refreshedResolution = await this.resumeAvailableAttempt(tests, refreshed, requestFingerprint, pendingSupersessions, (resumeDecision) => this.resumeReviewSnapshot(resumeDecision, refreshed, refreshedListing.reviewChildren, context2));
104179
- if (refreshedResolution.kind === "resumed") {
104180
- await this.persistPendingSupersessionsBestEffort(db2, pendingSupersessions);
104181
- return refreshedResolution.snapshot;
104182
- }
104183
- const refreshedDecision = refreshedResolution.decision;
104184
- if (!refreshedDecision || refreshedDecision.kind !== "start") {
104185
- throw AssessmentRuntimeError.from(assessmentNoEligibleTests(input.purpose));
104186
- }
104187
- decision = refreshedDecision;
104188
- if (catalog.test.identifier !== decision.test.qtiTestIdentifier) {
104189
- catalog = await this.loadReviewBankCatalog(decision.test.qtiTestIdentifier);
104190
- }
104191
- const exposures = this.reviewExposures(refreshedListing.reviewChildren.values(), {
104153
+ const exposures = assessmentReviewTranscriptExposures({
104154
+ attempts: listing.attempts.values(),
104155
+ children: listing.reviewChildren.values(),
104192
104156
  bankRevision: catalog.bank.bankRevision
104193
104157
  });
104194
104158
  const selected = selectReviewItems({
@@ -104209,24 +104173,27 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
104209
104173
  });
104210
104174
  const assessment = projectReviewAssessment(source.assessment, source.bank, selected.selections);
104211
104175
  const provisioningStartedAt = Date.now();
104212
- const parentLineItemId = await this.ensureReviewBankLineItem({
104213
- integration: context2.integration,
104214
- activityId: input.activityId,
104215
- source
104216
- });
104217
- const { attemptNumber, attemptId } = await this.claimAttemptId({
104218
- gameId,
104219
- studentId,
104220
- courseId: context2.integration.courseId,
104221
- activityId: input.activityId,
104222
- purpose: input.purpose
104223
- }, Math.max(0, ...[...refreshed.values()].map((entry) => entry.metadata.attemptNumber)) + 1);
104176
+ const [parentLineItemId, { attemptNumber, attemptId }] = await Promise.all([
104177
+ this.ensureReviewBankLineItem({
104178
+ integration: context2.integration,
104179
+ activityId: input.activityId,
104180
+ source
104181
+ }),
104182
+ this.claimAttemptId({
104183
+ gameId,
104184
+ studentId,
104185
+ courseId: context2.integration.courseId,
104186
+ activityId: input.activityId,
104187
+ purpose: input.purpose
104188
+ }, nextAssessmentAttemptNumber(listing.attempts.values()))
104189
+ ]);
104224
104190
  const timestamp6 = new Date().toISOString();
104225
104191
  const metadata2 = {
104226
104192
  version: PLAYCADEMY_ASSESSMENT_RESULT_METADATA_VERSION,
104227
104193
  activityId: input.activityId,
104228
104194
  purpose: input.purpose,
104229
104195
  courseId: context2.integration.courseId,
104196
+ courseName: context2.enrollment.course.title,
104230
104197
  integrationId: context2.integration.id,
104231
104198
  enrollmentId: context2.enrollment.id,
104232
104199
  selectedTest: {
@@ -104263,18 +104230,27 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
104263
104230
  grade: context2.integration.grade
104264
104231
  })
104265
104232
  });
104266
- await this.persistPendingSupersessionsBestEffort(db2, pendingSupersessions);
104233
+ const [snapshot] = await Promise.all([
104234
+ this.snapshot(result, metadata2, assessment, context2.integration),
104235
+ this.supersedeReplacedAttempts(db2, listing.attempts, context2.enrollment.id)
104236
+ ]);
104267
104237
  this.recordReviewPreparationPhase("provisioning", provisioningStartedAt);
104268
104238
  this.recordReviewPreparationPhase("total", preparationStartedAt, {
104269
104239
  candidateCount: catalog.bank.items.length,
104270
104240
  selectedCount: selected.selections.length
104271
104241
  });
104272
- return this.snapshot(result, metadata2, assessment, context2.integration);
104242
+ return snapshot;
104273
104243
  });
104274
104244
  }
104275
104245
  async get(params) {
104246
+ return this.resume({ ...params, input: {} });
104247
+ }
104248
+ async resume({
104249
+ input,
104250
+ ...params
104251
+ }) {
104276
104252
  const attempt = await this.authorizeAttemptRead(params);
104277
- return this.snapshotForResult(attempt.result, attempt.metadata, attempt.integration);
104253
+ return this.snapshotForResult(attempt.result, attempt.metadata, attempt.integration, input.transcript);
104278
104254
  }
104279
104255
  async latest({
104280
104256
  gameId,
@@ -104336,496 +104312,104 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
104336
104312
  }
104337
104313
  async save({ input, ...params }) {
104338
104314
  await this.deps.validateDeveloperAccess(params.user, params.gameId);
104339
- try {
104340
- const warmup = await this.peekAttempt(params);
104341
- if (this.isOpen(warmup.result) && !this.isPlatformRoutedDiagnosticMetadata(warmup.metadata)) {
104342
- await this.loadAttemptAssessment(warmup.metadata);
104343
- }
104344
- } catch (error88) {
104345
- addEvent("assessment.save_warmup_failed", {
104346
- "app.assessment.attempt_id": params.attemptId,
104347
- "exception.type": errorType(error88),
104348
- "app.error.message": errorMessage(error88)
104349
- });
104350
- }
104351
104315
  return this.deps.assessmentRuntimeLock(this.deps.db, [assessmentRuntimeAttemptLockKey(params.attemptId)], async (db2) => {
104352
- const attempt = await this.authorizeAttempt(params, {
104316
+ const { attempt } = await this.resolveAttemptAuthorization(params, {
104353
104317
  db: db2,
104354
- developerAccessValidated: true
104318
+ developerAccessValidated: true,
104319
+ includeEnrollment: false
104355
104320
  });
104356
- if (this.isPlatformRoutedDiagnosticMetadata(attempt.metadata)) {
104357
- throw AssessmentRuntimeError.from(assessmentFlowViolation("Platform-routed diagnostics do not accept draft saves.", { flow: "platform-routed-item-submit" }));
104358
- }
104359
- this.assertInProgress(attempt.result);
104360
- this.assertResponseVersion(attempt.metadata, input.expectedResponseVersion);
104361
- const assessment = await this.loadAttemptAssessment(attempt.metadata);
104362
- const flowFailure = assessmentResponseUpdateFlowFailure(attempt.metadata.purpose, attempt.metadata.itemSubmissions, input.responses);
104363
- if (flowFailure) {
104364
- throw AssessmentRuntimeError.from(flowFailure);
104365
- }
104366
- validateAssessmentResponseUpdate(assessment, input.responses);
104367
- const responses = applyAssessmentResponseUpdate(attempt.metadata.responses, input.responses);
104368
- validateAssessmentDraftResponses(assessment, responses);
104369
- const metadata2 = {
104370
- ...attempt.metadata,
104371
- responses,
104372
- responseVersion: attempt.metadata.responseVersion + 1,
104373
- updatedAt: new Date().toISOString()
104374
- };
104375
- await this.putResultMetadata(attempt.result, metadata2);
104321
+ requireAssessmentTranscriptCheckpoint(attempt.result, attempt.integration.status, input);
104322
+ const graded = await this.gradeTranscript({
104323
+ attemptId: params.attemptId,
104324
+ metadata: attempt.metadata
104325
+ }, input.transcript, false, db2);
104326
+ await this.requireClient().api.oneroster.assessmentResults.upsert(params.attemptId, buildAssessmentResultCheckpoint({
104327
+ result: attempt.result,
104328
+ metadata: attempt.metadata,
104329
+ transcript: graded.transcript,
104330
+ timestamp: graded.metadata.updatedAt
104331
+ }));
104332
+ this.reconcileTranscriptChildren(this.tokenContext(attempt), graded).catch((error88) => this.recordTranscriptFailure("checkpoint_projection", params.attemptId, error88));
104376
104333
  return {
104377
- attemptId: attempt.result.sourcedId,
104378
- responseVersion: metadata2.responseVersion,
104334
+ attemptId: params.attemptId,
104335
+ responseVersion: graded.metadata.responseVersion,
104379
104336
  status: "in_progress",
104380
- responses
104337
+ responses: graded.metadata.responses,
104338
+ transcript: graded.transcript
104381
104339
  };
104382
104340
  });
104383
104341
  }
104384
- async submitItem({
104342
+ async recordProgress({
104385
104343
  input,
104386
104344
  ...params
104387
104345
  }) {
104388
104346
  await this.deps.validateDeveloperAccess(params.user, params.gameId);
104389
- if (input.routingNodeKey !== undefined) {
104390
- const identity = await this.peekAttempt(params);
104391
- if (this.isPlatformRoutedDiagnosticMetadata(identity.metadata)) {
104392
- return this.submitDiagnosticItem(params, input);
104393
- }
104394
- throw AssessmentRuntimeError.from(assessmentFlowViolation("routingNodeKey is not accepted by the hosted conventional item-submit flow.", { routingNodeKey: input.routingNodeKey }));
104395
- }
104396
- let preview = await this.prepareItemSubmission(params, input);
104397
- let preparationAttempts = 1;
104398
- while (true) {
104399
- const committed = await this.deps.assessmentRuntimeLock(this.deps.db, [assessmentRuntimeAttemptLockKey(params.attemptId)], async (db2) => {
104400
- const { attempt, enrollment } = await this.resolveAttemptAuthorization(params, {
104401
- db: db2,
104402
- developerAccessValidated: true
104403
- });
104404
- if (this.isPlatformRoutedDiagnosticMetadata(attempt.metadata)) {
104405
- this.requireActiveAttempt({ attempt, enrollment }, params.attemptId);
104406
- throw AssessmentRuntimeError.from(assessmentFlowViolation("Platform-routed diagnostic submissions require routingNodeKey.", { flow: "platform-routed-item-submit" }));
104407
- }
104408
- const replay = resolveAssessmentItemReplay(attempt.metadata, input);
104409
- if (replay.action === "conflict") {
104410
- this.requireActiveAttempt({ attempt, enrollment }, params.attemptId);
104411
- throw AssessmentRuntimeError.from(replay.failure);
104412
- }
104413
- if (replay.action === "replay") {
104414
- const prior = replay.submission;
104415
- const child2 = attempt.metadata.purpose === "review" ? await this.prepareReviewAssessmentChildResult({
104416
- attempt,
104417
- metadata: attempt.metadata,
104418
- submission: prior
104419
- }) : null;
104420
- return {
104421
- action: "complete",
104422
- response: buildAssessmentItemReplayResult(attempt.result.sourcedId, attempt.metadata, prior),
104423
- checkpoint: child2,
104424
- projection: attempt.metadata.purpose === "review" ? {
104425
- result: attempt.result,
104426
- metadata: attempt.metadata,
104427
- itemIdentifier: input.itemIdentifier,
104428
- integration: attempt.integration
104429
- } : null
104430
- };
104431
- }
104432
- this.requireActiveAttempt({ attempt, enrollment }, params.attemptId);
104433
- this.assertInProgress(attempt.result);
104434
- if (!preview || preview.responseVersion !== attempt.metadata.responseVersion || preview.submissionId !== input.submissionId || preview.itemIdentifier !== input.itemIdentifier) {
104435
- if (preparationAttempts >= TimebackAssessmentRuntimeService.ITEM_SUBMISSION_MAX_PREPARATION_ATTEMPTS) {
104436
- throw assessmentPreparationContentionError(input.expectedResponseVersion, attempt.metadata.responseVersion, "assessment");
104437
- }
104438
- return { action: "prepare" };
104439
- }
104440
- const transition = prepareAssessmentItemSubmission({
104441
- purpose: attempt.metadata.purpose,
104442
- responseVersion: attempt.metadata.responseVersion,
104443
- responses: attempt.metadata.responses,
104444
- itemSubmissions: attempt.metadata.itemSubmissions,
104445
- assessment: preview.assessment
104446
- }, input);
104447
- if (transition.action === "reject") {
104448
- throw AssessmentRuntimeError.from(transition.failure);
104449
- }
104450
- if (transition.action === "replay") {
104451
- const child2 = attempt.metadata.purpose === "review" ? await this.prepareReviewAssessmentChildResult({
104452
- attempt,
104453
- metadata: attempt.metadata,
104454
- submission: transition.submission
104455
- }) : null;
104456
- return {
104457
- action: "complete",
104458
- response: {
104459
- attemptId: attempt.result.sourcedId,
104460
- responseVersion: transition.responseVersion,
104461
- status: "in_progress",
104462
- responses: transition.responses,
104463
- itemSubmissions: attempt.metadata.itemSubmissions,
104464
- submission: transition.submission
104465
- },
104466
- checkpoint: child2,
104467
- projection: attempt.metadata.purpose === "review" ? {
104468
- result: attempt.result,
104469
- metadata: attempt.metadata,
104470
- itemIdentifier: input.itemIdentifier,
104471
- integration: attempt.integration
104472
- } : null
104473
- };
104474
- }
104475
- if (!preview.scoring) {
104476
- throw new Error("Prepared item submission is missing its score");
104477
- }
104478
- const submittedAt = new Date().toISOString();
104479
- const completed = completeAssessmentItemSubmission(attempt.metadata, input, transition, preview.scoring, submittedAt);
104480
- const metadata2 = {
104481
- ...attempt.metadata,
104482
- responses: completed.responses,
104483
- responseVersion: completed.responseVersion,
104484
- itemSubmissions: completed.itemSubmissions,
104485
- updatedAt: submittedAt
104486
- };
104487
- const child = metadata2.purpose === "review" ? await this.prepareReviewAssessmentChildResult({
104488
- attempt,
104489
- metadata: metadata2,
104490
- submission: completed.submission
104491
- }) : null;
104492
- await this.putResultMetadata(attempt.result, metadata2);
104493
- return {
104494
- action: "complete",
104495
- response: {
104496
- attemptId: attempt.result.sourcedId,
104497
- responseVersion: completed.responseVersion,
104498
- status: "in_progress",
104499
- responses: completed.responses,
104500
- itemSubmissions: completed.itemSubmissions,
104501
- submission: completed.submission
104502
- },
104503
- checkpoint: child,
104504
- projection: metadata2.purpose === "review" ? {
104505
- result: attempt.result,
104506
- metadata: metadata2,
104507
- itemIdentifier: input.itemIdentifier,
104508
- integration: attempt.integration
104509
- } : null
104510
- };
104511
- });
104512
- if (committed.action === "prepare") {
104513
- preparationAttempts += 1;
104514
- preview = await this.scoreItemSubmission(params, input);
104515
- } else {
104516
- if (committed.projection) {
104517
- this.projectReviewItemResponse(params, committed.projection, committed.checkpoint);
104518
- }
104519
- return committed.response;
104520
- }
104521
- }
104522
- }
104523
- async submitDiagnosticItem(params, input) {
104524
- let preview = await this.prepareDiagnosticItemSubmission(params, input);
104525
- let preparationAttempts = 1;
104526
- while (true) {
104527
- const committed = await this.deps.assessmentRuntimeLock(this.deps.db, [assessmentRuntimeAttemptLockKey(params.attemptId)], async (db2) => {
104528
- const { attempt, enrollment } = await this.resolveAttemptAuthorization(params, { db: db2, developerAccessValidated: true });
104529
- if (!this.isPlatformRoutedDiagnosticMetadata(attempt.metadata)) {
104530
- this.requireActiveAttempt({ attempt, enrollment }, params.attemptId);
104531
- throw AssessmentRuntimeError.from(assessmentFlowViolation("routingNodeKey is accepted only for a platform-routed diagnostic.", { routingNodeKey: input.routingNodeKey }));
104532
- }
104533
- const metadata2 = attempt.metadata;
104534
- const replay = resolveDiagnosticItemReplay(metadata2, input);
104535
- if (replay.action === "conflict") {
104536
- this.requireActiveAttempt({ attempt, enrollment }, params.attemptId);
104537
- throw AssessmentRuntimeError.from(replay.failure);
104538
- }
104539
- if (replay.action === "replay") {
104540
- const { submission: prior, ledgerEntry, priorIndex } = replay;
104541
- let routing = replay.routing;
104542
- if (!routing) {
104543
- const assessment = preview?.assessment ?? await this.loadAttemptAssessment(metadata2);
104544
- const loaded2 = await this.loadAttemptDiagnosticRouting(metadata2, assessment, db2);
104545
- const replayed = replayDiagnosticRouting(loaded2.manifest, metadata2.diagnostic.ledger.slice(0, priorIndex + 1));
104546
- if (!replayed.ok) {
104547
- throw AssessmentRuntimeError.from(assessmentFlowViolation("The committed diagnostic routing state cannot be replayed.", { failure: replayed.failure }));
104548
- }
104549
- routing = this.diagnosticRoutingSnapshot(metadata2, replayed.state);
104550
- }
104551
- const child2 = await this.prepareDiagnosticAssessmentChildResult({
104552
- attempt,
104553
- metadata: metadata2,
104554
- submission: prior,
104555
- transition: ledgerEntry
104556
- });
104557
- return {
104558
- action: "complete",
104559
- response: buildDiagnosticItemReplayResult(attempt.result.sourcedId, prior, ledgerEntry, routing),
104560
- checkpoint: child2,
104561
- projection: {
104562
- result: attempt.result,
104563
- metadata: metadata2,
104564
- ledgerEntry,
104565
- integration: attempt.integration
104566
- }
104567
- };
104568
- }
104569
- this.requireActiveAttempt({ attempt, enrollment }, params.attemptId);
104570
- this.assertInProgress(attempt.result);
104571
- if (!preview || preview.responseVersion !== metadata2.responseVersion || preview.submissionId !== input.submissionId || preview.routingNodeKey !== input.routingNodeKey || preview.itemIdentifier !== input.itemIdentifier) {
104572
- if (preparationAttempts >= TimebackAssessmentRuntimeService.ITEM_SUBMISSION_MAX_PREPARATION_ATTEMPTS) {
104573
- throw assessmentPreparationContentionError(input.expectedResponseVersion, metadata2.responseVersion, "diagnostic");
104574
- }
104575
- return { action: "prepare" };
104576
- }
104577
- this.assertResponseVersion(metadata2, input.expectedResponseVersion);
104578
- const loaded = await this.loadAttemptDiagnosticRouting(metadata2, preview.assessment, db2);
104579
- const expected = loaded.state.next;
104580
- if (loaded.state.status !== "in-progress" || !expected || expected.nodeKey !== input.routingNodeKey || expected.itemIdentifier !== input.itemIdentifier) {
104581
- throw AssessmentRuntimeError.from(assessmentFlowViolation(expected ? "Submitted diagnostic item is not the canonical next item." : "Diagnostic routing is already ready to complete.", {
104582
- expectedRoutingNodeKey: expected?.nodeKey ?? null,
104583
- routingNodeKey: input.routingNodeKey,
104584
- expectedItemIdentifier: expected?.itemIdentifier ?? null,
104585
- itemIdentifier: input.itemIdentifier
104586
- }));
104587
- }
104588
- const responses = prepareDiagnosticAssessmentResponses(preview.assessment, metadata2.responses, input);
104589
- const scoring = preview.scoring;
104590
- if (!scoring || typeof scoring.isCorrect !== "boolean") {
104591
- throw AssessmentRuntimeError.from(assessmentFlowViolation(`Diagnostic item ${input.itemIdentifier} did not produce determinate binary grading.`, { itemIdentifier: input.itemIdentifier }));
104592
- }
104593
- const advanced = advanceDiagnosticRouting(loaded.manifest, metadata2.diagnostic.ledger, {
104594
- routingNodeKey: input.routingNodeKey,
104595
- itemIdentifier: input.itemIdentifier,
104596
- isCorrect: scoring.isCorrect
104597
- });
104598
- if (!advanced.ok) {
104599
- throw AssessmentRuntimeError.from(assessmentFlowViolation("The diagnostic routing transition was rejected.", { failure: advanced.failure }));
104600
- }
104601
- const submittedAt = new Date().toISOString();
104602
- const responseVersion = metadata2.responseVersion + 1;
104603
- const routingSnapshot = this.diagnosticRoutingSnapshot(metadata2, advanced.state);
104604
- const routingSnapshots = metadata2.diagnostic.routingSnapshots ? [...metadata2.diagnostic.routingSnapshots, routingSnapshot] : undefined;
104605
- const submission = {
104606
- submissionId: input.submissionId,
104607
- itemIdentifier: input.itemIdentifier,
104608
- submittedAt,
104609
- responseVersion,
104610
- answered: Object.keys(responses[input.itemIdentifier] ?? {}).length > 0,
104611
- score: scoring.score,
104612
- isCorrect: scoring.isCorrect,
104613
- grading: scoring.grading
104614
- };
104615
- const nextMetadata = {
104616
- ...metadata2,
104617
- responses,
104618
- responseVersion,
104619
- itemSubmissions: [...metadata2.itemSubmissions, submission],
104620
- updatedAt: submittedAt,
104621
- diagnostic: {
104622
- ...metadata2.diagnostic,
104623
- ledger: [...metadata2.diagnostic.ledger, advanced.ledgerEntry],
104624
- ...routingSnapshots ? { routingSnapshots } : {},
104625
- state: advanced.state
104626
- }
104627
- };
104628
- const child = await this.prepareDiagnosticAssessmentChildResult({
104629
- attempt,
104630
- metadata: nextMetadata,
104631
- submission,
104632
- transition: advanced.ledgerEntry
104633
- });
104634
- await this.putResultMetadata(attempt.result, nextMetadata);
104635
- return {
104636
- action: "complete",
104637
- response: {
104638
- attemptId: attempt.result.sourcedId,
104639
- responseVersion,
104640
- status: "in_progress",
104641
- routing: routingSnapshot,
104642
- submission: diagnosticReceipt(submission, advanced.ledgerEntry)
104643
- },
104644
- checkpoint: child,
104645
- projection: {
104646
- result: attempt.result,
104647
- metadata: nextMetadata,
104648
- ledgerEntry: advanced.ledgerEntry,
104649
- integration: attempt.integration
104650
- }
104651
- };
104652
- });
104653
- if (committed.action === "prepare") {
104654
- preparationAttempts += 1;
104655
- preview = await this.scoreDiagnosticItemSubmission(params, input);
104656
- } else {
104657
- this.projectDiagnosticItemResponse(committed.projection, preview?.assessment, committed.checkpoint);
104658
- return committed.response;
104659
- }
104660
- }
104347
+ const { context: context2, transcript, submissionId } = prepareAssessmentProgress(await this.deps.verifyAssessmentToken(input.assessmentToken), params, input);
104348
+ const graded = await this.gradeTranscript(context2, transcript);
104349
+ await this.reconcileTranscriptChildren(context2, graded, new Set([submissionId]));
104350
+ return { attemptId: params.attemptId };
104661
104351
  }
104662
- async prepareDiagnosticItemSubmission(params, input) {
104352
+ async submitItem({
104353
+ input,
104354
+ ...params
104355
+ }) {
104356
+ await this.deps.validateDeveloperAccess(params.user, params.gameId);
104357
+ const context2 = requireAssessmentFeedbackContext(await this.deps.verifyAssessmentToken(input.assessmentToken), params);
104358
+ const transcript = parseAssessmentTranscript(input.transcript);
104359
+ const assessment = await this.loadAttemptAssessment(context2.metadata);
104360
+ const appended = prepareAssessmentItemTranscript(assessment, { ...input, transcript });
104361
+ const graded = await this.gradeTranscript(context2, appended.transcript);
104362
+ const submission = graded.metadata.itemSubmissions[appended.index];
104363
+ this.reconcileTranscriptChildren(context2, graded, new Set([submission.submissionId])).catch((error88) => this.recordTranscriptFailure("child_projection", params.attemptId, error88));
104364
+ return buildAssessmentTranscriptFeedback(params.attemptId, graded, appended.index);
104365
+ }
104366
+ async gradeTranscript(context2, transcript, terminal = false, db2 = this.deps.db) {
104367
+ const startedAt = Date.now();
104663
104368
  try {
104664
- return await this.scoreDiagnosticItemSubmission(params, input);
104665
- } catch (error88) {
104666
- addEvent("assessment.diagnostic_item_submission_preview_failed", {
104667
- "app.assessment.attempt_id": params.attemptId,
104668
- "exception.type": errorType(error88),
104669
- "app.error.message": errorMessage(error88)
104670
- });
104671
- return null;
104672
- }
104673
- }
104674
- async scoreDiagnosticItemSubmission(params, input) {
104675
- const attempt = await this.peekAttempt(params);
104676
- if (!this.isPlatformRoutedDiagnosticMetadata(attempt.metadata)) {
104677
- return null;
104678
- }
104679
- const prior = attempt.metadata.itemSubmissions.find((submission) => submission.submissionId === input.submissionId);
104680
- if (prior || !this.isOpen(attempt.result)) {
104681
- return null;
104682
- }
104683
- const assessment = await this.loadAttemptAssessment(attempt.metadata);
104684
- const artifactIdentity = assessmentArtifactIdentityFromAttempt(attempt.metadata);
104685
- const scoringArtifact = await this.artifactLoader.loadMatchedScoring(assessment, artifactIdentity);
104686
- const loaded = await this.loadAttemptDiagnosticRouting(attempt.metadata, assessment, this.deps.db, scoringArtifact);
104687
- const expected = loaded.state.next;
104688
- let scoring = null;
104689
- if (loaded.state.status === "in-progress" && expected?.nodeKey === input.routingNodeKey && expected.itemIdentifier === input.itemIdentifier && attempt.metadata.responseVersion === input.expectedResponseVersion) {
104690
- const responses = prepareDiagnosticAssessmentResponses(assessment, attempt.metadata.responses, input);
104691
- scoring = await this.scoreItem({
104369
+ const assessment = await this.loadAttemptAssessment(context2.metadata);
104370
+ const prepared = prepareAttemptAssessmentTranscript(context2, assessment, transcript, terminal);
104371
+ const artifactIdentity = assessmentArtifactIdentityFromAttempt(context2.metadata);
104372
+ const artifact = await this.artifactLoader.loadMatchedScoring(assessment, artifactIdentity);
104373
+ const [itemResults, routing] = await Promise.all([
104374
+ scoreCachedAssessmentItems({
104375
+ assessment,
104376
+ responses: prepared.responses,
104377
+ artifactIdentity,
104378
+ purpose: context2.metadata.purpose,
104379
+ itemIdentifiers: new Set(prepared.transcript.administrations.map((entry) => entry.itemIdentifier)),
104380
+ artifact,
104381
+ cache: this.itemScoreCache,
104382
+ processResponse: (identifier, request) => this.requireClient().qtiApi.assessmentItems.processResponse(identifier, request),
104383
+ onEvent: addEvent
104384
+ }),
104385
+ this.isPlatformRoutedDiagnosticMetadata(context2.metadata) ? this.loadAttemptDiagnosticRouting(context2.metadata, assessment, db2, artifact) : undefined
104386
+ ]);
104387
+ const graded = buildGradedAssessmentTranscript({
104388
+ context: context2,
104692
104389
  assessment,
104693
- responses,
104694
- itemIdentifier: input.itemIdentifier,
104695
- artifactIdentity,
104696
- purpose: attempt.metadata.purpose,
104697
- preparedArtifact: scoringArtifact
104390
+ prepared,
104391
+ itemResults,
104392
+ submittedAt: new Date().toISOString(),
104393
+ manifest: routing?.manifest,
104394
+ terminal
104395
+ });
104396
+ addEvent("assessment.transcript_validated", {
104397
+ "app.assessment.attempt_id": context2.attemptId,
104398
+ "app.assessment.administration_count": graded.metadata.responseVersion,
104399
+ "app.assessment.duration_ms": Date.now() - startedAt
104698
104400
  });
104699
- }
104700
- return {
104701
- responseVersion: attempt.metadata.responseVersion,
104702
- submissionId: input.submissionId,
104703
- routingNodeKey: input.routingNodeKey,
104704
- itemIdentifier: input.itemIdentifier,
104705
- assessment,
104706
- scoring
104707
- };
104708
- }
104709
- async scoreItems({
104710
- preparedArtifact,
104711
- ...input
104712
- }) {
104713
- const artifact = preparedArtifact === undefined ? await this.artifactLoader.loadMatchedScoring(input.assessment, input.artifactIdentity) : preparedArtifact;
104714
- return scoreAssessmentItems({
104715
- ...input,
104716
- artifact,
104717
- processResponse: (identifier, request) => this.requireClient().qtiApi.assessmentItems.processResponse(identifier, request),
104718
- onEvent: addEvent
104719
- });
104720
- }
104721
- async scoreItem(input) {
104722
- const [scoring] = await this.scoreItems({
104723
- assessment: input.assessment,
104724
- responses: input.responses,
104725
- artifactIdentity: input.artifactIdentity,
104726
- purpose: input.purpose,
104727
- itemIdentifiers: new Set([input.itemIdentifier]),
104728
- preparedArtifact: input.preparedArtifact
104729
- });
104730
- return scoring;
104731
- }
104732
- async prepareItemSubmission(params, input) {
104733
- try {
104734
- return await this.scoreItemSubmission(params, input);
104401
+ return graded;
104735
104402
  } catch (error88) {
104736
- addEvent("assessment.item_submission_preview_failed", {
104737
- "app.assessment.attempt_id": params.attemptId,
104738
- "exception.type": errorType(error88),
104739
- "app.error.message": errorMessage(error88)
104740
- });
104741
- return null;
104403
+ throw assessmentTranscriptRuntimeError(error88);
104742
104404
  }
104743
104405
  }
104744
- async scoreItemSubmission(params, input) {
104745
- const attempt = await this.peekAttempt(params);
104746
- const prior = attempt.metadata.itemSubmissions.find((submission) => submission.submissionId === input.submissionId);
104747
- if (prior || !this.isOpen(attempt.result)) {
104748
- return null;
104749
- }
104750
- const assessment = await this.loadAttemptAssessment(attempt.metadata);
104751
- const transition = prepareAssessmentItemSubmission({
104752
- purpose: attempt.metadata.purpose,
104753
- responseVersion: attempt.metadata.responseVersion,
104754
- responses: attempt.metadata.responses,
104755
- itemSubmissions: attempt.metadata.itemSubmissions,
104756
- assessment
104757
- }, input);
104758
- const scoring = transition.action === "commit" ? await this.scoreItem({
104759
- assessment,
104760
- responses: transition.responses,
104761
- itemIdentifier: input.itemIdentifier,
104762
- artifactIdentity: assessmentArtifactIdentityFromAttempt(attempt.metadata),
104763
- purpose: attempt.metadata.purpose
104764
- }) : null;
104765
- return {
104766
- responseVersion: attempt.metadata.responseVersion,
104767
- submissionId: input.submissionId,
104768
- itemIdentifier: input.itemIdentifier,
104769
- assessment,
104770
- scoring
104771
- };
104772
- }
104773
- async scoreSubmission(attempt) {
104774
- const assessment = await this.loadAttemptAssessment(attempt.metadata);
104775
- validateAssessmentResponses(assessment, attempt.metadata.responses);
104776
- const itemResults = attempt.metadata.purpose === "review" ? committedReviewItemResults(attempt.metadata, assessment) : await this.scoreItems({
104777
- assessment,
104778
- responses: attempt.metadata.responses,
104779
- artifactIdentity: assessmentArtifactIdentityFromAttempt(attempt.metadata),
104780
- purpose: attempt.metadata.purpose
104781
- });
104782
- return {
104783
- responseVersion: attempt.metadata.responseVersion,
104784
- assessment,
104785
- itemResults,
104786
- score: aggregateAssessmentScore(itemResults.map((result) => result.score.earned), itemResults.reduce((total, result) => total + result.score.possible, 0)),
104787
- correctQuestions: countCorrectQuestions(itemResults.map((result) => result.isCorrect))
104788
- };
104789
- }
104790
- async peekAttempt({
104791
- studentId,
104792
- attemptId
104793
- }) {
104794
- const result = await this.requireClient().api.oneroster.assessmentResults.get(attemptId).catch((error88) => {
104795
- if (isApiError(error88) && error88.statusCode === 404) {
104796
- throw this.unauthorizedAttempt(attemptId);
104797
- }
104798
- throw error88;
104406
+ recordTranscriptFailure(operation, attemptId, error88) {
104407
+ addEvent("assessment.transcript_operation_failed", {
104408
+ "app.assessment.operation": operation,
104409
+ "app.assessment.attempt_id": attemptId,
104410
+ "exception.type": errorType(error88),
104411
+ "app.error.message": errorMessage(error88)
104799
104412
  });
104800
- const metadata2 = playcademyAssessmentResultMetadata(result.metadata);
104801
- if (!metadata2 || result.student.sourcedId !== studentId || result.status !== ONEROSTER_STATUS2.active) {
104802
- throw this.unauthorizedAttempt(attemptId);
104803
- }
104804
- return { result, metadata: metadata2 };
104805
- }
104806
- async prepareSubmission(params, input) {
104807
- try {
104808
- const attempt = await this.peekAttempt(params);
104809
- const disposition = classifyAssessmentSubmission({
104810
- inProgress: attempt.result.inProgress ?? "",
104811
- scoreStatus: attempt.result.scoreStatus,
104812
- submissionId: attempt.metadata.submissionId
104813
- }, input.submissionId);
104814
- if (disposition !== "submit") {
104815
- return null;
104816
- }
104817
- if (this.isPlatformRoutedDiagnosticMetadata(attempt.metadata)) {
104818
- return null;
104819
- }
104820
- return await this.scoreSubmission(attempt);
104821
- } catch (error88) {
104822
- addEvent("assessment.submission_preview_failed", {
104823
- "app.assessment.attempt_id": params.attemptId,
104824
- "exception.type": errorType(error88),
104825
- "app.error.message": errorMessage(error88)
104826
- });
104827
- return null;
104828
- }
104829
104413
  }
104830
104414
  async submit({
104831
104415
  input,
@@ -104833,121 +104417,83 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
104833
104417
  ...params
104834
104418
  }) {
104835
104419
  await this.deps.validateDeveloperAccess(params.user, params.gameId);
104836
- const preview = await this.prepareSubmission(params, input);
104837
- const submission = await this.deps.assessmentRuntimeLock(this.deps.db, [assessmentRuntimeAttemptLockKey(params.attemptId)], async (db2) => {
104838
- const { attempt: identity, enrollment } = await this.resolveAttemptAuthorization(params, { db: db2, developerAccessValidated: true });
104839
- const submissionDisposition = classifyAssessmentSubmission({
104840
- inProgress: identity.result.inProgress ?? "",
104841
- scoreStatus: identity.result.scoreStatus,
104842
- submissionId: identity.metadata.submissionId
104843
- }, input.submissionId);
104844
- if (submissionDisposition === "replay") {
104420
+ const transcript = parseAssessmentTranscript(input.transcript);
104421
+ const request = { ...input, transcript };
104422
+ const requestHash = await assessmentTranscriptRequestHash(request);
104423
+ const accepted = await this.deps.assessmentRuntimeLock(this.deps.db, [assessmentRuntimeAttemptLockKey(params.attemptId)], async (db2) => {
104424
+ const { attempt } = await this.resolveAttemptAuthorization(params, {
104425
+ db: db2,
104426
+ developerAccessValidated: true,
104427
+ includeEnrollment: false
104428
+ });
104429
+ const disposition = resolveAssessmentTranscriptSubmission(attempt.result, attempt.metadata, attempt.integration.status, request, requestHash);
104430
+ if (disposition === "replay") {
104845
104431
  return {
104846
- response: this.submittedResult(identity.result, identity.metadata),
104847
- emission: this.replayCompletion(identity, input.submissionId, params, game2)
104432
+ response: this.submittedResult(attempt.result, attempt.metadata),
104433
+ emission: this.replayCompletion(attempt, input.submissionId, params, game2)
104848
104434
  };
104849
104435
  }
104850
- const attempt = this.requireActiveAttempt({ attempt: identity, enrollment }, params.attemptId);
104851
- if (submissionDisposition === "reject") {
104852
- throw AssessmentRuntimeError.from(assessmentAlreadySubmitted(attempt.result.sourcedId));
104853
- }
104854
- if (input.session && input.session.runId !== attempt.result.sourcedId) {
104855
- throw new ValidationError("Assessment session runId must match the attemptId.");
104856
- }
104857
- this.assertResponseVersion(attempt.metadata, input.expectedResponseVersion);
104858
- if (this.isPlatformRoutedDiagnosticMetadata(attempt.metadata)) {
104859
- return this.finalizeDiagnosticSubmission({
104860
- attempt: { ...attempt, metadata: attempt.metadata },
104861
- input,
104862
- params,
104863
- game: game2,
104864
- db: db2
104865
- });
104866
- }
104867
- if (assessmentFlowForPurpose(attempt.metadata.purpose) === "item-submit") {
104868
- const assessment2 = await this.loadAttemptAssessment(attempt.metadata);
104869
- const flowFailure = assessmentCompletionFlowFailure(attempt.metadata.purpose, attempt.metadata.itemSubmissions, assessment2.items.length);
104870
- if (flowFailure) {
104871
- throw AssessmentRuntimeError.from(flowFailure);
104872
- }
104873
- }
104874
- const scored = preview && preview.responseVersion === attempt.metadata.responseVersion ? preview : await this.scoreSubmission(attempt);
104875
- const { assessment, itemResults, score, correctQuestions } = scored;
104876
- const client2 = this.requireClient();
104436
+ const graded = await this.gradeTranscript({
104437
+ attemptId: params.attemptId,
104438
+ metadata: attempt.metadata
104439
+ }, transcript, true, db2);
104440
+ const diagnostic = this.isPlatformRoutedDiagnosticMetadata(graded.metadata) ? summarizeDiagnosticSubmission(graded.metadata, graded.manifest) : null;
104441
+ const itemOutcomes = graded.metadata.purpose === "review" ? assessmentReviewTranscriptOutcomes({
104442
+ metadata: graded.metadata,
104443
+ itemResults: graded.itemResults
104444
+ }) : undefined;
104445
+ const courseName = attempt.metadata.courseName ?? "Game Course";
104877
104446
  const timestamp6 = new Date().toISOString();
104878
- const reviewOutcomes = attempt.metadata.purpose === "review" ? this.reviewItemOutcomes({
104879
- metadata: attempt.metadata,
104880
- itemResults
104881
- }) : null;
104882
104447
  const finalization = buildAssessmentResultSubmission({
104883
104448
  result: attempt.result,
104884
- metadata: attempt.metadata,
104885
- score,
104449
+ metadata: graded.metadata,
104450
+ score: graded.score,
104886
104451
  submissionId: input.submissionId,
104887
104452
  timestamp: timestamp6,
104888
- testName: assessment.title,
104889
- courseName: attempt.enrollment.course.title,
104890
- totalQuestions: itemResults.length,
104891
- correctQuestions,
104453
+ testName: graded.assessment.title,
104454
+ courseName,
104455
+ totalQuestions: graded.itemResults.length,
104456
+ correctQuestions: graded.correctQuestions,
104892
104457
  ...input.session ? { session: input.session } : {},
104893
- ...reviewOutcomes ? { itemOutcomes: reviewOutcomes } : {}
104458
+ ...itemOutcomes ? { itemOutcomes } : {},
104459
+ ...diagnostic ? { diagnosticTracks: diagnostic.tracks } : {}
104894
104460
  });
104895
- const metadata2 = finalization.metadata;
104896
- const submitted = await client2.api.oneroster.assessmentResults.upsert(attempt.result.sourcedId, finalization.resultUpdate);
104897
- const submittedForEmission = {
104898
- ...submitted,
104899
- score: score.normalized * 100,
104900
- scoreDate: timestamp6,
104901
- ...ASSESSMENT_ATTEMPT_AWAITING_AWARD,
104902
- metadata: finalization.resultUpdate.metadata
104461
+ const metadata2 = {
104462
+ ...finalization.metadata,
104463
+ completion: { ...finalization.metadata.completion, requestHash }
104464
+ };
104465
+ const resultUpdate = {
104466
+ ...finalization.resultUpdate,
104467
+ metadata: mergeAssessmentRuntimeMetadata(attempt.result.metadata, metadata2)
104468
+ };
104469
+ await this.requireClient().api.oneroster.assessmentResults.upsert(params.attemptId, resultUpdate);
104470
+ const result = { ...attempt.result, ...resultUpdate };
104471
+ const emission = {
104472
+ attempt: { ...attempt, result, metadata: metadata2 },
104473
+ testIdentifier: graded.assessment.identifier,
104474
+ testName: graded.assessment.title,
104475
+ score: graded.score,
104476
+ totalQuestions: graded.itemResults.length,
104477
+ correctQuestions: graded.correctQuestions,
104478
+ submissionId: input.submissionId,
104479
+ gameId: params.gameId,
104480
+ studentId: params.studentId,
104481
+ courseName,
104482
+ game: game2
104903
104483
  };
104904
- addEvent("assessment.awaiting_award_created", {
104905
- "app.assessment.attempt_id": submittedForEmission.sourcedId,
104906
- "app.assessment.submission_id": input.submissionId,
104907
- "app.assessment.purpose": metadata2.purpose,
104908
- "app.assessment.response_version": metadata2.responseVersion
104909
- });
104910
104484
  return {
104911
- response: {
104912
- attemptId: submittedForEmission.sourcedId,
104913
- submissionId: input.submissionId,
104914
- responseVersion: metadata2.responseVersion,
104915
- status: "awaiting_award",
104916
- responses: metadata2.responses,
104917
- score,
104918
- ...metadata2.purpose === "review" ? {
104919
- purpose: metadata2.purpose,
104920
- itemOutcomes: reviewOutcomes
104921
- } : { purpose: metadata2.purpose }
104922
- },
104923
- emission: {
104924
- attempt: { ...attempt, result: submittedForEmission, metadata: metadata2 },
104925
- testIdentifier: assessment.identifier,
104926
- testName: assessment.title,
104927
- score,
104928
- totalQuestions: itemResults.length,
104929
- correctQuestions,
104930
- submissionId: input.submissionId,
104931
- gameId: params.gameId,
104932
- studentId: params.studentId,
104933
- courseName: attempt.enrollment.course.title,
104934
- game: game2
104935
- }
104485
+ response: this.submittedResult(result, metadata2),
104486
+ emission,
104487
+ repair: { context: this.tokenContext(attempt), graded }
104936
104488
  };
104937
104489
  });
104938
- if (submission.emission) {
104939
- const diagnosticCompletion = this.isPlatformRoutedDiagnosticMetadata(submission.emission.attempt.metadata) ? {
104940
- ...submission.emission.attempt,
104941
- metadata: submission.emission.attempt.metadata
104942
- } : null;
104943
- const { result, metadata: metadata2 } = submission.emission.attempt;
104944
- await Promise.allSettled([
104945
- this.emitSessionBestEffort(submission.emission),
104946
- ...diagnosticCompletion ? [this.restoreDiagnosticChildResults(diagnosticCompletion)] : [],
104947
- ...metadata2.purpose === "review" ? [this.restoreAllCompletedReviewChildResponses(result, metadata2)] : []
104948
- ]);
104490
+ if (accepted.repair) {
104491
+ this.reconcileTranscriptChildren(accepted.repair.context, accepted.repair.graded).catch((error88) => this.recordTranscriptFailure("completion_repair", params.attemptId, error88));
104492
+ }
104493
+ if (accepted.emission) {
104494
+ this.emitSessionBestEffort(accepted.emission);
104949
104495
  }
104950
- return submission.response;
104496
+ return accepted.response;
104951
104497
  }
104952
104498
  async finalize({
104953
104499
  input,
@@ -105091,58 +104637,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
105091
104637
  });
105092
104638
  }
105093
104639
  }
105094
- async finalizeDiagnosticSubmission(input) {
105095
- const { attempt } = input;
105096
- const routing = await this.loadAttemptDiagnosticManifest(attempt.metadata, input.db);
105097
- const { tracks, score, correctQuestions } = summarizeDiagnosticSubmission(attempt.metadata, routing.manifest);
105098
- const timestamp6 = new Date().toISOString();
105099
- const finalization = buildAssessmentResultSubmission({
105100
- result: attempt.result,
105101
- metadata: attempt.metadata,
105102
- score,
105103
- submissionId: input.input.submissionId,
105104
- timestamp: timestamp6,
105105
- testName: attempt.metadata.diagnostic.testName,
105106
- courseName: attempt.enrollment.course.title,
105107
- totalQuestions: attempt.metadata.itemSubmissions.length,
105108
- correctQuestions,
105109
- ...input.input.session ? { session: input.input.session } : {},
105110
- diagnosticTracks: tracks
105111
- });
105112
- const metadata2 = finalization.metadata;
105113
- const submitted = await this.requireClient().api.oneroster.assessmentResults.upsert(attempt.result.sourcedId, finalization.resultUpdate);
105114
- const submittedForEmission = {
105115
- ...submitted,
105116
- score: score.normalized * 100,
105117
- scoreDate: timestamp6,
105118
- ...ASSESSMENT_ATTEMPT_AWAITING_AWARD,
105119
- metadata: finalization.resultUpdate.metadata
105120
- };
105121
- return {
105122
- response: {
105123
- purpose: "diagnostic",
105124
- attemptId: submittedForEmission.sourcedId,
105125
- submissionId: input.input.submissionId,
105126
- diagnosticKey: metadata2.diagnostic.diagnosticKey,
105127
- responseVersion: metadata2.responseVersion,
105128
- status: "awaiting_award",
105129
- tracks
105130
- },
105131
- emission: {
105132
- attempt: { ...attempt, result: submittedForEmission, metadata: metadata2 },
105133
- testIdentifier: attempt.metadata.selectedTest.identifier,
105134
- testName: attempt.metadata.diagnostic.testName,
105135
- score,
105136
- totalQuestions: attempt.metadata.itemSubmissions.length,
105137
- correctQuestions,
105138
- submissionId: input.input.submissionId,
105139
- gameId: input.params.gameId,
105140
- studentId: input.params.studentId,
105141
- courseName: attempt.enrollment.course.title,
105142
- game: input.game
105143
- }
105144
- };
105145
- }
105146
104640
  async exportFixture({
105147
104641
  gameId,
105148
104642
  user
@@ -105263,28 +104757,11 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
105263
104757
  liveTestIntegrationIds: integrationIdsWithLiveTests
105264
104758
  };
105265
104759
  }
105266
- async selectIntegration(candidates, studentId, input, results) {
105267
- const {
105268
- rows: candidateRows,
105269
- enrollmentByCourse,
105270
- liveTestIntegrationIds: integrationIdsWithLiveTests
105271
- } = candidates;
105272
- const needsOpenAnnotation = candidateRows.length > 1 || candidateRows.some((row) => !integrationIdsWithLiveTests.has(row.id));
105273
- const openAttemptScope = {
105274
- studentId,
105275
- activityId: input.activityId,
105276
- purpose: input.purpose,
105277
- enrollmentIdByCourse: new Map(candidateRows.map((row) => [row.courseId, enrollmentByCourse.get(row.courseId).id])),
105278
- requestFingerprint: this.startRequestFingerprint(input)
105279
- };
105280
- let openAttemptCourseIds = new Set;
105281
- if (needsOpenAnnotation) {
105282
- openAttemptCourseIds = results ? this.openAttemptCourseIdsFromResults(results, openAttemptScope) : await this.openAttemptCourseIds(openAttemptScope);
105283
- }
104760
+ async selectIntegration(candidates, input) {
104761
+ const { rows: candidateRows, enrollmentByCourse, liveTestIntegrationIds } = candidates;
105284
104762
  const annotated = candidateRows.map((row) => ({
105285
104763
  ...row,
105286
- hasLiveTests: integrationIdsWithLiveTests.has(row.id),
105287
- hasOpenAttempt: openAttemptCourseIds.has(row.courseId)
104764
+ hasLiveTests: liveTestIntegrationIds.has(row.id)
105288
104765
  }));
105289
104766
  const selection = selectAssessmentCourseCandidate(annotated, Boolean(input.subject));
105290
104767
  if (selection.outcome === "none") {
@@ -105313,9 +104790,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
105313
104790
  standard: assessmentStandardForRow(row)
105314
104791
  })).filter((test) => !matchesMasteryStandard || matchesMasteryStandard(test.standard));
105315
104792
  }
105316
- async openAttemptCourseIds(scope) {
105317
- return this.openAttemptCourseIdsFromResults(await this.studentAssessmentResults(scope.studentId), scope);
105318
- }
105319
104793
  async studentAssessmentResults(studentId) {
105320
104794
  const stream = this.requireClient().api.oneroster.assessmentResults.stream({
105321
104795
  where: {
@@ -105329,29 +104803,37 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
105329
104803
  }
105330
104804
  return results;
105331
104805
  }
105332
- openAttemptCourseIdsFromResults(results, scope) {
105333
- const courseIds = new Set;
105334
- for (const result of results) {
105335
- const metadata2 = playcademyAssessmentResultMetadata(result.metadata);
105336
- if (metadata2 && metadata2.activityId === scope.activityId && metadata2.purpose === scope.purpose && this.attemptMatchesRequestFingerprint(metadata2, scope.requestFingerprint) && result.student.sourcedId === scope.studentId && metadata2.enrollmentId === scope.enrollmentIdByCourse.get(metadata2.courseId) && (this.isOpen(result) || this.isAwaitingAward(result))) {
105337
- courseIds.add(metadata2.courseId);
105338
- }
105339
- }
105340
- return courseIds;
105341
- }
105342
104806
  async listAttempts(scope) {
105343
104807
  return this.attemptListing(await this.studentAssessmentResults(scope.studentId), scope);
105344
104808
  }
105345
- attemptListing(results, scope) {
104809
+ async attemptListing(results, scope) {
105346
104810
  const attempts = new Map;
105347
104811
  const reviewChildren = new Map;
105348
104812
  for (const result of results) {
105349
104813
  const metadata2 = playcademyAssessmentResultMetadata(result.metadata);
105350
- const childMetadata = scope.purpose === "review" ? playcademyAssessmentItemResultMetadata(result.metadata) : null;
105351
- if (childMetadata && childMetadata.activityId === scope.activityId && childMetadata.integrationId === scope.integrationId && childMetadata.enrollmentId === scope.enrollmentId && result.student.sourcedId === scope.studentId) {
105352
- reviewChildren.set(result.sourcedId, result);
104814
+ if (scope.purpose === "review") {
104815
+ const verified = await verifyAssessmentChildResultCommand(result.metadata);
104816
+ if (verified.ok && verified.command.payload.context.kind === "review") {
104817
+ const { command } = verified;
104818
+ const { attempt, administration } = command.payload;
104819
+ const parent = await reviewQuestionLineItemId(await deterministicUUID([
104820
+ "playcademy:review-bank-line-item:v1",
104821
+ scope.integrationId,
104822
+ scope.activityId,
104823
+ attempt.selectedTest.identifier,
104824
+ verified.command.payload.context.bankRevision
104825
+ ].join("\x00")), administration.itemIdentifier);
104826
+ const projection = await classifyAssessmentChildResult({
104827
+ result,
104828
+ command,
104829
+ childLineItemId: parent
104830
+ });
104831
+ if (attempt.activityId === scope.activityId && attempt.integrationId === scope.integrationId && attempt.enrollmentId === scope.enrollmentId && attempt.courseId === scope.courseId && attempt.studentId === scope.studentId && result.sourcedId === await assessmentChildResultId(attempt.attemptId, administration.submissionId) && projection.outcome === "replay") {
104832
+ reviewChildren.set(result.sourcedId, command);
104833
+ }
104834
+ }
105353
104835
  }
105354
- if (metadata2 && metadata2.activityId === scope.activityId && metadata2.purpose === scope.purpose && metadata2.courseId === scope.courseId && metadata2.integrationId === scope.integrationId && result.student.sourcedId === scope.studentId) {
104836
+ if (metadata2 && this.attemptInScope(result, metadata2, scope)) {
105355
104837
  const selectedAssessmentKey = metadata2.selectedTest.assessmentKey ?? assessmentKeyFromManagedQtiIdentifier(metadata2.selectedTest.identifier);
105356
104838
  if (selectedAssessmentKey) {
105357
104839
  attempts.set(result.sourcedId, {
@@ -105360,12 +104842,9 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
105360
104842
  selection: {
105361
104843
  attemptId: result.sourcedId,
105362
104844
  selectedAssessmentKey,
105363
- selectedTestIdentifier: metadata2.selectedTest.identifier,
105364
104845
  inProgress: result.inProgress ?? "",
105365
104846
  scoreStatus: result.scoreStatus,
105366
- scoreDate: result.scoreDate,
105367
- updatedAt: result.dateLastModified ?? metadata2.updatedAt,
105368
- resumable: metadata2.enrollmentId === scope.enrollmentId
104847
+ scoreDate: result.scoreDate
105369
104848
  }
105370
104849
  });
105371
104850
  }
@@ -105373,147 +104852,61 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
105373
104852
  }
105374
104853
  return { attempts, reviewChildren };
105375
104854
  }
105376
- attemptsMatchingRequest(attempts, requestFingerprint) {
105377
- if (requestFingerprint === undefined) {
105378
- return attempts;
105379
- }
105380
- return new Map([...attempts].filter(([, attempt]) => this.attemptMatchesRequestFingerprint(attempt.metadata, requestFingerprint)));
105381
- }
105382
- startRequestFingerprint(input) {
105383
- if (input.purpose === "review") {
105384
- return reviewRequestFingerprint(input, DEFAULT_REVIEW_SELECTION_POLICY);
105385
- }
105386
- if (input.purpose === "mastery") {
105387
- return masteryRequestFingerprint(input);
105388
- }
105389
- return input.purpose === "diagnostic" ? input.diagnosticKey : undefined;
105390
- }
105391
- attemptMatchesRequestFingerprint(metadata2, requestFingerprint) {
105392
- if (metadata2.purpose === "review") {
105393
- return metadata2.review.requestFingerprint === requestFingerprint;
105394
- }
105395
- if (metadata2.purpose === "mastery") {
105396
- return metadata2.mastery.requestFingerprint === requestFingerprint;
105397
- }
105398
- if (metadata2.purpose === "diagnostic" && requestFingerprint !== undefined) {
105399
- return this.isPlatformRoutedDiagnosticMetadata(metadata2) && metadata2.diagnostic.diagnosticKey === requestFingerprint;
105400
- }
105401
- return true;
105402
- }
105403
- reviewExposures(results, scope) {
105404
- const exposures = [];
105405
- for (const result of results) {
105406
- const metadata2 = playcademyAssessmentItemResultMetadata(result.metadata);
105407
- if (metadata2?.bankRevision === scope.bankRevision) {
105408
- exposures.push({
105409
- itemIdentifier: metadata2.itemIdentifier,
105410
- deliveredAt: metadata2.deliveredAt
105411
- });
105412
- }
105413
- }
105414
- return exposures;
105415
- }
105416
- requireDecision(decision, purpose) {
104855
+ selectFreshStart(tests, listing, purpose) {
104856
+ const decision = selectRuntimeAssessment(tests, [...listing.attempts.values()].map((entry) => entry.selection));
105417
104857
  if (!decision) {
105418
104858
  throw AssessmentRuntimeError.from(assessmentNoEligibleTests(purpose));
105419
104859
  }
105420
104860
  return decision;
105421
104861
  }
105422
- resumeSnapshot(decision, attempts, integration) {
105423
- if (decision.additionalResumableAttemptIds.length > 0) {
105424
- addEvent("assessment.multiple_resumable_attempts", {
105425
- "app.assessment.attempt_id": decision.attempt.attemptId,
105426
- "app.assessment.additional_attempt_ids": decision.additionalResumableAttemptIds.join(",")
105427
- });
105428
- }
105429
- const resumed = attempts.get(decision.attempt.attemptId);
105430
- return this.snapshotForResult(resumed.result, resumed.metadata, integration);
104862
+ pendingAwardAttempt(listing, enrollmentId) {
104863
+ const pending = [...listing.attempts.values()].filter((attempt) => attempt.metadata.enrollmentId === enrollmentId && this.isAwaitingAward(attempt.result));
104864
+ return pending.toSorted((left, right) => compareNewestCompletedAssessmentAttempt(left.selection, right.selection))[0] ?? null;
105431
104865
  }
105432
- async resumeDiagnosticSnapshot(decision, attempts, integration, db2 = this.deps.db) {
105433
- if (decision.additionalResumableAttemptIds.length > 0) {
105434
- addEvent("assessment.multiple_resumable_attempts", {
105435
- "app.assessment.attempt_id": decision.attempt.attemptId,
105436
- "app.assessment.additional_attempt_ids": decision.additionalResumableAttemptIds.join(",")
105437
- });
105438
- }
105439
- const resumed = attempts.get(decision.attempt.attemptId);
105440
- if (this.isSettled(resumed.result)) {
105441
- return this.snapshotForResult(resumed.result, resumed.metadata, integration);
105442
- }
105443
- if (!this.isPlatformRoutedDiagnosticMetadata(resumed.metadata)) {
105444
- throw new Error("A keyed diagnostic request selected a legacy fixed attempt");
104866
+ async pendingAwardSnapshot(listing, context2) {
104867
+ const pending = this.pendingAwardAttempt(listing, context2.enrollment.id);
104868
+ if (!pending) {
104869
+ return null;
105445
104870
  }
105446
- const assessment = await this.loadAttemptAssessment(resumed.metadata);
105447
- const routing = await this.loadAttemptDiagnosticRouting(resumed.metadata, assessment, db2);
105448
- await this.restoreDiagnosticChildResults({
105449
- result: resumed.result,
105450
- metadata: resumed.metadata,
105451
- integration,
105452
- assessment
104871
+ addEvent("assessment.pending_award_returned", {
104872
+ "app.assessment.attempt_id": pending.result.sourcedId
105453
104873
  });
105454
- return this.diagnosticSnapshot(resumed.result, resumed.metadata, assessment, integration, routing.state);
105455
- }
105456
- async resumeAvailableAttempt(tests, attempts, requestFingerprint, pendingSupersessions, resume) {
105457
- const select3 = () => selectRuntimeAssessment(tests, [...this.attemptsMatchingRequest(attempts, requestFingerprint).values()].map((entry) => entry.selection));
105458
- return resolveAvailableAssessmentAttempt(select3(), select3, (decision) => this.resumeSnapshotOrStageSupersession(decision, attempts, pendingSupersessions, () => resume(decision)));
104874
+ return this.snapshotForResult(pending.result, pending.metadata, context2.integration);
105459
104875
  }
105460
- async resumeSnapshotOrStageSupersession(decision, attempts, pendingSupersessions, resume) {
105461
- try {
105462
- return await resume();
105463
- } catch (error88) {
105464
- if (!(error88 instanceof AssessmentRuntimeError) || error88.runtimeCode !== "SELECTED_TEST_VERSION_UNAVAILABLE") {
105465
- throw error88;
104876
+ replacedAttemptIds(attempts, enrollmentId) {
104877
+ const replaced = new Set;
104878
+ for (const attempt of attempts.values()) {
104879
+ if (attempt.metadata.enrollmentId === enrollmentId && this.isOpen(attempt.result)) {
104880
+ replaced.add(attempt.result.sourcedId);
105466
104881
  }
105467
- if (shouldConfirmAssessmentVersionUnavailable(error88.details)) {
105468
- await sleep(TimebackAssessmentRuntimeService.ASSESSMENT_VERSION_CONFIRMATION_DELAY_MS);
105469
- try {
105470
- return await resume();
105471
- } catch (confirmationError) {
105472
- if (!(confirmationError instanceof AssessmentRuntimeError) || confirmationError.runtimeCode !== "SELECTED_TEST_VERSION_UNAVAILABLE") {
105473
- throw confirmationError;
105474
- }
105475
- }
105476
- }
105477
- const staged = attempts.get(decision.attempt.attemptId);
105478
- pendingSupersessions.add(staged.result.sourcedId);
105479
- stageAssessmentAttemptSupersession(staged);
105480
- addEvent("assessment.attempt_supersession_staged", {
105481
- "app.assessment.attempt_id": decision.attempt.attemptId,
105482
- "app.assessment.reason": "selected_test_version_unavailable"
105483
- });
105484
- return null;
105485
104882
  }
104883
+ return replaced;
104884
+ }
104885
+ supersedeReplacedAttempts(db2, attempts, enrollmentId) {
104886
+ return this.supersedeAttemptsBestEffort(db2, this.replacedAttemptIds(attempts, enrollmentId));
105486
104887
  }
105487
- async persistPendingSupersessions(db2, attemptIds) {
104888
+ async supersedeAttempts(db2, attemptIds) {
105488
104889
  for (const attemptId of [...attemptIds].toSorted()) {
105489
104890
  await this.deps.assessmentRuntimeLock(db2, [assessmentRuntimeAttemptLockKey(attemptId)], async () => {
105490
- let result;
105491
- try {
105492
- result = await this.requireClient().api.oneroster.assessmentResults.get(attemptId);
105493
- } catch (error88) {
105494
- if (isApiError(error88) && error88.statusCode === 404) {
105495
- return;
105496
- }
105497
- throw error88;
105498
- }
105499
- const update2 = buildAssessmentAttemptSupersessionUpdate(result);
104891
+ const result = await this.findAttemptResult(attemptId);
104892
+ const update2 = result && buildAssessmentAttemptSupersessionUpdate(result);
105500
104893
  if (!update2) {
105501
104894
  return;
105502
104895
  }
105503
104896
  await this.requireClient().api.oneroster.assessmentResults.upsert(attemptId, update2);
105504
104897
  addEvent("assessment.attempt_superseded", {
105505
104898
  "app.assessment.attempt_id": attemptId,
105506
- "app.assessment.reason": "selected_test_version_unavailable"
104899
+ "app.assessment.reason": "replaced_by_fresh_start"
105507
104900
  });
105508
104901
  });
105509
104902
  }
105510
104903
  }
105511
- async persistPendingSupersessionsBestEffort(db2, attemptIds) {
104904
+ async supersedeAttemptsBestEffort(db2, attemptIds) {
105512
104905
  if (attemptIds.size === 0) {
105513
104906
  return;
105514
104907
  }
105515
104908
  try {
105516
- await this.persistPendingSupersessions(db2, attemptIds);
104909
+ await this.supersedeAttempts(db2, attemptIds);
105517
104910
  } catch (error88) {
105518
104911
  addEvent("assessment.attempt_supersession_persist_failed", {
105519
104912
  "app.assessment.attempt_ids": [...attemptIds].toSorted().join(","),
@@ -105522,43 +104915,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
105522
104915
  });
105523
104916
  }
105524
104917
  }
105525
- async resumeReviewSnapshot(decision, attempts, existingChildResults, context2) {
105526
- if (decision.additionalResumableAttemptIds.length > 0) {
105527
- addEvent("assessment.multiple_resumable_attempts", {
105528
- "app.assessment.attempt_id": decision.attempt.attemptId,
105529
- "app.assessment.additional_attempt_ids": decision.additionalResumableAttemptIds.join(",")
105530
- });
105531
- }
105532
- const resumed = attempts.get(decision.attempt.attemptId);
105533
- if (resumed.metadata.purpose !== "review") {
105534
- throw new Error("A standards-review request selected a non-review attempt");
105535
- }
105536
- const reviewMetadata = resumed.metadata;
105537
- let assessment;
105538
- const current = await reconcileReviewChildResults({
105539
- initial: resumed,
105540
- writeOpen: async () => {
105541
- const source = await this.loadPinnedReviewBankSource(reviewMetadata);
105542
- assessment = projectReviewAssessment(source.assessment, source.bank, reviewMetadata.review.selections);
105543
- await this.ensureReviewChildResults({
105544
- result: resumed.result,
105545
- metadata: reviewMetadata,
105546
- integration: context2.integration,
105547
- source,
105548
- assessment,
105549
- existingChildResults,
105550
- lookupMissingResults: true
105551
- });
105552
- },
105553
- crossBarrier: () => crossAssessmentAttemptLockBarrier(this.deps.assessmentRuntimeLock, this.deps.db, resumed.result.sourcedId),
105554
- readLatest: () => this.peekAttempt({
105555
- studentId: resumed.result.student.sourcedId,
105556
- attemptId: resumed.result.sourcedId
105557
- }),
105558
- restoreTerminal: (latest) => this.restoreAllCompletedReviewChildResponses(latest.result, latest.metadata)
105559
- });
105560
- return assessmentChildProjectionIsTerminal(current.result) ? this.snapshotForResult(current.result, current.metadata, context2.integration) : this.snapshot(current.result, current.metadata, assessment, context2.integration);
105561
- }
105562
104918
  recordReviewPreparationPhase(phase, startedAt, counts = {}) {
105563
104919
  addEvent("assessment.review_preparation_phase", {
105564
104920
  "app.assessment.review_phase": phase,
@@ -105909,12 +105265,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
105909
105265
  }
105910
105266
  });
105911
105267
  } catch (error88) {
105912
- if (!isApiError(error88) || error88.statusCode !== 409) {
105913
- throw error88;
105914
- }
105915
- addEvent("assessment.line_item_create_conflict", {
105916
- "app.assessment.line_item_id": lineItemId
105917
- });
105268
+ await this.confirmLineItemAfterRefusedCreate(lineItemId, error88, "assessment.line_item_create_conflict", () => client2.api.oneroster.assessmentLineItems.get(lineItemId));
105918
105269
  }
105919
105270
  return lineItemId;
105920
105271
  }
@@ -105956,144 +105307,99 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
105956
105307
  }
105957
105308
  });
105958
105309
  } catch (error88) {
105959
- if (!isApiError(error88) || error88.statusCode !== 409) {
105960
- throw error88;
105961
- }
105962
- addEvent("assessment.review_bank_line_item_create_conflict", {
105963
- "app.assessment.line_item_id": lineItemId
105964
- });
105310
+ await this.confirmLineItemAfterRefusedCreate(lineItemId, error88, "assessment.review_bank_line_item_create_conflict", () => client2.api.oneroster.assessmentLineItems.get(lineItemId));
105965
105311
  }
105966
105312
  return lineItemId;
105967
105313
  }
105968
- assessmentChildAttempt(attempt) {
105969
- const { metadata: metadata2 } = attempt;
105970
- const assessmentKey = metadata2.selectedTest.assessmentKey ?? assessmentKeyFromManagedQtiIdentifier(metadata2.selectedTest.identifier);
105971
- if (!assessmentKey) {
105972
- addEvent("assessment.child_result_legacy_attempt_skipped", {
105973
- "app.assessment.attempt_id": attempt.result.sourcedId
105974
- });
105975
- return null;
105314
+ async confirmLineItemAfterRefusedCreate(lineItemId, createError, event, read) {
105315
+ let stored;
105316
+ try {
105317
+ stored = await read();
105318
+ } catch (readError) {
105319
+ if (isApiError(readError) && readError.statusCode === 404) {
105320
+ throw createError;
105321
+ }
105322
+ throw readError;
105976
105323
  }
105324
+ addEvent(event, {
105325
+ "app.assessment.line_item_id": lineItemId,
105326
+ ...isApiError(createError) ? { "app.timeback.status": createError.statusCode } : {}
105327
+ });
105328
+ return stored;
105329
+ }
105330
+ tokenContext(attempt) {
105977
105331
  return {
105978
- attemptId: attempt.result.sourcedId,
105332
+ version: 1,
105979
105333
  gameId: attempt.integration.gameId,
105980
105334
  studentId: attempt.result.student.sourcedId,
105981
- activityId: metadata2.activityId,
105982
- courseId: metadata2.courseId,
105983
- integrationId: metadata2.integrationId,
105984
- enrollmentId: metadata2.enrollmentId,
105985
- selectedTest: {
105986
- assessmentKey,
105987
- identifier: metadata2.selectedTest.identifier,
105988
- contentRevision: metadata2.selectedTest.contentRevision
105989
- }
105990
- };
105991
- }
105992
- reviewChildResultContext(metadata2, submission) {
105993
- const selection = metadata2.review.selections.find((candidate) => candidate.itemIdentifier === submission.itemIdentifier);
105994
- if (!selection) {
105995
- throw new Error(`Committed review item ${submission.itemIdentifier} is not in its pinned selection`);
105996
- }
105997
- return {
105998
- kind: "review",
105999
- bankRevision: metadata2.review.bankRevision,
106000
- standard: selection.standard
105335
+ attemptId: attempt.result.sourcedId,
105336
+ parentLineItemId: attempt.result.assessmentLineItem.sourcedId,
105337
+ metadata: assessmentTokenMetadata(attempt.metadata)
106001
105338
  };
106002
105339
  }
106003
- prepareReviewAssessmentChildResult(input) {
106004
- return this.prepareAssessmentChildResult({
106005
- attempt: input.attempt,
106006
- metadata: input.metadata,
106007
- submission: input.submission,
106008
- context: () => this.reviewChildResultContext(input.metadata, input.submission)
105340
+ async ensureTranscriptLineItems(context2, assessment) {
105341
+ await runWithConcurrency(assessment.items, TimebackAssessmentRuntimeService.SCORING_CONCURRENCY, async (item) => {
105342
+ const id = await assessmentTranscriptChildLineItemId(context2, item.identifier);
105343
+ if (this.childLineItemCache.get(id)) {
105344
+ return;
105345
+ }
105346
+ const metadata2 = context2.metadata;
105347
+ const common2 = {
105348
+ parentLineItemId: context2.parentLineItemId,
105349
+ integration: { courseId: metadata2.courseId },
105350
+ item
105351
+ };
105352
+ if (metadata2.purpose === "review") {
105353
+ const standard = metadata2.review.selections.find((selection) => selection.itemIdentifier === item.identifier)?.standard;
105354
+ if (!standard) {
105355
+ throw new Error("Review candidate is not in the pinned selection");
105356
+ }
105357
+ await this.ensureReviewQuestionLineItem({
105358
+ ...common2,
105359
+ bankRevision: metadata2.review.bankRevision,
105360
+ standards: [standard]
105361
+ });
105362
+ } else if (this.isPlatformRoutedDiagnosticMetadata(metadata2)) {
105363
+ await this.ensureDiagnosticQuestionLineItem({ ...common2, metadata: metadata2 });
105364
+ } else {
105365
+ const expected = {
105366
+ status: "active",
105367
+ parentAssessmentLineItem: { sourcedId: context2.parentLineItemId },
105368
+ course: { sourcedId: metadata2.courseId },
105369
+ resultValueMin: 0,
105370
+ resultValueMax: item.maxScore,
105371
+ metadata: {
105372
+ playcademyAssessmentQuestionDefinition: {
105373
+ version: 1,
105374
+ itemIdentifier: item.identifier,
105375
+ contentRevision: metadata2.selectedTest.contentRevision
105376
+ }
105377
+ }
105378
+ };
105379
+ const store = this.requireClient().api.oneroster.assessmentLineItems;
105380
+ try {
105381
+ await store.create({ sourcedId: id, title: item.title, ...expected });
105382
+ } catch (error88) {
105383
+ this.assertAssessmentChildLineItem(id, await this.confirmLineItemAfterRefusedCreate(id, error88, "assessment.question_line_item_create_conflict", () => store.get(id)), expected);
105384
+ }
105385
+ }
105386
+ this.childLineItemCache.set(id, true);
106009
105387
  });
106010
105388
  }
106011
- prepareDiagnosticAssessmentChildResult(input) {
106012
- return this.prepareAssessmentChildResult({
106013
- attempt: input.attempt,
106014
- metadata: input.metadata,
106015
- submission: input.submission,
106016
- context: () => ({
106017
- kind: "platform-routed-diagnostic",
106018
- definitionId: input.metadata.diagnostic.definitionId,
106019
- diagnosticKey: input.metadata.diagnostic.diagnosticKey,
106020
- routingRevision: input.metadata.diagnostic.routingRevision,
106021
- transition: input.transition
105389
+ async reconcileTranscriptChildren(context2, graded, only) {
105390
+ return reconcileAssessmentTranscriptChildren({
105391
+ context: context2,
105392
+ graded,
105393
+ only,
105394
+ assessmentResults: this.requireClient().api.oneroster.assessmentResults,
105395
+ ensureLineItems: (assessment) => this.ensureTranscriptLineItems(context2, assessment),
105396
+ onPersisted: (submissionId) => addEvent("assessment.child_result_persisted", {
105397
+ "app.assessment.attempt_id": context2.attemptId,
105398
+ "app.assessment.submission_id": submissionId,
105399
+ "app.assessment.child_result_outcome": "upserted"
106022
105400
  })
106023
105401
  });
106024
105402
  }
106025
- async prepareAssessmentChildResult(input) {
106026
- try {
106027
- const attemptContext = this.assessmentChildAttempt({
106028
- ...input.attempt,
106029
- metadata: input.metadata
106030
- });
106031
- if (!attemptContext) {
106032
- return null;
106033
- }
106034
- const parentLineItemId = input.attempt.result.assessmentLineItem.sourcedId;
106035
- const { itemIdentifier } = input.submission;
106036
- const context2 = input.context();
106037
- const [command, childLineItemId] = await Promise.all([
106038
- buildSubmissionChildResultCommand({
106039
- attempt: attemptContext,
106040
- submission: input.submission,
106041
- responses: input.metadata.responses[itemIdentifier],
106042
- grading: input.submission.grading ?? QTI_CHILD_GRADING,
106043
- context: context2
106044
- }),
106045
- context2.kind === "review" ? reviewQuestionLineItemId(parentLineItemId, itemIdentifier) : diagnosticQuestionLineItemId(parentLineItemId, itemIdentifier)
106046
- ]);
106047
- return { command, childLineItemId };
106048
- } catch (error88) {
106049
- addEvent("assessment.child_result_prepare_failed", {
106050
- "app.assessment.attempt_id": input.attempt.result.sourcedId,
106051
- "app.assessment.submission_id": input.submission.submissionId,
106052
- "app.assessment.qti_item_identifier": input.submission.itemIdentifier,
106053
- "exception.type": errorType(error88),
106054
- "app.error.message": errorMessage(error88)
106055
- });
106056
- return null;
106057
- }
106058
- }
106059
- async projectPreparedAssessmentChildResult(prepared) {
106060
- if (!prepared) {
106061
- return;
106062
- }
106063
- const startedAt = Date.now();
106064
- const identity = {
106065
- "app.assessment.attempt_id": prepared.command.payload.attempt.attemptId,
106066
- "app.assessment.submission_id": prepared.command.payload.administration.submissionId
106067
- };
106068
- try {
106069
- const disposition = await persistAssessmentChildResult({
106070
- assessmentResults: this.requireClient().api.oneroster.assessmentResults,
106071
- command: prepared.command,
106072
- childLineItemId: prepared.childLineItemId
106073
- });
106074
- if (disposition.outcome === "conflict") {
106075
- addEvent("assessment.child_result_conflict", {
106076
- ...identity,
106077
- "app.assessment.child_result_id": disposition.resultId,
106078
- "app.assessment.child_result_conflict_reason": disposition.reason
106079
- });
106080
- return;
106081
- }
106082
- addEvent("assessment.child_result_persisted", {
106083
- ...identity,
106084
- "app.assessment.child_result_id": disposition.resultId,
106085
- "app.assessment.child_result_outcome": disposition.outcome,
106086
- "app.assessment.duration_ms": Date.now() - startedAt
106087
- });
106088
- } catch (error88) {
106089
- addEvent("assessment.child_result_persist_failed", {
106090
- ...identity,
106091
- "app.assessment.duration_ms": Date.now() - startedAt,
106092
- "exception.type": errorType(error88),
106093
- "app.error.message": errorMessage(error88)
106094
- });
106095
- }
106096
- }
106097
105403
  assertAssessmentChildLineItem(lineItemId, stored, expected) {
106098
105404
  if (assessmentChildLineItemMatches(stored, expected)) {
106099
105405
  return;
@@ -106140,102 +105446,11 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
106140
105446
  ...expected
106141
105447
  });
106142
105448
  } catch (error88) {
106143
- if (!isApiError(error88) || error88.statusCode !== 409) {
106144
- throw error88;
106145
- }
106146
- const stored = await client2.api.oneroster.assessmentLineItems.get(lineItemId);
105449
+ const stored = await this.confirmLineItemAfterRefusedCreate(lineItemId, error88, "assessment.diagnostic_question_line_item_create_conflict", () => client2.api.oneroster.assessmentLineItems.get(lineItemId));
106147
105450
  this.assertAssessmentChildLineItem(lineItemId, stored, expected);
106148
- addEvent("assessment.diagnostic_question_line_item_create_conflict", {
106149
- "app.assessment.line_item_id": lineItemId
106150
- });
106151
105451
  }
106152
105452
  return lineItemId;
106153
105453
  }
106154
- async projectDiagnosticItemResponse(projection, assessment, checkpoint = null) {
106155
- try {
106156
- const loadedAssessment = assessment ?? await this.loadAttemptAssessment(projection.metadata);
106157
- const item = loadedAssessment.items.find((candidate) => candidate.identifier === projection.ledgerEntry.itemIdentifier);
106158
- if (!item) {
106159
- throw new Error(`Diagnostic assessment is missing item ${projection.ledgerEntry.itemIdentifier}`);
106160
- }
106161
- const childLineItemId = await this.ensureDiagnosticQuestionLineItem({
106162
- parentLineItemId: projection.result.assessmentLineItem.sourcedId,
106163
- integration: projection.integration,
106164
- metadata: projection.metadata,
106165
- item
106166
- });
106167
- if (checkpoint && checkpoint.childLineItemId !== childLineItemId) {
106168
- throw new Error("Prepared diagnostic checkpoint resolved a different line item");
106169
- }
106170
- const childResultId = await diagnosticQuestionResultId(projection.result.sourcedId, childLineItemId);
106171
- const finalized = buildFinalizedDiagnosticChildResult({
106172
- childLineItemId,
106173
- student: projection.result.student,
106174
- parentAttemptId: projection.result.sourcedId,
106175
- metadata: projection.metadata,
106176
- ledgerEntry: projection.ledgerEntry
106177
- });
106178
- await Promise.all([
106179
- this.projectPreparedAssessmentChildResult(checkpoint),
106180
- this.requireClient().api.oneroster.assessmentResults.upsert(childResultId, finalized.resultUpdate)
106181
- ]);
106182
- } catch (error88) {
106183
- addEvent("assessment.diagnostic_child_projection_failed", {
106184
- "app.assessment.attempt_id": projection.result.sourcedId,
106185
- "app.assessment.qti_item_identifier": projection.ledgerEntry.itemIdentifier,
106186
- "exception.type": errorType(error88),
106187
- "app.error.message": errorMessage(error88)
106188
- });
106189
- }
106190
- }
106191
- async restoreDiagnosticChildResults(attempt) {
106192
- const assessment = attempt.assessment ?? await this.loadAttemptAssessment(attempt.metadata);
106193
- await runWithConcurrency(attempt.metadata.diagnostic.ledger, TimebackAssessmentRuntimeService.SCORING_CONCURRENCY, async (ledgerEntry) => {
106194
- try {
106195
- const childLineItemId = await diagnosticQuestionLineItemId(attempt.result.assessmentLineItem.sourcedId, ledgerEntry.itemIdentifier);
106196
- const childResultId = await diagnosticQuestionResultId(attempt.result.sourcedId, childLineItemId);
106197
- let childResult;
106198
- try {
106199
- childResult = await this.requireClient().api.oneroster.assessmentResults.get(childResultId);
106200
- } catch (error88) {
106201
- if (!isApiError(error88) || error88.statusCode !== 404) {
106202
- throw error88;
106203
- }
106204
- }
106205
- const resolution = resolveDiagnosticChildResult({
106206
- childResult,
106207
- childLineItemId,
106208
- parentResult: attempt.result,
106209
- parentMetadata: attempt.metadata,
106210
- ledgerEntry
106211
- });
106212
- if (resolution.action === "reuse") {
106213
- return;
106214
- }
106215
- if (resolution.action === "restore") {
106216
- addEvent("assessment.diagnostic_child_result_restore", {
106217
- "app.assessment.diagnostic_child_restore_reason": resolution.reason,
106218
- "app.assessment.attempt_id": attempt.result.sourcedId,
106219
- "app.assessment.child_result_id": childResultId,
106220
- "app.assessment.qti_item_identifier": ledgerEntry.itemIdentifier
106221
- });
106222
- }
106223
- await this.projectDiagnosticItemResponse({
106224
- result: attempt.result,
106225
- metadata: attempt.metadata,
106226
- ledgerEntry,
106227
- integration: attempt.integration
106228
- }, assessment);
106229
- } catch (error88) {
106230
- addEvent("assessment.diagnostic_child_reconciliation_failed", {
106231
- "app.assessment.attempt_id": attempt.result.sourcedId,
106232
- "app.assessment.qti_item_identifier": ledgerEntry.itemIdentifier,
106233
- "exception.type": errorType(error88),
106234
- "app.error.message": errorMessage(error88)
106235
- });
106236
- }
106237
- });
106238
- }
106239
105454
  async ensureReviewQuestionLineItem(input) {
106240
105455
  const client2 = this.requireClient();
106241
105456
  const lineItemId = await reviewQuestionLineItemId(input.parentLineItemId, input.item.identifier);
@@ -106271,231 +105486,11 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
106271
105486
  ...expected
106272
105487
  });
106273
105488
  } catch (error88) {
106274
- if (!isApiError(error88) || error88.statusCode !== 409) {
106275
- throw error88;
106276
- }
106277
- const stored = await client2.api.oneroster.assessmentLineItems.get(lineItemId);
105489
+ const stored = await this.confirmLineItemAfterRefusedCreate(lineItemId, error88, "assessment.review_question_line_item_create_conflict", () => client2.api.oneroster.assessmentLineItems.get(lineItemId));
106278
105490
  this.assertAssessmentChildLineItem(lineItemId, stored, expected);
106279
- addEvent("assessment.review_question_line_item_create_conflict", {
106280
- "app.assessment.line_item_id": lineItemId
106281
- });
106282
105491
  }
106283
105492
  return lineItemId;
106284
105493
  }
106285
- async putOpenReviewChildResult(input) {
106286
- const metadata2 = buildReviewItemResultMetadata({
106287
- metadata: input.parentMetadata,
106288
- selection: input.selection,
106289
- parentAttemptId: input.parentResult.sourcedId
106290
- });
106291
- await this.requireClient().api.oneroster.assessmentResults.upsert(input.childResultId, buildOpenReviewChildResult({
106292
- childLineItemId: input.childLineItemId,
106293
- student: input.parentResult.student,
106294
- metadata: metadata2
106295
- }));
106296
- }
106297
- async ensureReviewChildResults(input) {
106298
- const itemByIdentifier = new Map(input.assessment.items.map((item) => [item.identifier, item]));
106299
- const bankItemByIdentifier = new Map(input.source.bank.items.map((item) => [item.itemIdentifier, item]));
106300
- const physicalItems = new Set(input.metadata.review.selections.map((selection) => selection.itemIdentifier));
106301
- if (physicalItems.size !== input.metadata.review.selections.length) {
106302
- throw new Error("MVP review selection must use each physical item at most once");
106303
- }
106304
- const administeredItems = administeredItemIdentifiers(input.metadata.itemSubmissions);
106305
- await runWithConcurrency(input.metadata.review.selections, TimebackAssessmentRuntimeService.SCORING_CONCURRENCY, async (selection) => {
106306
- const item = itemByIdentifier.get(selection.itemIdentifier);
106307
- const indexed = bankItemByIdentifier.get(selection.itemIdentifier);
106308
- if (!item || !indexed) {
106309
- throw new Error(`Review assessment or bank is missing selected item ${selection.itemIdentifier}`);
106310
- }
106311
- const childLineItemId = await this.ensureReviewQuestionLineItem({
106312
- parentLineItemId: input.result.assessmentLineItem.sourcedId,
106313
- integration: input.integration,
106314
- bankRevision: input.metadata.review.bankRevision,
106315
- standards: [...indexed.standards],
106316
- item
106317
- });
106318
- if (!administeredItems.has(selection.itemIdentifier)) {
106319
- return;
106320
- }
106321
- const childResultId = await reviewQuestionResultId(input.result.sourcedId, childLineItemId);
106322
- const streamedChildResult = input.existingChildResults.get(childResultId);
106323
- let childResult = streamedChildResult;
106324
- if (!childResult && input.lookupMissingResults) {
106325
- try {
106326
- childResult = await this.requireClient().api.oneroster.assessmentResults.get(childResultId);
106327
- } catch (error88) {
106328
- if (!isApiError(error88) || error88.statusCode !== 404) {
106329
- throw error88;
106330
- }
106331
- }
106332
- }
106333
- const resolution = resolveReviewChildResult({
106334
- childResult,
106335
- childLineItemId,
106336
- parentResult: input.result,
106337
- parentMetadata: input.metadata,
106338
- selection
106339
- });
106340
- if (resolution.action === "reuse") {
106341
- return;
106342
- }
106343
- if (resolution.action === "restore") {
106344
- addEvent("assessment.review_child_result_restore", {
106345
- "app.assessment.review_child_restore_reason": resolution.reason,
106346
- "app.assessment.attempt_id": input.result.sourcedId,
106347
- "app.assessment.child_result_id": childResultId,
106348
- "app.assessment.qti_item_identifier": selection.itemIdentifier
106349
- });
106350
- }
106351
- await this.putOpenReviewChildResult({
106352
- childResultId,
106353
- childLineItemId,
106354
- parentResult: input.result,
106355
- parentMetadata: input.metadata,
106356
- selection
106357
- });
106358
- });
106359
- }
106360
- async putReviewChildResponse(projection, checkpoint) {
106361
- const { result, metadata: metadata2, itemIdentifier, integration } = projection;
106362
- const selection = metadata2.review.selections.find((candidate) => candidate.itemIdentifier === itemIdentifier);
106363
- if (!selection) {
106364
- return;
106365
- }
106366
- try {
106367
- const source = await this.loadPinnedReviewBankSource(metadata2);
106368
- const item = source.assessment.items.find((candidate) => candidate.identifier === selection.itemIdentifier);
106369
- const indexed = source.bank.items.find((candidate) => candidate.itemIdentifier === selection.itemIdentifier);
106370
- if (!item || !indexed) {
106371
- throw new Error(`Review assessment or bank is missing selected item ${selection.itemIdentifier}`);
106372
- }
106373
- const childLineItemId = await this.ensureReviewQuestionLineItem({
106374
- parentLineItemId: result.assessmentLineItem.sourcedId,
106375
- integration,
106376
- bankRevision: metadata2.review.bankRevision,
106377
- standards: [...indexed.standards],
106378
- item
106379
- });
106380
- if (checkpoint && checkpoint.childLineItemId !== childLineItemId) {
106381
- throw new Error("Prepared review checkpoint resolved a different line item");
106382
- }
106383
- const childResultId = await reviewQuestionResultId(result.sourcedId, childLineItemId);
106384
- const childMetadata = buildReviewItemResultMetadata({
106385
- metadata: metadata2,
106386
- selection,
106387
- parentAttemptId: result.sourcedId
106388
- });
106389
- await Promise.all([
106390
- this.projectPreparedAssessmentChildResult(checkpoint),
106391
- this.requireClient().api.oneroster.assessmentResults.upsert(childResultId, buildOpenReviewChildResult({
106392
- childLineItemId,
106393
- student: result.student,
106394
- metadata: childMetadata
106395
- }))
106396
- ]);
106397
- } catch (error88) {
106398
- addEvent("assessment.review_child_response_projection_failed", {
106399
- "app.assessment.attempt_id": result.sourcedId,
106400
- "app.assessment.qti_item_identifier": selection.itemIdentifier,
106401
- "exception.type": errorType(error88),
106402
- "app.error.message": errorMessage(error88)
106403
- });
106404
- }
106405
- }
106406
- async projectReviewItemResponse(params, projection, checkpoint) {
106407
- const terminal = assessmentChildProjectionIsTerminal(projection.result);
106408
- try {
106409
- await reconcileReviewChildResults({
106410
- initial: projection,
106411
- writeOpen: () => this.putReviewChildResponse(projection, checkpoint),
106412
- crossBarrier: () => crossAssessmentAttemptLockBarrier(this.deps.assessmentRuntimeLock, this.deps.db, params.attemptId),
106413
- readLatest: () => this.peekAttempt(params),
106414
- restoreTerminal: async (latest) => {
106415
- const repair = this.restoreCompletedReviewChildResponses(latest.result, latest.metadata, new Set([projection.itemIdentifier]));
106416
- if (terminal) {
106417
- await Promise.allSettled([
106418
- this.projectPreparedAssessmentChildResult(checkpoint),
106419
- repair
106420
- ]);
106421
- } else {
106422
- await repair;
106423
- }
106424
- }
106425
- });
106426
- } catch (error88) {
106427
- addEvent("assessment.review_child_projection_barrier_failed", {
106428
- "app.assessment.attempt_id": params.attemptId,
106429
- "app.assessment.qti_item_identifier": projection.itemIdentifier,
106430
- "exception.type": errorType(error88),
106431
- "app.error.message": errorMessage(error88)
106432
- });
106433
- }
106434
- }
106435
- restoreAllCompletedReviewChildResponses(result, metadata2) {
106436
- return this.restoreCompletedReviewChildResponses(result, metadata2, new Set(metadata2.itemSubmissions.map((item) => item.itemIdentifier)));
106437
- }
106438
- async restoreCompletedReviewChildResponses(result, metadata2, changedItemIdentifiers) {
106439
- const outcomes = metadata2.completion?.itemOutcomes;
106440
- const submissionId = metadata2.submissionId;
106441
- if (!submissionId || !outcomes) {
106442
- addEvent("assessment.review_child_terminal_restore_unavailable", {
106443
- "app.assessment.attempt_id": result.sourcedId
106444
- });
106445
- return;
106446
- }
106447
- const outcomeByItem = new Map(outcomes.map((outcome) => [outcome.itemIdentifier, outcome]));
106448
- await runWithConcurrency(metadata2.review.selections.filter((selection) => changedItemIdentifiers.has(selection.itemIdentifier)), TimebackAssessmentRuntimeService.SCORING_CONCURRENCY, async (selection) => {
106449
- try {
106450
- const outcome = outcomeByItem.get(selection.itemIdentifier);
106451
- if (!outcome) {
106452
- throw new Error(`Completed review omitted item ${selection.itemIdentifier}`);
106453
- }
106454
- const childLineItemId = await reviewQuestionLineItemId(result.assessmentLineItem.sourcedId, selection.itemIdentifier);
106455
- const childResultId = await reviewQuestionResultId(result.sourcedId, childLineItemId);
106456
- const finalized = buildFinalizedReviewChildResult({
106457
- childLineItemId,
106458
- student: result.student,
106459
- parentAttemptId: result.sourcedId,
106460
- metadata: metadata2,
106461
- selection,
106462
- itemResult: {
106463
- itemIdentifier: outcome.itemIdentifier,
106464
- score: outcome.score,
106465
- isCorrect: outcome.isCorrect
106466
- },
106467
- submissionId,
106468
- timestamp: result.scoreDate
106469
- });
106470
- await this.requireClient().api.oneroster.assessmentResults.upsert(childResultId, finalized.resultUpdate);
106471
- } catch (error88) {
106472
- addEvent("assessment.review_child_terminal_restore_failed", {
106473
- "app.assessment.attempt_id": result.sourcedId,
106474
- "app.assessment.qti_item_identifier": selection.itemIdentifier,
106475
- "exception.type": errorType(error88),
106476
- "app.error.message": errorMessage(error88)
106477
- });
106478
- }
106479
- });
106480
- }
106481
- reviewItemOutcomes(input) {
106482
- const resultByItem = new Map(input.itemResults.map((itemResult) => [itemResult.itemIdentifier, itemResult]));
106483
- const administeredItems = administeredItemIdentifiers(input.metadata.itemSubmissions);
106484
- return input.metadata.review.selections.filter((selection) => administeredItems.has(selection.itemIdentifier)).map((selection) => {
106485
- const itemResult = resultByItem.get(selection.itemIdentifier);
106486
- const submission = itemAdministration(input.metadata.itemSubmissions, selection.itemIdentifier);
106487
- if (!itemResult || !submission) {
106488
- throw new Error(`Scoring omitted selected review item ${selection.itemIdentifier}`);
106489
- }
106490
- return {
106491
- itemIdentifier: selection.itemIdentifier,
106492
- standard: selection.standard,
106493
- answered: submission.answered,
106494
- score: itemResult.score,
106495
- isCorrect: itemResult.isCorrect
106496
- };
106497
- });
106498
- }
106499
105494
  async resolveAttemptAuthorization({ gameId, studentId, attemptId, user }, options = {}) {
106500
105495
  if (!options.developerAccessValidated) {
106501
105496
  await this.deps.validateDeveloperAccess(user, gameId);
@@ -106524,14 +105519,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
106524
105519
  const enrollment = enrollments.find((candidate) => candidate.id === metadata2.enrollmentId && candidate.course.id === integration.courseId);
106525
105520
  return { attempt: { result, metadata: metadata2, integration }, enrollment };
106526
105521
  }
106527
- async authorizeAttempt(params, options = {}) {
106528
- const authorization = await this.resolveAttemptAuthorization(params, options);
106529
- return this.requireActiveAttempt(authorization, params.attemptId);
106530
- }
106531
- requireActiveAttempt(authorization, attemptId) {
106532
- const enrollment = requireAssessmentMutationAccess(attemptId, authorization.attempt.integration.status, authorization.enrollment);
106533
- return { ...authorization.attempt, enrollment };
106534
- }
106535
105522
  async authorizeAttemptRead(params) {
106536
105523
  const { attempt, enrollment } = await this.resolveAttemptAuthorization(params);
106537
105524
  if ((!enrollment || attempt.integration.status === "deactivated") && !this.isSettled(attempt.result)) {
@@ -106542,7 +105529,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
106542
105529
  unauthorizedAttempt(attemptId) {
106543
105530
  return AssessmentRuntimeError.from(assessmentAttemptUnauthorized(attemptId));
106544
105531
  }
106545
- async snapshotForResult(result, metadata2, integration) {
105532
+ async snapshotForResult(result, metadata2, integration, suppliedTranscript) {
106546
105533
  if (isAssessmentAttemptSuperseded({
106547
105534
  inProgress: result.inProgress ?? "",
106548
105535
  scoreStatus: result.scoreStatus
@@ -106578,38 +105565,59 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
106578
105565
  result: buildCompletedAssessmentSubmitResult({ result, metadata: metadata2 })
106579
105566
  };
106580
105567
  }
106581
- const assessment = await this.loadAttemptAssessment(metadata2);
106582
- if (this.isPlatformRoutedDiagnosticMetadata(metadata2)) {
106583
- const routing = await this.loadAttemptDiagnosticRouting(metadata2, assessment);
106584
- await this.restoreDiagnosticChildResults({
106585
- result,
106586
- metadata: metadata2,
106587
- integration,
106588
- assessment
106589
- });
106590
- return this.diagnosticSnapshot(result, metadata2, assessment, integration, routing.state);
106591
- }
106592
- return this.snapshot(result, metadata2, assessment, integration);
106593
- }
106594
- diagnosticSnapshot(result, metadata2, assessment, integration, state) {
106595
- return buildDiagnosticAssessmentSnapshot({
106596
- attemptId: result.sourcedId,
106597
- metadata: metadata2,
106598
- assessment,
106599
- state,
106600
- activityData: this.activityData(metadata2, assessment, integration)
105568
+ const context2 = this.tokenContext({ result, metadata: metadata2, integration });
105569
+ const graded = await resumeAssessmentTranscript({
105570
+ suppliedTranscript,
105571
+ checkpointTranscript: metadata2.transcript,
105572
+ recover: async () => recoverAssessmentTranscript(context2, await this.studentAssessmentResults(context2.studentId)),
105573
+ grade: (transcript) => this.gradeTranscript(context2, transcript)
106601
105574
  });
105575
+ const session2 = await this.assessmentSession(context2, graded.transcript);
105576
+ const common2 = {
105577
+ attemptId: result.sourcedId,
105578
+ assessment: graded.assessment,
105579
+ activityData: this.activityData(metadata2, graded.assessment, integration)
105580
+ };
105581
+ return this.isPlatformRoutedDiagnosticMetadata(graded.metadata) ? {
105582
+ ...buildDiagnosticAssessmentSnapshot({
105583
+ ...common2,
105584
+ metadata: graded.metadata,
105585
+ state: graded.metadata.diagnostic.state
105586
+ }),
105587
+ ...session2
105588
+ } : {
105589
+ ...buildConventionalAssessmentSnapshot({ ...common2, metadata: graded.metadata }),
105590
+ ...session2
105591
+ };
105592
+ }
105593
+ async assessmentSession(context2, transcript = emptyAssessmentTranscript(context2.attemptId, context2.metadata.selectedTest.contentRevision)) {
105594
+ return { transcript, assessmentToken: await this.deps.mintAssessmentToken(context2) };
105595
+ }
105596
+ async diagnosticSnapshot(result, metadata2, assessment, integration, options) {
105597
+ return {
105598
+ ...buildDiagnosticAssessmentSnapshot({
105599
+ attemptId: result.sourcedId,
105600
+ metadata: metadata2,
105601
+ assessment,
105602
+ state: options.state,
105603
+ activityData: this.activityData(metadata2, assessment, integration)
105604
+ }),
105605
+ ...await this.assessmentSession(this.tokenContext({ result, metadata: metadata2, integration }))
105606
+ };
106602
105607
  }
106603
105608
  diagnosticRoutingSnapshot(metadata2, state) {
106604
105609
  return projectDiagnosticRoutingSnapshot(metadata2.diagnostic.routingRevision, state);
106605
105610
  }
106606
- snapshot(result, metadata2, assessment, integration) {
106607
- return buildConventionalAssessmentSnapshot({
106608
- attemptId: result.sourcedId,
106609
- metadata: metadata2,
106610
- assessment,
106611
- activityData: this.activityData(metadata2, assessment, integration)
106612
- });
105611
+ async snapshot(result, metadata2, assessment, integration) {
105612
+ return {
105613
+ ...buildConventionalAssessmentSnapshot({
105614
+ attemptId: result.sourcedId,
105615
+ metadata: metadata2,
105616
+ assessment,
105617
+ activityData: this.activityData(metadata2, assessment, integration)
105618
+ }),
105619
+ ...await this.assessmentSession(this.tokenContext({ result, metadata: metadata2, integration }))
105620
+ };
106613
105621
  }
106614
105622
  activityData(metadata2, assessment, integration) {
106615
105623
  const course = this.trackableCourse(integration, "assessment.activity_tracking_invalid_course_metadata");
@@ -106670,16 +105678,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
106670
105678
  isSettled(result) {
106671
105679
  return this.isAwaitingAward(result) || this.isCompleted(result);
106672
105680
  }
106673
- assertInProgress(result) {
106674
- if (!this.isOpen(result)) {
106675
- throw AssessmentRuntimeError.from(assessmentAttemptNotOpen(result.sourcedId));
106676
- }
106677
- }
106678
- assertResponseVersion(metadata2, expectedResponseVersion) {
106679
- if (metadata2.responseVersion !== expectedResponseVersion) {
106680
- throw AssessmentRuntimeError.from(assessmentResponseVersionConflict(expectedResponseVersion, metadata2.responseVersion));
106681
- }
106682
- }
106683
105681
  scoreFromResult(result, possible, metadata2) {
106684
105682
  return metadata2.score ?? assessmentScoreFromPercentage(result.score, possible);
106685
105683
  }
@@ -106730,7 +105728,22 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
106730
105728
  }
106731
105729
  return projectReviewAssessment(playable, preparedReviewBankIndex(plan), plan.review.selections);
106732
105730
  }
106733
- async createPreparedAttempt(plan, assessment, integration) {
105731
+ async claimPreparedAttempt(db2, plan, assessment, integration) {
105732
+ const occupant = await this.findAttemptResult(plan.attemptId);
105733
+ if (occupant) {
105734
+ const metadata3 = playcademyAssessmentResultMetadata(occupant.metadata);
105735
+ if (occupant.status !== ONEROSTER_STATUS2.active || !metadata3 || !this.attemptInScope(occupant, metadata3, plan) || metadata3.enrollmentId !== plan.enrollmentId || !this.isOpen(occupant) || !this.attemptMatchesPlan(metadata3, plan)) {
105736
+ return null;
105737
+ }
105738
+ addEvent("assessment.prepared_attempt_recovered", {
105739
+ "app.assessment.attempt_id": plan.attemptId
105740
+ });
105741
+ const [snapshot2] = await Promise.all([
105742
+ this.snapshotForResult(occupant, metadata3, integration),
105743
+ this.supersedeAttemptsBestEffort(db2, new Set(plan.supersedes))
105744
+ ]);
105745
+ return snapshot2;
105746
+ }
106734
105747
  const timestamp6 = new Date().toISOString();
106735
105748
  const metadata2 = buildPreparedAssessmentMetadata(plan, timestamp6, assessment.title);
106736
105749
  const result = await this.requireClient().api.oneroster.assessmentResults.upsert(plan.attemptId, {
@@ -106747,7 +105760,42 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
106747
105760
  grade: integration.grade
106748
105761
  })
106749
105762
  });
106750
- return this.isPlatformRoutedDiagnosticMetadata(metadata2) ? this.diagnosticSnapshot(result, metadata2, assessment, integration, metadata2.diagnostic.state) : this.snapshot(result, metadata2, assessment, integration);
105763
+ const [snapshot] = await Promise.all([
105764
+ this.isPlatformRoutedDiagnosticMetadata(metadata2) ? this.diagnosticSnapshot(result, metadata2, assessment, integration, {
105765
+ state: metadata2.diagnostic.state
105766
+ }) : this.snapshot(result, metadata2, assessment, integration),
105767
+ this.supersedeAttemptsBestEffort(db2, new Set(plan.supersedes))
105768
+ ]);
105769
+ return snapshot;
105770
+ }
105771
+ attemptMatchesPlan(metadata2, plan) {
105772
+ const planned = buildPreparedAssessmentMetadata(plan, metadata2.startedAt, "");
105773
+ if (metadata2.selectedTest.assessmentKey !== planned.selectedTest.assessmentKey || metadata2.selectedTest.identifier !== planned.selectedTest.identifier || metadata2.selectedTest.contentRevision !== planned.selectedTest.contentRevision) {
105774
+ return false;
105775
+ }
105776
+ if (metadata2.purpose === "review" && planned.purpose === "review") {
105777
+ return metadata2.review.requestFingerprint === planned.review.requestFingerprint && metadata2.review.bankRevision === planned.review.bankRevision && metadata2.review.selections.length === planned.review.selections.length && metadata2.review.selections.every((selection, index2) => selection.itemIdentifier === planned.review.selections[index2].itemIdentifier);
105778
+ }
105779
+ if (metadata2.purpose === "mastery" && planned.purpose === "mastery") {
105780
+ return metadata2.mastery.requestFingerprint === planned.mastery.requestFingerprint;
105781
+ }
105782
+ if (metadata2.purpose === "diagnostic" && plan.purpose === "diagnostic") {
105783
+ return this.isPlatformRoutedDiagnosticMetadata(metadata2) && metadata2.diagnostic.definitionId === plan.diagnostic.definitionId && metadata2.diagnostic.routingRevision === plan.diagnostic.routingRevision;
105784
+ }
105785
+ return metadata2.purpose === planned.purpose;
105786
+ }
105787
+ async findAttemptResult(attemptId) {
105788
+ try {
105789
+ return await this.requireClient().api.oneroster.assessmentResults.get(attemptId);
105790
+ } catch (error88) {
105791
+ if (isApiError(error88) && error88.statusCode === 404) {
105792
+ return null;
105793
+ }
105794
+ throw error88;
105795
+ }
105796
+ }
105797
+ attemptInScope(result, metadata2, scope) {
105798
+ return metadata2.activityId === scope.activityId && metadata2.purpose === scope.purpose && metadata2.courseId === scope.courseId && metadata2.integrationId === scope.integrationId && result.student.sourcedId === scope.studentId;
106751
105799
  }
106752
105800
  async claimAttemptId(scope, from) {
106753
105801
  for (let attemptNumber = from;attemptNumber < from + 20; attemptNumber += 1) {
@@ -106763,15 +105811,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
106763
105811
  throw new Error(`Assessment attempt numbers ${from} through ${from + 19} are all occupied by records this code cannot read; refusing to overwrite them.`);
106764
105812
  }
106765
105813
  async attemptIdOccupied(attemptId) {
106766
- try {
106767
- await this.requireClient().api.oneroster.assessmentResults.get(attemptId);
106768
- } catch (error88) {
106769
- if (isApiError(error88) && error88.statusCode === 404) {
106770
- return false;
106771
- }
106772
- throw error88;
106773
- }
106774
- return true;
105814
+ return await this.findAttemptResult(attemptId) !== null;
106775
105815
  }
106776
105816
  replayCompletion(attempt, submissionId, params, game2) {
106777
105817
  const { score, completion } = attempt.metadata;
@@ -106792,18 +105832,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
106792
105832
  game: game2
106793
105833
  };
106794
105834
  }
106795
- async putResultMetadata(result, metadata2) {
106796
- await this.requireClient().api.oneroster.assessmentResults.upsert(result.sourcedId, {
106797
- status: ONEROSTER_STATUS2.active,
106798
- assessmentLineItem: result.assessmentLineItem,
106799
- student: result.student,
106800
- score: result.score ?? 0,
106801
- scoreDate: result.scoreDate,
106802
- scoreStatus: result.scoreStatus,
106803
- ...result.inProgress === undefined || result.inProgress === null ? {} : { inProgress: result.inProgress },
106804
- metadata: mergeAssessmentRuntimeMetadata(result.metadata, metadata2)
106805
- });
106806
- }
106807
105835
  async emitCompletionBestEffort(input) {
106808
105836
  const { attempt } = input;
106809
105837
  const integration = attempt.integration;
@@ -110392,6 +109420,8 @@ function createPlatformServices(deps) {
110392
109420
  mintPlatformServiceToken,
110393
109421
  mintAssessmentPreparationReceipt,
110394
109422
  verifyAssessmentPreparationReceipt,
109423
+ mintAssessmentToken,
109424
+ verifyAssessmentToken,
110395
109425
  validateDeveloperAccessBySlug,
110396
109426
  validateDeveloperAccess,
110397
109427
  validateGameManagementAccess
@@ -110466,7 +109496,9 @@ function createPlatformServices(deps) {
110466
109496
  validateDeveloperAccess,
110467
109497
  assessmentRuntimeLock,
110468
109498
  mintAssessmentPreparationReceipt,
110469
- verifyAssessmentPreparationReceipt
109499
+ verifyAssessmentPreparationReceipt,
109500
+ mintAssessmentToken,
109501
+ verifyAssessmentToken
110470
109502
  });
110471
109503
  return {
110472
109504
  assessmentAssets,
@@ -111216,6 +110248,8 @@ function createServices(ctx) {
111216
110248
  mintPlatformServiceToken: auth2.mintPlatformServiceToken.bind(auth2),
111217
110249
  mintAssessmentPreparationReceipt: auth2.mintAssessmentPreparationReceipt.bind(auth2),
111218
110250
  verifyAssessmentPreparationReceipt: auth2.verifyAssessmentPreparationReceipt.bind(auth2),
110251
+ mintAssessmentToken: auth2.mintAssessmentToken.bind(auth2),
110252
+ verifyAssessmentToken: auth2.verifyAssessmentToken.bind(auth2),
111219
110253
  ...gaming.validators
111220
110254
  });
111221
110255
  const standalone = createStandaloneServices({ db: db2, auth: auth2, timeback: timeback2 });
@@ -111744,6 +110778,15 @@ function createSandboxAuthProvider() {
111744
110778
  });
111745
110779
  return { receipt, expiresAt: new Date(expiresAt).toISOString() };
111746
110780
  },
110781
+ async mintAssessmentToken(context2) {
110782
+ const token = `sandbox_assessment_${crypto.randomUUID()}`;
110783
+ sandboxAssessmentTokens.set(token, structuredClone(context2));
110784
+ return token;
110785
+ },
110786
+ async verifyAssessmentToken(token) {
110787
+ const context2 = sandboxAssessmentTokens.get(token);
110788
+ return context2 ? structuredClone(context2) : null;
110789
+ },
111747
110790
  async verifyAssessmentPreparationReceipt(receipt) {
111748
110791
  const prepared = sandboxAssessmentPreparationReceipts.get(receipt);
111749
110792
  if (!prepared) {
@@ -111796,13 +110839,18 @@ function createSandboxAuthProvider() {
111796
110839
  }
111797
110840
  };
111798
110841
  }
111799
- var sandboxApiKeys, sandboxAssessmentPreparationReceipts, ASSESSMENT_PREPARATION_RECEIPT_LIFETIME_MS;
111800
- var init_auth_provider = __esm(() => {
110842
+ var sandboxApiKeys, sandboxAssessmentPreparationReceipts, ASSESSMENT_PREPARATION_RECEIPT_LIFETIME_MS, sandboxAssessmentTokens;
110843
+ var init_auth_provider = __esm(async () => {
111801
110844
  init_src6();
111802
110845
  init_constants();
110846
+ await init_dist5();
111803
110847
  sandboxApiKeys = new Map;
111804
110848
  sandboxAssessmentPreparationReceipts = new Map;
111805
110849
  ASSESSMENT_PREPARATION_RECEIPT_LIFETIME_MS = 5 * 60 * 1000;
110850
+ sandboxAssessmentTokens = new TimebackCache({
110851
+ defaultTTL: 2 * 60 * 60 * 1000,
110852
+ maxSize: 1000
110853
+ });
111806
110854
  });
111807
110855
 
111808
110856
  // src/infrastructure/api/providers/cache.provider.ts
@@ -111963,10 +111011,10 @@ var init_storage_provider = __esm(() => {
111963
111011
  });
111964
111012
 
111965
111013
  // src/infrastructure/api/providers/index.ts
111966
- var init_providers = __esm(() => {
111967
- init_auth_provider();
111014
+ var init_providers = __esm(async () => {
111968
111015
  init_cache_provider();
111969
111016
  init_storage_provider();
111017
+ await init_auth_provider();
111970
111018
  });
111971
111019
 
111972
111020
  // src/infrastructure/api/context.ts
@@ -112036,10 +111084,10 @@ var init_context = __esm(async () => {
112036
111084
  init_config2();
112037
111085
  init_utils7();
112038
111086
  init_src6();
112039
- init_providers();
112040
111087
  await __promiseAll([
112041
111088
  init_factory(),
112042
- init_clients()
111089
+ init_clients(),
111090
+ init_providers()
112043
111091
  ]);
112044
111092
  });
112045
111093
 
@@ -168435,7 +167483,7 @@ function parseQtiLibraryParams(searchParams) {
168435
167483
  limit
168436
167484
  };
168437
167485
  }
168438
- var populateStudent, getUser, getUserEnrollments, getUserById, setupIntegration, getIntegrations, getRemovedIntegrations, createIntegration, updateIntegration, deactivateCourse, reactivateCourse, getIntegrationConfig, verifyIntegration, getConfig, deleteIntegrations, endActivity, heartbeat, advanceCourse, unenrollCourse, prepareRuntimeAssessment, startRuntimeAssessment, getRuntimeAssessment, getLatestRuntimeAssessment, saveRuntimeAssessment, 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;
167486
+ 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;
168439
167487
  var init_timeback_controller = __esm(() => {
168440
167488
  init_esm();
168441
167489
  init_src();
@@ -168737,6 +167785,20 @@ var init_timeback_controller = __esm(() => {
168737
167785
  user: ctx.user
168738
167786
  });
168739
167787
  });
167788
+ resumeRuntimeAssessment = requireDeveloper(async (ctx) => {
167789
+ const attemptId = ctx.params.attemptId;
167790
+ const body2 = await parseRequestBody(ctx.request, ResumeRuntimeAssessmentRequestSchema);
167791
+ if (!attemptId) {
167792
+ throw ApiError.badRequest("Missing attemptId");
167793
+ }
167794
+ return ctx.services.timebackAssessmentRuntime.resume({
167795
+ gameId: body2.gameId,
167796
+ studentId: body2.studentId,
167797
+ attemptId,
167798
+ input: { transcript: body2.transcript },
167799
+ user: ctx.user
167800
+ });
167801
+ });
168740
167802
  saveRuntimeAssessment = requireDeveloper(async (ctx) => {
168741
167803
  const attemptId = ctx.params.attemptId;
168742
167804
  const body2 = await parseRequestBody(ctx.request, SaveRuntimeAssessmentRequestSchema);
@@ -168749,7 +167811,26 @@ var init_timeback_controller = __esm(() => {
168749
167811
  attemptId,
168750
167812
  input: {
168751
167813
  expectedResponseVersion: body2.expectedResponseVersion,
168752
- responses: body2.responses
167814
+ transcript: body2.transcript
167815
+ },
167816
+ user: ctx.user
167817
+ });
167818
+ });
167819
+ recordRuntimeAssessmentProgress = requireDeveloper(async (ctx) => {
167820
+ const attemptId = ctx.params.attemptId;
167821
+ const body2 = await parseRequestBody(ctx.request, RecordRuntimeAssessmentProgressRequestSchema);
167822
+ if (!attemptId) {
167823
+ throw ApiError.badRequest("Missing attemptId");
167824
+ }
167825
+ return ctx.services.timebackAssessmentRuntime.recordProgress({
167826
+ gameId: body2.gameId,
167827
+ studentId: body2.studentId,
167828
+ attemptId,
167829
+ input: {
167830
+ assessmentToken: body2.assessmentToken,
167831
+ transcript: body2.transcript,
167832
+ expectedResponseVersion: body2.expectedResponseVersion,
167833
+ itemIdentifier: body2.itemIdentifier
168753
167834
  },
168754
167835
  user: ctx.user
168755
167836
  });
@@ -168765,6 +167846,8 @@ var init_timeback_controller = __esm(() => {
168765
167846
  studentId: body2.studentId,
168766
167847
  attemptId,
168767
167848
  input: {
167849
+ assessmentToken: body2.assessmentToken,
167850
+ transcript: body2.transcript,
168768
167851
  expectedResponseVersion: body2.expectedResponseVersion,
168769
167852
  submissionId: body2.submissionId,
168770
167853
  itemIdentifier: body2.itemIdentifier,
@@ -168787,6 +167870,7 @@ var init_timeback_controller = __esm(() => {
168787
167870
  input: {
168788
167871
  expectedResponseVersion: body2.expectedResponseVersion,
168789
167872
  submissionId: body2.submissionId,
167873
+ transcript: body2.transcript,
168790
167874
  ...body2.session ? { session: body2.session } : {}
168791
167875
  },
168792
167876
  game: {
@@ -169264,6 +168348,8 @@ var init_timeback_controller = __esm(() => {
169264
168348
  getLatestRuntimeAssessment,
169265
168349
  getRuntimeAssessment,
169266
168350
  saveRuntimeAssessment,
168351
+ resumeRuntimeAssessment,
168352
+ recordRuntimeAssessmentProgress,
169267
168353
  submitRuntimeAssessmentItem,
169268
168354
  submitRuntimeAssessment,
169269
168355
  finalizeRuntimeAssessment,
@@ -170256,6 +169342,8 @@ var init_timeback8 = __esm(async () => {
170256
169342
  timebackRouter.post("/assessments/start", handle2(timeback2.startRuntimeAssessment));
170257
169343
  timebackRouter.get("/assessments/latest", handle2(timeback2.getLatestRuntimeAssessment));
170258
169344
  timebackRouter.get("/assessments/:attemptId", handle2(timeback2.getRuntimeAssessment));
169345
+ timebackRouter.post("/assessments/:attemptId/resume", handle2(timeback2.resumeRuntimeAssessment));
169346
+ timebackRouter.post("/assessments/:attemptId/progress", handle2(timeback2.recordRuntimeAssessmentProgress));
170259
169347
  timebackRouter.post("/assessments/:attemptId/save", handle2(timeback2.saveRuntimeAssessment));
170260
169348
  timebackRouter.post("/assessments/:attemptId/submit-item", handle2(timeback2.submitRuntimeAssessmentItem));
170261
169349
  timebackRouter.post("/assessments/:attemptId/submit", handle2(timeback2.submitRuntimeAssessment));
@@ -170664,13 +169752,13 @@ var init_server = __esm(async () => {
170664
169752
  init_port();
170665
169753
  init_package();
170666
169754
  init_config();
170667
- init_providers();
170668
169755
  init_app();
170669
169756
  init_database2();
170670
169757
  init_options();
170671
169758
  await __promiseAll([
170672
169759
  init_api3(),
170673
- init_context()
169760
+ init_context(),
169761
+ init_providers()
170674
169762
  ]);
170675
169763
  version5 = package_default.version;
170676
169764
  });