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