@playcademy/vite-plugin 1.3.2-beta.2 → 1.3.2-beta.4

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +1196 -1927
  2. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -24317,7 +24317,7 @@ import path2 from "node:path";
24317
24317
  // package.json
24318
24318
  var package_default = {
24319
24319
  name: "@playcademy/vite-plugin",
24320
- version: "1.3.2-beta.2",
24320
+ version: "1.3.2-beta.4",
24321
24321
  type: "module",
24322
24322
  exports: {
24323
24323
  ".": {
@@ -25058,7 +25058,6 @@ var TIMEBACK_GRADES;
25058
25058
  var TIMEBACK_SUBJECTS;
25059
25059
  var ASSESSMENT_PURPOSES;
25060
25060
  var PLAYCADEMY_ASSESSMENT_RESULT_METADATA_VERSION = 1;
25061
- var PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_VERSION = 1;
25062
25061
  var PLAYCADEMY_ASSESSMENT_CONTRACT_VERSION = 1;
25063
25062
  var PLAYCADEMY_ASSESSMENT_RESULT_METADATA_KEY = "playcademyAssessment";
25064
25063
  var PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY = "playcademyAssessmentItem";
@@ -25934,7 +25933,7 @@ var package_default2;
25934
25933
  var init_package = __esm(() => {
25935
25934
  package_default2 = {
25936
25935
  name: "@playcademy/sandbox",
25937
- version: "0.8.1-beta.5",
25936
+ version: "0.8.1-beta.6",
25938
25937
  description: "Local development server for Playcademy game development",
25939
25938
  type: "module",
25940
25939
  exports: {
@@ -34175,25 +34174,6 @@ var init_v3 = __esm(() => {
34175
34174
  var init_esm = __esm(() => {
34176
34175
  init_v3();
34177
34176
  });
34178
- var PLAYABLE_ASSESSMENT_SHAPES;
34179
- var init_assessment_runtime = __esm(() => {
34180
- PLAYABLE_ASSESSMENT_SHAPES = ["circle", "ellipse", "rect", "poly", "default"];
34181
- });
34182
- var DIAGNOSTIC_ROUTING_MANIFEST_VERSION = 1;
34183
- var DIAGNOSTIC_ROUTING_STATE_VERSION = 1;
34184
- var DIAGNOSTIC_TRACK_OUTCOMES;
34185
- var init_diagnostic_routing = __esm(() => {
34186
- DIAGNOSTIC_TRACK_OUTCOMES = [
34187
- "provisional",
34188
- "unresolved",
34189
- "route-to-instruction",
34190
- "not-assessed"
34191
- ];
34192
- });
34193
- var init_timeback4 = __esm(() => {
34194
- init_assessment_runtime();
34195
- init_diagnostic_routing();
34196
- });
34197
34177
  function isValidUUID(value) {
34198
34178
  if (!value || typeof value !== "string") {
34199
34179
  return false;
@@ -34215,6 +34195,25 @@ var UUID_REGEX;
34215
34195
  var init_uuid = __esm(() => {
34216
34196
  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}$/;
34217
34197
  });
34198
+ var PLAYABLE_ASSESSMENT_SHAPES;
34199
+ var init_assessment_runtime = __esm(() => {
34200
+ PLAYABLE_ASSESSMENT_SHAPES = ["circle", "ellipse", "rect", "poly", "default"];
34201
+ });
34202
+ var DIAGNOSTIC_ROUTING_MANIFEST_VERSION = 1;
34203
+ var DIAGNOSTIC_ROUTING_STATE_VERSION = 1;
34204
+ var DIAGNOSTIC_TRACK_OUTCOMES;
34205
+ var init_diagnostic_routing = __esm(() => {
34206
+ DIAGNOSTIC_TRACK_OUTCOMES = [
34207
+ "provisional",
34208
+ "unresolved",
34209
+ "route-to-instruction",
34210
+ "not-assessed"
34211
+ ];
34212
+ });
34213
+ var init_timeback4 = __esm(() => {
34214
+ init_assessment_runtime();
34215
+ init_diagnostic_routing();
34216
+ });
34218
34217
  function qtiDirectedPairValidationIssue(values, contract) {
34219
34218
  const sourceCounts = new Map;
34220
34219
  const targetCounts = new Map;
@@ -34492,6 +34491,25 @@ function assessmentReviewBankShortage(fulfillment) {
34492
34491
  function assessmentFlowForPurpose(purpose) {
34493
34492
  return purpose === "review" ? "item-submit" : "attempt-submit";
34494
34493
  }
34494
+ function isObject(value) {
34495
+ return typeof value === "object" && value !== null;
34496
+ }
34497
+ function isTimebackGrade(value) {
34498
+ return typeof value === "number" && Number.isInteger(value) && GRADE_VALUES.includes(value);
34499
+ }
34500
+ function isAssessmentSessionTiming(value) {
34501
+ 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));
34502
+ }
34503
+ function isNonNegativeDuration(value) {
34504
+ return typeof value === "number" && Number.isFinite(value) && value >= 0;
34505
+ }
34506
+ function isAssessmentPurpose(value) {
34507
+ return ASSESSMENT_PURPOSES.includes(value);
34508
+ }
34509
+ function isAssessmentResponseValue(value) {
34510
+ const entries = Array.isArray(value) ? value : [value];
34511
+ return entries.length > 0 && entries.every((entry2) => typeof entry2 === "string" && entry2.length > 0 && entry2.length <= TIMEBACK_ASSESSMENT_RESPONSE_VALUE_MAX_LENGTH);
34512
+ }
34495
34513
  function compareCodeUnits(left, right) {
34496
34514
  if (left < right) {
34497
34515
  return -1;
@@ -35345,48 +35363,6 @@ function projectDiagnosticRoutingSnapshot(revision, state) {
35345
35363
  }))
35346
35364
  };
35347
35365
  }
35348
- function isObject(value) {
35349
- return typeof value === "object" && value !== null;
35350
- }
35351
- function isTimebackGrade(value) {
35352
- return typeof value === "number" && Number.isInteger(value) && GRADE_VALUES.includes(value);
35353
- }
35354
- function isAssessmentSessionTiming(value) {
35355
- 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));
35356
- }
35357
- function isNonNegativeDuration(value) {
35358
- return typeof value === "number" && Number.isFinite(value) && value >= 0;
35359
- }
35360
- function isAssessmentPurpose(value) {
35361
- return ASSESSMENT_PURPOSES.includes(value);
35362
- }
35363
- function isAssessmentResponseValue(value) {
35364
- const entries = Array.isArray(value) ? value : [value];
35365
- return entries.length > 0 && entries.every((entry2) => typeof entry2 === "string" && entry2.length > 0 && entry2.length <= TIMEBACK_ASSESSMENT_RESPONSE_VALUE_MAX_LENGTH);
35366
- }
35367
- function applyAssessmentItemResponseUpdate(current, update) {
35368
- const next = { ...current };
35369
- for (const [responseId, value] of Object.entries(update)) {
35370
- if (value === null) {
35371
- delete next[responseId];
35372
- } else {
35373
- next[responseId] = value;
35374
- }
35375
- }
35376
- return Object.keys(next).length === 0 ? undefined : next;
35377
- }
35378
- function applyAssessmentResponseUpdate(current, update) {
35379
- const next = structuredClone(current);
35380
- for (const [questionId, questionUpdate] of Object.entries(update)) {
35381
- const questionResponses = applyAssessmentItemResponseUpdate(next[questionId], questionUpdate);
35382
- if (questionResponses === undefined) {
35383
- delete next[questionId];
35384
- } else {
35385
- next[questionId] = questionResponses;
35386
- }
35387
- }
35388
- return next;
35389
- }
35390
35366
  function assessmentResponseUpdateValidationMessage(assessment, update) {
35391
35367
  const items = new Map(assessment.items.map((item) => [item.identifier, item]));
35392
35368
  for (const [itemIdentifier, itemUpdate] of Object.entries(update)) {
@@ -35548,147 +35524,154 @@ function interactionResponseValidationMessage(itemIdentifier, interaction, respo
35548
35524
  }
35549
35525
  return null;
35550
35526
  }
35551
- function invalidResponse(message) {
35552
- return { code: "INVALID_RESPONSE", message };
35527
+ function emptyAssessmentTranscript(attemptId, contentRevision) {
35528
+ return { version: 1, attemptId, contentRevision, administrations: [] };
35553
35529
  }
35554
- function administeredItemIdentifiers(itemSubmissions) {
35555
- return new Set(itemSubmissions.map((submission) => submission.itemIdentifier));
35556
- }
35557
- function itemAdministration(itemSubmissions, itemIdentifier) {
35558
- return itemSubmissions.find((submission) => submission.itemIdentifier === itemIdentifier);
35559
- }
35560
- function responseValuesEqual(left, right) {
35561
- if (left === undefined || right === undefined) {
35562
- return left === right;
35563
- }
35564
- if (Array.isArray(left) || Array.isArray(right)) {
35565
- return Array.isArray(left) && Array.isArray(right) && left.length === right.length && left.every((value, index) => value === right[index]);
35566
- }
35567
- return left === right;
35568
- }
35569
- function itemResponsesEqual(left, right) {
35570
- const leftEntries = Object.entries(left ?? {});
35571
- const rightEntries = Object.entries(right ?? {});
35572
- return leftEntries.length === rightEntries.length && leftEntries.every(([identifier, value]) => responseValuesEqual(value, right?.[identifier]));
35573
- }
35574
- function itemResponseUpdateMatches(current, update) {
35575
- return itemResponsesEqual(current, applyAssessmentItemResponseUpdate(current, update));
35576
- }
35577
- function resolveAssessmentItemReplay(state, input) {
35578
- const flow = assessmentFlowForPurpose(state.purpose);
35579
- if (flow !== "item-submit") {
35530
+ function assessmentProgressAdministration(purpose, input) {
35531
+ if (purpose !== "mastery") {
35580
35532
  return {
35581
- action: "conflict",
35582
- failure: assessmentFlowViolation("Items cannot be submitted individually in this flow.", { flow })
35533
+ ok: false,
35534
+ failure: assessmentFlowViolation("Background progress is only available for mastery.")
35583
35535
  };
35584
35536
  }
35585
- const priorSubmission = state.itemSubmissions.find((submission) => submission.submissionId === input.submissionId);
35586
- if (priorSubmission) {
35587
- const sameRequest = priorSubmission.itemIdentifier === input.itemIdentifier && itemResponseUpdateMatches(state.responses[input.itemIdentifier], input.responses);
35588
- if (!sameRequest) {
35589
- return {
35590
- action: "conflict",
35591
- failure: assessmentFlowViolation("This item submission ID was already used for a different request.", {
35592
- submissionId: input.submissionId,
35593
- itemIdentifier: input.itemIdentifier,
35594
- committedItemIdentifier: priorSubmission.itemIdentifier,
35595
- flow
35596
- })
35597
- };
35598
- }
35537
+ const { administrations } = input.transcript;
35538
+ if (input.expectedResponseVersion !== administrations.length) {
35599
35539
  return {
35600
- action: "replay",
35601
- responseVersion: state.responseVersion,
35602
- responses: state.responses,
35603
- submission: priorSubmission
35540
+ ok: false,
35541
+ failure: assessmentResponseVersionConflict(input.expectedResponseVersion, administrations.length)
35604
35542
  };
35605
35543
  }
35606
- return { action: "missing" };
35544
+ const administration = administrations.find((entry2) => entry2.itemIdentifier === input.itemIdentifier);
35545
+ return administration ? { ok: true, submissionId: administration.submissionId } : {
35546
+ ok: false,
35547
+ failure: assessmentFlowViolation("The progress item must be present in the transcript.")
35548
+ };
35607
35549
  }
35608
- function prepareAssessmentItemSubmission(state, input) {
35609
- const replay = resolveAssessmentItemReplay(state, input);
35610
- if (replay.action === "conflict") {
35611
- return { action: "reject", failure: replay.failure };
35612
- }
35613
- if (replay.action === "replay") {
35614
- return replay;
35615
- }
35616
- if (state.responseVersion !== input.expectedResponseVersion) {
35550
+ function appendAssessmentAdministration(transcript, input) {
35551
+ const entry2 = {
35552
+ submissionId: input.submissionId,
35553
+ itemIdentifier: input.itemIdentifier,
35554
+ ...input.routingNodeKey === undefined ? {} : { routingNodeKey: input.routingNodeKey },
35555
+ responses: Object.fromEntries(Object.entries(input.responses).filter((pair) => pair[1] !== null))
35556
+ };
35557
+ const priorIndex = transcript.administrations.findIndex((item) => item.submissionId === input.submissionId);
35558
+ const index = priorIndex === -1 ? transcript.administrations.length : priorIndex;
35559
+ if (input.expectedResponseVersion !== index) {
35617
35560
  return {
35618
- action: "reject",
35619
- failure: assessmentResponseVersionConflict(input.expectedResponseVersion, state.responseVersion)
35561
+ ok: false,
35562
+ failure: assessmentResponseVersionConflict(input.expectedResponseVersion, index)
35620
35563
  };
35621
35564
  }
35622
- const itemIndex = state.assessment.items.findIndex((item) => item.identifier === input.itemIdentifier);
35623
- const previousItemIdentifier = state.itemSubmissions.at(-1)?.itemIdentifier;
35624
- const previousItemIndex = state.assessment.items.findIndex((item) => item.identifier === previousItemIdentifier);
35625
- if (itemIndex === -1 || itemIndex <= previousItemIndex) {
35565
+ if (priorIndex !== -1 && canonicalJson(transcript.administrations[priorIndex]) !== canonicalJson(entry2)) {
35626
35566
  return {
35627
- action: "reject",
35628
- failure: assessmentFlowViolation("Items must be submitted once and in candidate order.", {
35629
- itemIdentifier: input.itemIdentifier,
35630
- previousItemIdentifier: previousItemIdentifier ?? null,
35631
- flow: assessmentFlowForPurpose(state.purpose)
35632
- })
35567
+ ok: false,
35568
+ failure: assessmentFlowViolation("This administration identity was reused with different answers.")
35633
35569
  };
35634
35570
  }
35635
- const update = { [input.itemIdentifier]: input.responses };
35636
- const updateMessage = assessmentResponseUpdateValidationMessage(state.assessment, update);
35637
- if (updateMessage) {
35638
- return { action: "reject", failure: invalidResponse(updateMessage) };
35639
- }
35640
- const responses = applyAssessmentResponseUpdate(state.responses, update);
35641
- const validationMessage = assessmentResponseValidationMessage(state.assessment, responses);
35642
- if (validationMessage) {
35643
- return { action: "reject", failure: invalidResponse(validationMessage) };
35644
- }
35645
35571
  return {
35646
- action: "commit",
35647
- itemIndex,
35648
- responseVersion: state.responseVersion + 1,
35649
- responses,
35650
- answered: Object.keys(responses[input.itemIdentifier] ?? {}).length > 0
35572
+ ok: true,
35573
+ index,
35574
+ transcript: priorIndex === -1 ? { ...transcript, administrations: [...transcript.administrations, entry2] } : transcript
35651
35575
  };
35652
35576
  }
35653
- function completeAssessmentItemSubmission(state, input, prepared, scoring, submittedAt) {
35654
- const submission = {
35655
- submissionId: input.submissionId,
35656
- itemIdentifier: input.itemIdentifier,
35657
- submittedAt,
35658
- responseVersion: prepared.responseVersion,
35659
- answered: prepared.answered,
35660
- score: scoring.score,
35661
- isCorrect: scoring.isCorrect,
35662
- ...scoring.grading ? { grading: scoring.grading } : {}
35663
- };
35664
- return {
35665
- responseVersion: prepared.responseVersion,
35666
- responses: prepared.responses,
35667
- itemSubmissions: [...state.itemSubmissions, submission],
35668
- submission
35669
- };
35577
+ function prepareAssessmentTranscript(input) {
35578
+ const parsed = AssessmentTranscriptSchema.safeParse(input.transcript);
35579
+ if (!parsed.success) {
35580
+ throw new AssessmentTranscriptError("Invalid assessment transcript", 0, "INVALID_RESPONSE");
35581
+ }
35582
+ const transcript = parsed.data;
35583
+ if (transcript.attemptId !== input.attemptId || transcript.contentRevision !== input.contentRevision) {
35584
+ throw new AssessmentTranscriptError("Transcript does not belong to this attempt and revision");
35585
+ }
35586
+ const responses = {};
35587
+ const submissions = new Set;
35588
+ const items = new Set;
35589
+ let previousItemIndex = -1;
35590
+ for (const [index, entry2] of transcript.administrations.entries()) {
35591
+ const itemIndex = input.assessment.items.findIndex((item) => item.identifier === entry2.itemIdentifier);
35592
+ if (submissions.has(entry2.submissionId) || items.has(entry2.itemIdentifier)) {
35593
+ throw new AssessmentTranscriptError("Transcript contains a repeated administration or item", index);
35594
+ }
35595
+ if (itemIndex === -1) {
35596
+ throw new AssessmentTranscriptError("Transcript references an unknown assessment item", index, "INVALID_RESPONSE");
35597
+ }
35598
+ if (!input.routed && itemIndex <= previousItemIndex) {
35599
+ throw new AssessmentTranscriptError("Transcript items must follow the selected candidate order", index);
35600
+ }
35601
+ if (input.routed !== (entry2.routingNodeKey !== undefined)) {
35602
+ throw new AssessmentTranscriptError("Transcript routing identity does not match the assessment flow", index);
35603
+ }
35604
+ const itemResponses = { [entry2.itemIdentifier]: entry2.responses };
35605
+ const validate2 = input.routed || input.allowIncomplete ? assessmentDraftResponseValidationMessage : assessmentResponseValidationMessage;
35606
+ const invalid = Object.keys(entry2.responses).length === 0 ? null : validate2(input.assessment, itemResponses);
35607
+ if (invalid) {
35608
+ throw new AssessmentTranscriptError(invalid, index, "INVALID_RESPONSE");
35609
+ }
35610
+ submissions.add(entry2.submissionId);
35611
+ items.add(entry2.itemIdentifier);
35612
+ previousItemIndex = itemIndex;
35613
+ Object.assign(responses, itemResponses);
35614
+ }
35615
+ return { transcript, responses };
35616
+ }
35617
+ function completeAssessmentTranscript(input) {
35618
+ const itemSubmissions = [];
35619
+ let state;
35620
+ const ledger = [];
35621
+ if (input.manifest) {
35622
+ const initial = initializeDiagnosticRouting(input.manifest);
35623
+ if (!initial.ok) {
35624
+ throw new AssessmentTranscriptError("The pinned diagnostic routing manifest is invalid");
35625
+ }
35626
+ state = initial.state;
35627
+ }
35628
+ const scores = new Map(input.scores.map((score) => [score.itemIdentifier, score]));
35629
+ for (const [index, entry2] of input.transcript.administrations.entries()) {
35630
+ const scoring = scores.get(entry2.itemIdentifier);
35631
+ if (!scoring) {
35632
+ throw new AssessmentTranscriptError("Transcript item has no trusted grade", index);
35633
+ }
35634
+ if (input.manifest) {
35635
+ if (typeof scoring.isCorrect !== "boolean" || !entry2.routingNodeKey) {
35636
+ throw new AssessmentTranscriptError("Diagnostic grading must be determinate", index);
35637
+ }
35638
+ const advanced = advanceDiagnosticRouting(input.manifest, ledger, {
35639
+ routingNodeKey: entry2.routingNodeKey,
35640
+ itemIdentifier: entry2.itemIdentifier,
35641
+ isCorrect: scoring.isCorrect
35642
+ });
35643
+ if (!advanced.ok) {
35644
+ throw new AssessmentTranscriptError("Transcript does not follow the pinned diagnostic route", index);
35645
+ }
35646
+ ledger.push(advanced.ledgerEntry);
35647
+ state = advanced.state;
35648
+ }
35649
+ itemSubmissions.push({
35650
+ submissionId: entry2.submissionId,
35651
+ itemIdentifier: entry2.itemIdentifier,
35652
+ responseVersion: index + 1,
35653
+ submittedAt: input.submittedAt,
35654
+ answered: Object.keys(entry2.responses).length > 0,
35655
+ score: scoring.score,
35656
+ isCorrect: scoring.isCorrect,
35657
+ ...scoring.grading ? { grading: scoring.grading } : {}
35658
+ });
35659
+ }
35660
+ if (input.terminal) {
35661
+ const count = itemSubmissions.length;
35662
+ const conventionalComplete = assessmentFlowForPurpose(input.purpose) === "item-submit" ? count > 0 : count === input.itemCount;
35663
+ const complete = state ? state.status === "ready-to-complete" && count > 0 : conventionalComplete;
35664
+ if (!complete) {
35665
+ throw new AssessmentTranscriptError("A complete administered transcript is required for submission", count);
35666
+ }
35667
+ }
35668
+ return { itemSubmissions, ledger, state };
35670
35669
  }
35671
- function assessmentResponseUpdateFlowFailure(purpose, itemSubmissions, update) {
35672
- const administered = administeredItemIdentifiers(itemSubmissions);
35673
- const committedUpdate = Object.keys(update).find((identifier) => administered.has(identifier));
35674
- return committedUpdate ? assessmentFlowViolation("A submitted item response cannot be edited.", {
35675
- itemIdentifier: committedUpdate,
35676
- flow: assessmentFlowForPurpose(purpose)
35677
- }) : null;
35670
+ function administeredItemIdentifiers(itemSubmissions) {
35671
+ return new Set(itemSubmissions.map((submission) => submission.itemIdentifier));
35678
35672
  }
35679
- function assessmentCompletionFlowFailure(purpose, itemSubmissions, itemCount) {
35680
- const flow = assessmentFlowForPurpose(purpose);
35681
- if (flow !== "item-submit") {
35682
- return null;
35683
- }
35684
- if (itemSubmissions.length > 0 && itemSubmissions.length <= itemCount) {
35685
- return null;
35686
- }
35687
- return assessmentFlowViolation("At least one candidate must be administered before the review can be completed.", {
35688
- flow,
35689
- submittedItemCount: itemSubmissions.length,
35690
- itemCount
35691
- });
35673
+ function itemAdministration(itemSubmissions, itemIdentifier) {
35674
+ return itemSubmissions.find((submission) => submission.itemIdentifier === itemIdentifier);
35692
35675
  }
35693
35676
  function selectAssessmentCourseCandidate(candidates, subjectSpecified) {
35694
35677
  if (candidates.length === 0) {
@@ -36021,9 +36004,6 @@ function reviewRequestFingerprint(request, policy) {
36021
36004
  function reviewQuestionLineItemId(parentLineItemId, itemIdentifier) {
36022
36005
  return deterministicUUID(["playcademy:review-question-line-item:v1", parentLineItemId, itemIdentifier].join("\x00"));
36023
36006
  }
36024
- function reviewQuestionResultId(parentAttemptId, childQuestionLineItemId) {
36025
- return deterministicUUID(["playcademy:review-question-result:v1", parentAttemptId, childQuestionLineItemId].join("\x00"));
36026
- }
36027
36007
  function qtiQuestionByIdentifier(questions) {
36028
36008
  const byIdentifier = new Map;
36029
36009
  for (const entry2 of questions.questions) {
@@ -36583,11 +36563,8 @@ function isDiagnosticRoutingState(value) {
36583
36563
  const nextValid = value.next === null || isRecord(value.next) && isNonEmptyString(value.next.stageKey) && isNonEmptyString(value.next.trackKey) && isNonEmptyString(value.next.nodeKey) && isNonEmptyString(value.next.itemIdentifier);
36584
36564
  return stagesValid && tracksValid && nextValid && (value.status === "in-progress" && value.next !== null || value.status === "ready-to-complete" && value.next === null);
36585
36565
  }
36586
- function isDiagnosticRoutingSnapshot(value, revision) {
36587
- 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);
36588
- }
36589
36566
  function isRoutedDiagnosticAttemptMetadata(value, itemSubmissions, responseVersion) {
36590
- 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) {
36567
+ 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) {
36591
36568
  return false;
36592
36569
  }
36593
36570
  const itemIdentifiers = new Set;
@@ -36641,19 +36618,7 @@ function isPlaycademyAssessmentResultMetadataV1(value) {
36641
36618
  const review = metadata2.review;
36642
36619
  const diagnostic = metadata2.diagnostic;
36643
36620
  const routedDiagnostic = metadata2.purpose === "diagnostic" && diagnostic !== undefined && Array.isArray(metadata2.itemSubmissions) && Number.isInteger(metadata2.responseVersion) && isRoutedDiagnosticAttemptMetadata(diagnostic, metadata2.itemSubmissions, metadata2.responseVersion);
36644
- 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);
36645
- }
36646
- function isPlaycademyReviewAssessmentItemResultMetadataV1(value) {
36647
- if (!isRecord(value) || !isRecord(value.responses)) {
36648
- return false;
36649
- }
36650
- 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");
36651
- }
36652
- function isPlaycademyDiagnosticAssessmentItemResultMetadataV1(value) {
36653
- if (!isRecord(value) || !isRecord(value.responses)) {
36654
- return false;
36655
- }
36656
- 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";
36621
+ 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);
36657
36622
  }
36658
36623
  function playcademyAssessmentResultMetadata(value) {
36659
36624
  if (!isRecord(value)) {
@@ -36673,12 +36638,6 @@ function playcademyAssessmentResultMetadata(value) {
36673
36638
  diagnostic: { ...normalized.diagnostic, ledger: [] }
36674
36639
  };
36675
36640
  }
36676
- if (isRecord(normalized.diagnostic) && Array.isArray(normalized.diagnostic.ledger) && normalized.diagnostic.ledger.length === 0 && normalized.diagnostic.routingSnapshots === undefined) {
36677
- normalized = {
36678
- ...normalized,
36679
- diagnostic: { ...normalized.diagnostic, routingSnapshots: [] }
36680
- };
36681
- }
36682
36641
  if (isRecord(normalized.diagnostic) && isRecord(normalized.diagnostic.state)) {
36683
36642
  const state = normalized.diagnostic.state;
36684
36643
  const tracks = Array.isArray(state.tracks) ? state.tracks.map((track) => isRecord(track) ? {
@@ -36686,12 +36645,10 @@ function playcademyAssessmentResultMetadata(value) {
36686
36645
  currentNodeKey: track.currentNodeKey ?? null,
36687
36646
  terminal: track.terminal ?? null
36688
36647
  } : track) : state.tracks;
36689
- const routingSnapshots = Array.isArray(normalized.diagnostic.routingSnapshots) ? normalized.diagnostic.routingSnapshots.map((snapshot) => isRecord(snapshot) ? { ...snapshot, next: snapshot.next ?? null } : snapshot) : normalized.diagnostic.routingSnapshots;
36690
36648
  normalized = {
36691
36649
  ...normalized,
36692
36650
  diagnostic: {
36693
36651
  ...normalized.diagnostic,
36694
- ...routingSnapshots === undefined ? {} : { routingSnapshots },
36695
36652
  state: {
36696
36653
  ...state,
36697
36654
  ...tracks === undefined ? {} : { tracks },
@@ -36701,6 +36658,16 @@ function playcademyAssessmentResultMetadata(value) {
36701
36658
  };
36702
36659
  }
36703
36660
  const review = normalized.review;
36661
+ if (isRecord(normalized.transcript)) {
36662
+ const transcript = normalized.transcript;
36663
+ normalized = {
36664
+ ...normalized,
36665
+ transcript: {
36666
+ ...transcript,
36667
+ administrations: Array.isArray(transcript.administrations) ? transcript.administrations.map((entry2) => isRecord(entry2) ? { ...entry2, responses: entry2.responses ?? {} } : entry2) : transcript.administrations ?? []
36668
+ }
36669
+ };
36670
+ }
36704
36671
  if (isRecord(review) && isRecord(review.fulfillment) && review.fulfillment.status === "complete" && review.fulfillment.shortages === undefined) {
36705
36672
  normalized = {
36706
36673
  ...normalized,
@@ -36715,28 +36682,6 @@ function playcademyAssessmentResultMetadata(value) {
36715
36682
  }
36716
36683
  return isPlaycademyAssessmentResultMetadataV1(normalized) ? normalized : null;
36717
36684
  }
36718
- function playcademyAssessmentItemResultMetadata(value) {
36719
- if (!isRecord(value)) {
36720
- return null;
36721
- }
36722
- const metadata2 = value[PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY];
36723
- if (!isRecord(metadata2)) {
36724
- return null;
36725
- }
36726
- const normalized = metadata2.responses === undefined ? { ...metadata2, responses: {} } : metadata2;
36727
- return isPlaycademyReviewAssessmentItemResultMetadataV1(normalized) ? normalized : null;
36728
- }
36729
- function playcademyDiagnosticAssessmentItemResultMetadata(value) {
36730
- if (!isRecord(value)) {
36731
- return null;
36732
- }
36733
- const metadata2 = value[PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY];
36734
- if (!isRecord(metadata2)) {
36735
- return null;
36736
- }
36737
- const normalized = metadata2.responses === undefined ? { ...metadata2, responses: {} } : metadata2;
36738
- return isPlaycademyDiagnosticAssessmentItemResultMetadataV1(normalized) ? normalized : null;
36739
- }
36740
36685
  var SCRIPT_SCHEMES;
36741
36686
  var DATA_RASTER_IMAGE_PATTERN;
36742
36687
  var QTI_MATHML_ALLOWED_TAGS;
@@ -36794,6 +36739,7 @@ var ASSESSMENT_ATTEMPT_COMPLETED;
36794
36739
  var ASSESSMENT_ATTEMPT_AWAITING_AWARD;
36795
36740
  var ASSESSMENT_ATTEMPT_SUPERSEDED;
36796
36741
  var ASSESSMENT_RUNTIME_ERROR_STATUS;
36742
+ var GRADE_VALUES;
36797
36743
  var ROUTING_KEY_MAX_LENGTH = 128;
36798
36744
  var ROUTING_RESULT_KEY_MAX_LENGTH = 256;
36799
36745
  var ROUTING_STAGE_LIMIT = 32;
@@ -36809,8 +36755,10 @@ var DiagnosticTrackOutcomeSchema;
36809
36755
  var DiagnosticActivationPredicateSchema;
36810
36756
  var DiagnosticRoutingTransitionSchema;
36811
36757
  var DiagnosticRoutingManifestV1Schema;
36812
- var GRADE_VALUES;
36813
36758
  var POINT_RESPONSE_PATTERN;
36759
+ var Identity;
36760
+ var AssessmentTranscriptSchema;
36761
+ var AssessmentTranscriptError;
36814
36762
  var ZERO_WIDTH_CHARACTERS;
36815
36763
  var CURLY_SINGLE_QUOTES;
36816
36764
  var CURLY_DOUBLE_QUOTES;
@@ -36836,9 +36784,11 @@ var AssessmentChildResultAttemptV1Schema;
36836
36784
  var AssessmentChildResultAdministrationV1Schema;
36837
36785
  var AssessmentChildResultGradingV1Schema;
36838
36786
  var ReviewChildResultContextV1Schema;
36787
+ var FixedChildResultContextV1Schema;
36839
36788
  var DiagnosticChildResultContextV1Schema;
36840
36789
  var CommonPayloadShape;
36841
36790
  var ReviewChildResultPayloadV1Schema;
36791
+ var FixedChildResultPayloadV1Schema;
36842
36792
  var DiagnosticChildResultPayloadV1Schema;
36843
36793
  var AssessmentChildResultPayloadV1Schema;
36844
36794
  var AssessmentChildResultCommandV1Schema;
@@ -36857,12 +36807,16 @@ var StartAssessmentBodySchema;
36857
36807
  var AssessmentPreparationReceiptSchema;
36858
36808
  var StartAssessmentRequestBodySchema;
36859
36809
  var SaveAssessmentBodySchema;
36810
+ var RecordAssessmentProgressBodySchema;
36811
+ var ResumeAssessmentBodySchema;
36812
+ var ResumeRuntimeAssessmentRequestSchema;
36860
36813
  var SubmitAssessmentItemBodySchema;
36861
36814
  var SubmitAssessmentBodySchema;
36862
36815
  var FinalizeAssessmentBodySchema;
36863
36816
  var PrepareRuntimeAssessmentRequestSchema;
36864
36817
  var StartRuntimeAssessmentRequestSchema;
36865
36818
  var SaveRuntimeAssessmentRequestSchema;
36819
+ var RecordRuntimeAssessmentProgressRequestSchema;
36866
36820
  var SubmitAssessmentItemRuntimeRequestSchema;
36867
36821
  var SubmitRuntimeAssessmentRequestSchema;
36868
36822
  var FinalizeRuntimeAssessmentRequestSchema;
@@ -36875,13 +36829,14 @@ var init_assessment_runtime2 = __esm(() => {
36875
36829
  init_timeback3();
36876
36830
  init_esm();
36877
36831
  init_esm();
36832
+ init_src();
36833
+ init_uuid();
36834
+ init_esm();
36878
36835
  init_timeback4();
36879
36836
  init_src();
36880
36837
  init_timeback3();
36881
36838
  init_src();
36882
36839
  init_uuid();
36883
- init_src();
36884
- init_uuid();
36885
36840
  init_esm();
36886
36841
  init_src();
36887
36842
  init_uuid();
@@ -37177,6 +37132,7 @@ var init_assessment_runtime2 = __esm(() => {
37177
37132
  ATTEMPT_ALREADY_SUBMITTED: 409,
37178
37133
  ASSESSMENT_FLOW_VIOLATION: 409
37179
37134
  };
37135
+ GRADE_VALUES = TIMEBACK_GRADES;
37180
37136
  RoutingKeySchema = exports_external.string().trim().min(1).max(ROUTING_KEY_MAX_LENGTH);
37181
37137
  RoutingResultKeySchema = exports_external.string().trim().min(1).max(ROUTING_RESULT_KEY_MAX_LENGTH);
37182
37138
  DiagnosticTrackOutcomeSchema = exports_external.enum(DIAGNOSTIC_TRACK_OUTCOMES);
@@ -37234,8 +37190,31 @@ var init_assessment_runtime2 = __esm(() => {
37234
37190
  resultKey: RoutingResultKeySchema
37235
37191
  }).strict()).min(1).max(ROUTING_TERMINAL_LIMIT)
37236
37192
  }).strict();
37237
- GRADE_VALUES = TIMEBACK_GRADES;
37238
37193
  POINT_RESPONSE_PATTERN = /^-?\d+(?:\.\d+)? -?\d+(?:\.\d+)?$/;
37194
+ Identity = exports_external.string().min(1).max(512).refine((value) => value.trim() === value);
37195
+ AssessmentTranscriptSchema = exports_external.object({
37196
+ version: exports_external.literal(1),
37197
+ attemptId: Identity,
37198
+ contentRevision: Identity,
37199
+ administrations: exports_external.array(exports_external.object({
37200
+ submissionId: Identity,
37201
+ itemIdentifier: Identity,
37202
+ routingNodeKey: Identity.optional(),
37203
+ responses: exports_external.record(Identity, exports_external.custom(isAssessmentResponseValue))
37204
+ }).strict()).max(1000)
37205
+ }).strict().refine((value) => new TextEncoder().encode(JSON.stringify(value)).length <= 256000, {
37206
+ message: "Assessment transcript exceeds 256 KB"
37207
+ });
37208
+ AssessmentTranscriptError = class AssessmentTranscriptError2 extends Error {
37209
+ index;
37210
+ code;
37211
+ constructor(message, index = 0, code = "ASSESSMENT_FLOW_VIOLATION") {
37212
+ super(message);
37213
+ this.name = "AssessmentTranscriptError";
37214
+ this.index = index;
37215
+ this.code = code;
37216
+ }
37217
+ };
37239
37218
  ZERO_WIDTH_CHARACTERS = /[\u200B\uFEFF\u200C\u200D]/g;
37240
37219
  CURLY_SINGLE_QUOTES = /[\u2018\u2019\u201A\u201B]/g;
37241
37220
  CURLY_DOUBLE_QUOTES = /[\u201C\u201D\u201E\u201F]/g;
@@ -37327,6 +37306,10 @@ var init_assessment_runtime2 = __esm(() => {
37327
37306
  bankRevision: IdentitySchema,
37328
37307
  standard: AssessmentStandardRefSchema
37329
37308
  }).strict();
37309
+ FixedChildResultContextV1Schema = exports_external.object({
37310
+ kind: exports_external.literal("fixed"),
37311
+ purpose: exports_external.enum(["mastery", "end_of_course", "diagnostic"])
37312
+ }).strict();
37330
37313
  DiagnosticChildResultContextV1Schema = exports_external.object({
37331
37314
  kind: exports_external.literal("platform-routed-diagnostic"),
37332
37315
  definitionId: IdentitySchema,
@@ -37346,6 +37329,11 @@ var init_assessment_runtime2 = __esm(() => {
37346
37329
  isCorrect: exports_external.boolean().optional(),
37347
37330
  context: ReviewChildResultContextV1Schema
37348
37331
  }).strict();
37332
+ FixedChildResultPayloadV1Schema = exports_external.object({
37333
+ ...CommonPayloadShape,
37334
+ isCorrect: exports_external.boolean().optional(),
37335
+ context: FixedChildResultContextV1Schema
37336
+ }).strict();
37349
37337
  DiagnosticChildResultPayloadV1Schema = exports_external.object({
37350
37338
  ...CommonPayloadShape,
37351
37339
  isCorrect: exports_external.boolean(),
@@ -37361,7 +37349,8 @@ var init_assessment_runtime2 = __esm(() => {
37361
37349
  });
37362
37350
  AssessmentChildResultPayloadV1Schema = exports_external.union([
37363
37351
  ReviewChildResultPayloadV1Schema,
37364
- DiagnosticChildResultPayloadV1Schema
37352
+ DiagnosticChildResultPayloadV1Schema,
37353
+ FixedChildResultPayloadV1Schema
37365
37354
  ]);
37366
37355
  AssessmentChildResultCommandV1Schema = exports_external.object({
37367
37356
  kind: exports_external.literal(ASSESSMENT_CHILD_RESULT_KIND),
@@ -37438,9 +37427,19 @@ var init_assessment_runtime2 = __esm(() => {
37438
37427
  StartAssessmentRequestBodySchema = exports_external.intersection(StartAssessmentBodySchema, exports_external.object({ preparationReceipt: AssessmentPreparationReceiptSchema.optional() }));
37439
37428
  SaveAssessmentBodySchema = exports_external.object({
37440
37429
  expectedResponseVersion: exports_external.number().int().nonnegative(),
37441
- responses: exports_external.record(ResponseKeySchema2, exports_external.record(ResponseKeySchema2, AssessmentResponseValueSchema2.nullable()))
37430
+ transcript: AssessmentTranscriptSchema
37431
+ });
37432
+ RecordAssessmentProgressBodySchema = SaveAssessmentBodySchema.extend({
37433
+ assessmentToken: exports_external.string().min(1).max(32000),
37434
+ itemIdentifier: ResponseKeySchema2
37435
+ });
37436
+ ResumeAssessmentBodySchema = exports_external.object({
37437
+ transcript: AssessmentTranscriptSchema.optional()
37442
37438
  });
37439
+ ResumeRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.extend(ResumeAssessmentBodySchema.shape);
37443
37440
  SubmitAssessmentItemBodySchema = exports_external.object({
37441
+ assessmentToken: exports_external.string().min(1).max(32000),
37442
+ transcript: AssessmentTranscriptSchema,
37444
37443
  expectedResponseVersion: exports_external.number().int().nonnegative(),
37445
37444
  submissionId: exports_external.string().trim().min(1),
37446
37445
  itemIdentifier: ResponseKeySchema2,
@@ -37448,6 +37447,7 @@ var init_assessment_runtime2 = __esm(() => {
37448
37447
  responses: exports_external.record(ResponseKeySchema2, AssessmentResponseValueSchema2.nullable())
37449
37448
  });
37450
37449
  SubmitAssessmentBodySchema = exports_external.object({
37450
+ transcript: AssessmentTranscriptSchema,
37451
37451
  expectedResponseVersion: exports_external.number().int().nonnegative(),
37452
37452
  submissionId: exports_external.string().trim().min(1),
37453
37453
  session: exports_external.custom(isAssessmentSessionTiming, {
@@ -37466,6 +37466,7 @@ var init_assessment_runtime2 = __esm(() => {
37466
37466
  PrepareRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.and(StartAssessmentBodySchema);
37467
37467
  StartRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.and(StartAssessmentRequestBodySchema);
37468
37468
  SaveRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.extend(SaveAssessmentBodySchema.shape);
37469
+ RecordRuntimeAssessmentProgressRequestSchema = AssessmentRuntimeIdentitySchema.extend(RecordAssessmentProgressBodySchema.shape);
37469
37470
  SubmitAssessmentItemRuntimeRequestSchema = AssessmentRuntimeIdentitySchema.extend(SubmitAssessmentItemBodySchema.shape);
37470
37471
  SubmitRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.extend(SubmitAssessmentBodySchema.shape).extend({
37471
37472
  appName: exports_external.string().trim().min(1),
@@ -126102,22 +126103,6 @@ var init_locks = __esm(() => {
126102
126103
  }
126103
126104
  };
126104
126105
  });
126105
- async function crossAssessmentAttemptLockBarrier(lock, db2, attemptId) {
126106
- const maxAttempts = 5;
126107
- for (let attempt = 1;attempt <= maxAttempts; attempt += 1) {
126108
- try {
126109
- await lock(db2, [assessmentRuntimeAttemptLockKey(attemptId)], async () => {
126110
- return;
126111
- });
126112
- return;
126113
- } catch (error88) {
126114
- if (!(error88 instanceof ServiceUnavailableError) || attempt === maxAttempts) {
126115
- throw error88;
126116
- }
126117
- await sleep(25);
126118
- }
126119
- }
126120
- }
126121
126106
  function normalizeKeys(keys) {
126122
126107
  return [...new Set(keys)].toSorted();
126123
126108
  }
@@ -126191,17 +126176,6 @@ var init_assessment_runtime_lock_util = __esm(() => {
126191
126176
  }
126192
126177
  };
126193
126178
  });
126194
- function assessmentPreparationContentionError(expectedResponseVersion, responseVersion, kind) {
126195
- if (expectedResponseVersion === responseVersion) {
126196
- return new ServiceUnavailableError(`The ${kind} response state changed repeatedly while this item was being prepared. Try again shortly.`, {
126197
- retryable: true,
126198
- reason: "PREPARATION_CONTENTION",
126199
- expectedResponseVersion,
126200
- responseVersion
126201
- });
126202
- }
126203
- return AssessmentRuntimeError.from(assessmentResponseVersionConflict(expectedResponseVersion, responseVersion));
126204
- }
126205
126179
  function assessmentTrackableCourse(integration) {
126206
126180
  if (!isTimebackGrade2(integration.grade) || !isTimebackSubject(integration.subject)) {
126207
126181
  return null;
@@ -126267,7 +126241,7 @@ function buildConventionalAssessmentSnapshot(input) {
126267
126241
  ...input.activityData ? { activityData: input.activityData } : {},
126268
126242
  assessment: assessmentPresentationForAttempt(input.assessment, input.attemptId),
126269
126243
  responses: metadata2.responses,
126270
- itemSubmissions: metadata2.itemSubmissions,
126244
+ itemSubmissions: assessmentFlowForPurpose(metadata2.purpose) === "item-submit" ? metadata2.itemSubmissions : [],
126271
126245
  score: null,
126272
126246
  selection
126273
126247
  };
@@ -126317,151 +126291,9 @@ async function resolveAvailableAssessmentAttempt(initialDecision, select3, resum
126317
126291
  }
126318
126292
  return { kind: "continue", decision };
126319
126293
  }
126320
- function buildReviewItemResultMetadata(input) {
126321
- const administration = itemAdministration(input.metadata.itemSubmissions, input.selection.itemIdentifier);
126322
- if (!administration) {
126323
- throw new Error(`Cannot create a review result for unadministered candidate ${input.selection.itemIdentifier}`);
126324
- }
126325
- return {
126326
- version: PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_VERSION,
126327
- parentAttemptId: input.parentAttemptId,
126328
- activityId: input.metadata.activityId,
126329
- integrationId: input.metadata.integrationId,
126330
- enrollmentId: input.metadata.enrollmentId,
126331
- bankRevision: input.metadata.review.bankRevision,
126332
- itemIdentifier: input.selection.itemIdentifier,
126333
- standard: input.selection.standard,
126334
- deliveredAt: administration.submittedAt,
126335
- responseVersion: input.responseVersion ?? input.metadata.responseVersion,
126336
- responses: input.metadata.responses[input.selection.itemIdentifier] ?? {},
126337
- ...input.submissionId ? { submissionId: input.submissionId } : {},
126338
- ...input.isCorrect === undefined ? {} : { isCorrect: input.isCorrect }
126339
- };
126340
- }
126341
- function buildOpenReviewChildResult(input) {
126342
- return {
126343
- status: "active",
126344
- assessmentLineItem: { sourcedId: input.childLineItemId },
126345
- student: input.student,
126346
- score: 0,
126347
- scoreDate: input.metadata.deliveredAt,
126348
- ...ASSESSMENT_ATTEMPT_OPEN,
126349
- metadata: {
126350
- [PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY]: input.metadata
126351
- }
126352
- };
126353
- }
126354
126294
  function diagnosticQuestionLineItemId(parentLineItemId, itemIdentifier) {
126355
126295
  return deterministicUUID(["playcademy:diagnostic-question-line-item:v1", parentLineItemId, itemIdentifier].join("\x00"));
126356
126296
  }
126357
- function diagnosticQuestionResultId(parentAttemptId, childLineItemId) {
126358
- return deterministicUUID(["playcademy:diagnostic-question-result:v1", parentAttemptId, childLineItemId].join("\x00"));
126359
- }
126360
- function diagnosticAdministration(input) {
126361
- const ledgerIndex = input.metadata.diagnostic.ledger.findIndex((entry2) => entry2.routingNodeKey === input.ledgerEntry.routingNodeKey);
126362
- const persistedEntry = input.metadata.diagnostic.ledger[ledgerIndex];
126363
- const administration = input.metadata.itemSubmissions[ledgerIndex];
126364
- 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) {
126365
- throw new Error(`Cannot create a diagnostic result for uncommitted routing node ${input.ledgerEntry.routingNodeKey}`);
126366
- }
126367
- return administration;
126368
- }
126369
- function buildDiagnosticItemResultMetadata(input) {
126370
- const administration = diagnosticAdministration(input);
126371
- return {
126372
- version: PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_VERSION,
126373
- parentAttemptId: input.parentAttemptId,
126374
- activityId: input.metadata.activityId,
126375
- integrationId: input.metadata.integrationId,
126376
- enrollmentId: input.metadata.enrollmentId,
126377
- definitionId: input.metadata.diagnostic.definitionId,
126378
- diagnosticKey: input.metadata.diagnostic.diagnosticKey,
126379
- routingRevision: input.metadata.diagnostic.routingRevision,
126380
- contentRevision: input.metadata.selectedTest.contentRevision,
126381
- stageKey: input.ledgerEntry.stageKey,
126382
- trackKey: input.ledgerEntry.trackKey,
126383
- routingNodeKey: input.ledgerEntry.routingNodeKey,
126384
- itemIdentifier: input.ledgerEntry.itemIdentifier,
126385
- deliveredAt: administration.submittedAt,
126386
- responseVersion: administration.responseVersion,
126387
- responses: input.metadata.responses[input.ledgerEntry.itemIdentifier] ?? {},
126388
- submissionId: administration.submissionId,
126389
- isCorrect: input.ledgerEntry.isCorrect
126390
- };
126391
- }
126392
- function resolveDiagnosticChildResult(input) {
126393
- if (!input.childResult) {
126394
- return { action: "create" };
126395
- }
126396
- const childMetadata = playcademyDiagnosticAssessmentItemResultMetadata(input.childResult.metadata);
126397
- const expectedMetadata = buildDiagnosticItemResultMetadata({
126398
- metadata: input.parentMetadata,
126399
- ledgerEntry: input.ledgerEntry,
126400
- parentAttemptId: input.parentResult.sourcedId
126401
- });
126402
- if (input.childResult.assessmentLineItem.sourcedId !== input.childLineItemId || input.childResult.student.sourcedId !== input.parentResult.student.sourcedId || !childMetadata || canonicalJson2(childMetadata) !== canonicalJson2(expectedMetadata)) {
126403
- return { action: "restore", reason: "incompatible" };
126404
- }
126405
- return input.childResult.status === "active" ? { action: "reuse", metadata: expectedMetadata } : { action: "restore", reason: "inactive" };
126406
- }
126407
- function buildFinalizedDiagnosticChildResult(input) {
126408
- const administration = diagnosticAdministration(input);
126409
- const metadata2 = buildDiagnosticItemResultMetadata(input);
126410
- return {
126411
- resultUpdate: {
126412
- status: "active",
126413
- assessmentLineItem: { sourcedId: input.childLineItemId },
126414
- student: input.student,
126415
- score: administration.score.earned,
126416
- scoreDate: administration.submittedAt,
126417
- ...ASSESSMENT_ATTEMPT_COMPLETED,
126418
- metadata: {
126419
- [PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY]: metadata2
126420
- }
126421
- }
126422
- };
126423
- }
126424
- function buildFinalizedReviewChildResult(input) {
126425
- const metadata2 = buildReviewItemResultMetadata({
126426
- metadata: input.metadata,
126427
- selection: input.selection,
126428
- parentAttemptId: input.parentAttemptId,
126429
- submissionId: input.submissionId,
126430
- isCorrect: input.itemResult.isCorrect
126431
- });
126432
- return {
126433
- resultUpdate: {
126434
- status: "active",
126435
- assessmentLineItem: { sourcedId: input.childLineItemId },
126436
- student: input.student,
126437
- score: input.itemResult.score.earned,
126438
- scoreDate: input.timestamp,
126439
- ...ASSESSMENT_ATTEMPT_COMPLETED,
126440
- metadata: {
126441
- [PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY]: metadata2
126442
- }
126443
- },
126444
- outcome: {
126445
- itemIdentifier: input.selection.itemIdentifier,
126446
- standard: input.selection.standard,
126447
- answered: Object.keys(input.metadata.responses[input.selection.itemIdentifier] ?? {}).length > 0,
126448
- score: input.itemResult.score,
126449
- isCorrect: input.itemResult.isCorrect
126450
- }
126451
- };
126452
- }
126453
- function resolveReviewChildResult(input) {
126454
- if (!input.childResult) {
126455
- return { action: "create" };
126456
- }
126457
- const childMetadata = playcademyAssessmentItemResultMetadata(input.childResult.metadata);
126458
- const expectedResponses = input.parentMetadata.responses[input.selection.itemIdentifier] ?? {};
126459
- const administration = itemAdministration(input.parentMetadata.itemSubmissions, input.selection.itemIdentifier);
126460
- 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)) {
126461
- return { action: "restore", reason: "incompatible" };
126462
- }
126463
- return input.childResult.status === "active" ? { action: "reuse", metadata: childMetadata } : { action: "restore", reason: "inactive" };
126464
- }
126465
126297
  function buildAssessmentSubmitResult(input) {
126466
126298
  if (!input.metadata.score || !input.metadata.submissionId) {
126467
126299
  return null;
@@ -126847,32 +126679,12 @@ function assessmentFixtureScoringKeys(assessment, questions) {
126847
126679
  ...Object.keys(responseAreas).length > 0 ? { responseAreas } : {}
126848
126680
  };
126849
126681
  }
126850
- function prepareDiagnosticAssessmentResponses(assessment, current, input) {
126851
- const update2 = { [input.itemIdentifier]: input.responses };
126852
- validateAssessmentResponseUpdate(assessment, update2);
126853
- const responses = applyAssessmentResponseUpdate(current, update2);
126854
- const itemResponses = responses[input.itemIdentifier];
126855
- validateAssessmentDraftResponses(assessment, itemResponses ? { [input.itemIdentifier]: itemResponses } : {});
126856
- return responses;
126857
- }
126858
126682
  function validateAssessmentResponseUpdate(assessment, update2) {
126859
126683
  const message = assessmentResponseUpdateValidationMessage(assessment, update2);
126860
126684
  if (message) {
126861
126685
  throw new AssessmentRuntimeError("INVALID_RESPONSE", message);
126862
126686
  }
126863
126687
  }
126864
- function validateAssessmentResponses(assessment, responses) {
126865
- const message = assessmentResponseValidationMessage(assessment, responses);
126866
- if (message) {
126867
- throw new AssessmentRuntimeError("INVALID_RESPONSE", message);
126868
- }
126869
- }
126870
- function validateAssessmentDraftResponses(assessment, responses) {
126871
- const message = assessmentDraftResponseValidationMessage(assessment, responses);
126872
- if (message) {
126873
- throw new AssessmentRuntimeError("INVALID_RESPONSE", message);
126874
- }
126875
- }
126876
126688
  function assessmentScoringRequests(assessment, responses) {
126877
126689
  return assessment.items.map((item) => ({
126878
126690
  itemIdentifier: item.identifier,
@@ -126912,6 +126724,21 @@ function assessmentScoreFromPercentage(percentage, possible) {
126912
126724
  const earned = Math.round(normalized * possible * 1e6) / 1e6;
126913
126725
  return { earned, possible, normalized };
126914
126726
  }
126727
+ function buildAssessmentResultCheckpoint(input) {
126728
+ return {
126729
+ status: input.result.status,
126730
+ assessmentLineItem: input.result.assessmentLineItem,
126731
+ student: input.result.student,
126732
+ score: input.result.score ?? 0,
126733
+ scoreDate: input.result.scoreDate,
126734
+ ...ASSESSMENT_ATTEMPT_OPEN,
126735
+ metadata: mergeAssessmentRuntimeMetadata(input.result.metadata, {
126736
+ ...input.metadata,
126737
+ transcript: input.transcript,
126738
+ updatedAt: input.timestamp
126739
+ })
126740
+ };
126741
+ }
126915
126742
  function buildAssessmentResultSubmission(input) {
126916
126743
  const metadata2 = {
126917
126744
  ...input.metadata,
@@ -128316,6 +128143,184 @@ var init_timeback_assessment_artifacts_util = __esm(() => {
128316
128143
  init_timeback_assessment_runtime_util();
128317
128144
  init_timeback_review_mapping_util();
128318
128145
  });
128146
+ async function scoreCachedAssessmentItems(input) {
128147
+ const selected = input.assessment.items.filter((item) => input.itemIdentifiers.has(item.identifier));
128148
+ const keys = new Map(await Promise.all(selected.map(async (item) => [
128149
+ item.identifier,
128150
+ await sha256Hex(canonicalJson2({
128151
+ identity: input.artifactIdentity,
128152
+ purpose: input.purpose,
128153
+ itemIdentifier: item.identifier,
128154
+ responses: input.responses[item.identifier] ?? {},
128155
+ strategy: input.artifact?.items.find((candidate) => candidate.itemIdentifier === item.identifier)?.strategy ?? "qti"
128156
+ }))
128157
+ ])));
128158
+ const scored = new Map;
128159
+ for (const item of selected) {
128160
+ const cached3 = input.cache.get(keys.get(item.identifier));
128161
+ if (cached3) {
128162
+ scored.set(item.identifier, cached3);
128163
+ }
128164
+ }
128165
+ const missing = new Set(selected.filter((item) => !scored.has(item.identifier)).map((item) => item.identifier));
128166
+ const fresh = await scoreAssessmentItems({ ...input, itemIdentifiers: missing });
128167
+ for (const result of fresh) {
128168
+ input.cache.set(keys.get(result.itemIdentifier), result);
128169
+ scored.set(result.itemIdentifier, result);
128170
+ }
128171
+ return selected.map((item) => scored.get(item.identifier));
128172
+ }
128173
+ async function scoreAssessmentItems({
128174
+ assessment,
128175
+ responses,
128176
+ artifactIdentity,
128177
+ purpose,
128178
+ itemIdentifiers,
128179
+ artifact,
128180
+ processResponse,
128181
+ onEvent
128182
+ }) {
128183
+ const scoringItems = assessmentScoringRequests(assessment, responses).filter((item) => !itemIdentifiers || itemIdentifiers.has(item.itemIdentifier));
128184
+ const artifactItems = new Map(artifact?.items.map((item) => [item.itemIdentifier, item]));
128185
+ const scored = new Map;
128186
+ const qti2 = [];
128187
+ for (const item of scoringItems) {
128188
+ const artifactItem = artifactItems.get(item.itemIdentifier);
128189
+ let fallback2 = null;
128190
+ if (!artifact) {
128191
+ fallback2 = { reason: "artifact-unavailable" };
128192
+ } else if (!artifactItem) {
128193
+ fallback2 = { reason: "artifact-item-missing" };
128194
+ } else if (artifactItem.strategy === "qti") {
128195
+ fallback2 = { reason: "artifact-qti", artifactReason: artifactItem.reason };
128196
+ } else {
128197
+ const startedAt = Date.now();
128198
+ try {
128199
+ scored.set(item.itemIdentifier, {
128200
+ ...scorePlatformAssessmentItem(artifactItem, responses[item.itemIdentifier]),
128201
+ grading: PLATFORM_CHILD_GRADING
128202
+ });
128203
+ onEvent("assessment.item_scoring", {
128204
+ "app.assessment.qti_item_identifier": item.itemIdentifier,
128205
+ "app.assessment.purpose": purpose,
128206
+ "app.assessment.artifact_identity_kind": artifactIdentity.kind,
128207
+ "app.assessment.grading_source": "platform-artifact",
128208
+ "app.assessment.grading_duration_ms": Date.now() - startedAt
128209
+ });
128210
+ } catch (error88) {
128211
+ fallback2 = { reason: "platform-grader-error", error: error88 };
128212
+ }
128213
+ }
128214
+ if (fallback2) {
128215
+ qti2.push({ item, ...fallback2 });
128216
+ }
128217
+ }
128218
+ if (qti2.length > 0) {
128219
+ const fallbackTasks = qti2.flatMap(({ item, reason, artifactReason, error: graderError }) => {
128220
+ const interactionResults = [];
128221
+ let startedAt;
128222
+ let completedInteractions = 0;
128223
+ let failed = false;
128224
+ const omittedResponseCount = item.requests.filter((request) => responses[item.itemIdentifier]?.[request.identifier] === undefined).length;
128225
+ let qtiRemoteInteractionCount = 0;
128226
+ let syntheticIncorrectInteractionCount = 0;
128227
+ function fallbackEvent(outcome) {
128228
+ onEvent("assessment.item_scoring_fallback", {
128229
+ "app.assessment.qti_item_identifier": item.itemIdentifier,
128230
+ "app.assessment.purpose": purpose,
128231
+ "app.assessment.artifact_identity_kind": artifactIdentity.kind,
128232
+ "app.assessment.grading_source": omittedResponseCount > 0 ? "platform-qti-adapter" : "timeback-qti",
128233
+ "app.assessment.fallback_reason": reason,
128234
+ "app.assessment.fallback_duration_ms": startedAt === undefined ? 0 : Date.now() - startedAt,
128235
+ "app.assessment.qti_remote_interaction_count": qtiRemoteInteractionCount,
128236
+ "app.assessment.synthetic_incorrect_interaction_count": syntheticIncorrectInteractionCount,
128237
+ ...artifactReason ? { "app.assessment.artifact_fallback_reason": artifactReason } : {},
128238
+ ...graderError ? {
128239
+ "app.assessment.platform_grader_exception_type": errorType(graderError)
128240
+ } : {},
128241
+ ...outcome
128242
+ });
128243
+ }
128244
+ function completeItem() {
128245
+ const earned = assessmentItemEarnedScore(interactionResults.map((result) => Number.isFinite(result.score) ? result.score : 0), item.maxScore);
128246
+ const possible = item.maxScore;
128247
+ fallbackEvent({ "app.assessment.fallback_outcome": "succeeded" });
128248
+ scored.set(item.itemIdentifier, {
128249
+ itemIdentifier: item.itemIdentifier,
128250
+ score: {
128251
+ earned,
128252
+ possible,
128253
+ normalized: possible === 0 ? 0 : earned / possible
128254
+ },
128255
+ isCorrect: assessmentItemCorrectness(interactionResults.map((result) => result.isCorrect)),
128256
+ grading: omittedResponseCount > 0 ? PLATFORM_QTI_ADAPTER_CHILD_GRADING : QTI_CHILD_GRADING
128257
+ });
128258
+ }
128259
+ if (item.requests.length === 0) {
128260
+ completeItem();
128261
+ return [];
128262
+ }
128263
+ return item.requests.map((request, index2) => async () => {
128264
+ startedAt ??= Date.now();
128265
+ try {
128266
+ const response = responses[item.itemIdentifier]?.[request.identifier];
128267
+ if (response === undefined) {
128268
+ syntheticIncorrectInteractionCount += 1;
128269
+ interactionResults[index2] = { score: 0, isCorrect: false };
128270
+ } else {
128271
+ qtiRemoteInteractionCount += 1;
128272
+ interactionResults[index2] = await processResponse(item.itemIdentifier, request);
128273
+ }
128274
+ completedInteractions += 1;
128275
+ if (completedInteractions === item.requests.length && !failed) {
128276
+ completeItem();
128277
+ }
128278
+ } catch (error88) {
128279
+ if (!failed) {
128280
+ failed = true;
128281
+ fallbackEvent({
128282
+ "app.assessment.fallback_outcome": "failed",
128283
+ "exception.type": errorType(error88),
128284
+ "app.error.message": errorMessage2(error88)
128285
+ });
128286
+ }
128287
+ throw error88;
128288
+ }
128289
+ });
128290
+ });
128291
+ await runWithConcurrency(fallbackTasks, SCORING_CONCURRENCY, (task) => task());
128292
+ }
128293
+ return scoringItems.map((item) => {
128294
+ const result = scored.get(item.itemIdentifier);
128295
+ if (!result) {
128296
+ throw new Error(`Assessment item ${item.itemIdentifier} was not scored`);
128297
+ }
128298
+ return result;
128299
+ });
128300
+ }
128301
+ var QTI_CHILD_GRADING;
128302
+ var PLATFORM_QTI_ADAPTER_CHILD_GRADING;
128303
+ var PLATFORM_CHILD_GRADING;
128304
+ var SCORING_CONCURRENCY = 4;
128305
+ var init_timeback_assessment_scoring_util = __esm(() => {
128306
+ init_assessment_runtime2();
128307
+ init_assessment_artifact_envelope_util();
128308
+ init_timeback_assessment_runtime_util();
128309
+ QTI_CHILD_GRADING = {
128310
+ source: "timeback-qti",
128311
+ graderVersion: "timeback-qti-process-response-v1"
128312
+ };
128313
+ PLATFORM_QTI_ADAPTER_CHILD_GRADING = {
128314
+ source: "platform-qti-adapter",
128315
+ graderVersion: "platform-qti-adapter-v1",
128316
+ qtiGraderVersion: "timeback-qti-process-response-v1"
128317
+ };
128318
+ PLATFORM_CHILD_GRADING = {
128319
+ source: "platform-artifact",
128320
+ artifactVersion: `assessment-artifact-v${ASSESSMENT_ARTIFACT_SCHEMA_VERSION}`,
128321
+ graderVersion: `platform-grader-v${ASSESSMENT_GRADER_PROTOCOL_VERSION}`
128322
+ };
128323
+ });
128319
128324
  async function buildSubmissionChildResultCommand(input) {
128320
128325
  const { submission, context: context2 } = input;
128321
128326
  const { isCorrect } = submission;
@@ -128330,12 +128335,9 @@ async function buildSubmissionChildResultCommand(input) {
128330
128335
  score: submission.score,
128331
128336
  grading: input.grading
128332
128337
  };
128333
- if (context2.kind === "review") {
128334
- return buildAssessmentChildResultCommand({
128335
- ...common2,
128336
- ...typeof isCorrect === "boolean" ? { isCorrect } : {},
128337
- context: context2
128338
- }, submission.submittedAt);
128338
+ if (context2.kind !== "platform-routed-diagnostic") {
128339
+ const payload = { ...common2, ...typeof isCorrect === "boolean" ? { isCorrect } : {} };
128340
+ return context2.kind === "review" ? buildAssessmentChildResultCommand({ ...payload, context: context2 }, submission.submittedAt) : buildAssessmentChildResultCommand({ ...payload, context: context2 }, submission.submittedAt);
128339
128341
  }
128340
128342
  if (typeof isCorrect !== "boolean") {
128341
128343
  throw new Error("Diagnostic child results require determinate correctness");
@@ -128356,11 +128358,7 @@ function assessmentChildLineItemMetadataMatches(key, stored, expected) {
128356
128358
  if (!Array.isArray(storedStandards) || !Array.isArray(expectedStandards)) {
128357
128359
  return false;
128358
128360
  }
128359
- const sameDefinition = canonicalJson2(reviewDefinitionWithoutStandards(storedDefinition)) === canonicalJson2(reviewDefinitionWithoutStandards(expectedDefinition));
128360
- function contains(standards, candidate) {
128361
- return standards.some((standard) => canonicalJson2(standard) === canonicalJson2(candidate));
128362
- }
128363
- return sameDefinition && (expectedStandards.every((standard) => contains(storedStandards, standard)) || storedStandards.every((standard) => contains(expectedStandards, standard)));
128361
+ return canonicalJson2(reviewDefinitionWithoutStandards(storedDefinition)) === canonicalJson2(reviewDefinitionWithoutStandards(expectedDefinition));
128364
128362
  }
128365
128363
  function assessmentChildLineItemMatches(stored, expected) {
128366
128364
  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) {
@@ -128418,19 +128416,14 @@ async function classifyAssessmentChildResult(input) {
128418
128416
  result.scoreScale,
128419
128417
  result.scorePercentile,
128420
128418
  result.comment,
128421
- result.learningObjectiveSet,
128422
- result.incomplete,
128423
- result.late,
128424
- result.missing
128419
+ result.learningObjectiveSet
128425
128420
  ];
128426
- if (auxiliaryFields.some((value) => value !== undefined && value !== null)) {
128421
+ const flags2 = [result.incomplete, result.late, result.missing];
128422
+ if (auxiliaryFields.some((value) => value !== undefined && value !== null) || flags2.some((value) => value !== undefined && value !== null && value !== "false")) {
128427
128423
  return { outcome: "conflict", reason: "auxiliary-field-mismatch" };
128428
128424
  }
128429
128425
  return { outcome: "replay" };
128430
128426
  }
128431
- function hasApiStatus(error88, statusCode) {
128432
- return isApiError(error88) && error88.statusCode === statusCode;
128433
- }
128434
128427
  async function persistAssessmentChildResult(input) {
128435
128428
  const { assessmentResults, childLineItemId } = input;
128436
128429
  const verification2 = await verifyAssessmentChildResultCommand(input.command);
@@ -128439,38 +128432,127 @@ async function persistAssessmentChildResult(input) {
128439
128432
  }
128440
128433
  const { command } = verification2;
128441
128434
  const resultId = await assessmentChildResultId(command.payload.attempt.attemptId, command.payload.administration.submissionId);
128442
- async function classifyOccupant() {
128443
- const resolution = await classifyAssessmentChildResult({
128444
- result: await assessmentResults.get(resultId),
128435
+ await assessmentResults.upsert(resultId, buildAssessmentChildResultUpdate(command, childLineItemId));
128436
+ }
128437
+ function assessmentTranscriptChildLineItemId(context2, itemIdentifier) {
128438
+ return context2.metadata.purpose === "review" ? reviewQuestionLineItemId(context2.parentLineItemId, itemIdentifier) : diagnosticQuestionLineItemId(context2.parentLineItemId, itemIdentifier);
128439
+ }
128440
+ async function reconcileAssessmentTranscriptChildren({
128441
+ context: context2,
128442
+ graded,
128443
+ only,
128444
+ assessmentResults,
128445
+ ensureLineItems,
128446
+ onPersisted
128447
+ }) {
128448
+ const metadata2 = graded.metadata;
128449
+ const assessmentKey = metadata2.selectedTest.assessmentKey;
128450
+ if (!assessmentKey) {
128451
+ throw new Error("Assessment context requires a selected assessment key");
128452
+ }
128453
+ const submissions = metadata2.itemSubmissions.filter((submission) => !only || only.has(submission.submissionId));
128454
+ const administered = new Set(submissions.map((submission) => submission.itemIdentifier));
128455
+ await ensureLineItems({
128456
+ items: graded.assessment.items.filter((item) => administered.has(item.identifier))
128457
+ });
128458
+ await runWithConcurrency(submissions, 4, async (submission) => {
128459
+ const index2 = submission.responseVersion - 1;
128460
+ let flow;
128461
+ if (metadata2.purpose === "review") {
128462
+ const selection = metadata2.review.selections.find((item) => item.itemIdentifier === submission.itemIdentifier);
128463
+ flow = {
128464
+ kind: "review",
128465
+ bankRevision: metadata2.review.bankRevision,
128466
+ standard: selection.standard
128467
+ };
128468
+ } else if (metadata2.purpose === "diagnostic" && metadata2.diagnostic) {
128469
+ flow = {
128470
+ kind: "platform-routed-diagnostic",
128471
+ definitionId: metadata2.diagnostic.definitionId,
128472
+ diagnosticKey: metadata2.diagnostic.diagnosticKey,
128473
+ routingRevision: metadata2.diagnostic.routingRevision,
128474
+ transition: metadata2.diagnostic.ledger[index2]
128475
+ };
128476
+ } else {
128477
+ flow = { kind: "fixed", purpose: metadata2.purpose };
128478
+ }
128479
+ const command = await buildSubmissionChildResultCommand({
128480
+ attempt: {
128481
+ attemptId: context2.attemptId,
128482
+ gameId: context2.gameId,
128483
+ studentId: context2.studentId,
128484
+ activityId: metadata2.activityId,
128485
+ courseId: metadata2.courseId,
128486
+ integrationId: metadata2.integrationId,
128487
+ enrollmentId: metadata2.enrollmentId,
128488
+ selectedTest: { ...metadata2.selectedTest, assessmentKey }
128489
+ },
128490
+ submission,
128491
+ responses: graded.transcript.administrations[index2].responses,
128492
+ grading: submission.grading ?? QTI_CHILD_GRADING,
128493
+ context: flow
128494
+ });
128495
+ await persistAssessmentChildResult({
128496
+ assessmentResults,
128445
128497
  command,
128446
- childLineItemId
128498
+ childLineItemId: await assessmentTranscriptChildLineItemId(context2, submission.itemIdentifier)
128447
128499
  });
128448
- return resolution.outcome === "replay" ? { outcome: "replay", resultId } : { outcome: "conflict", resultId, reason: resolution.reason };
128500
+ onPersisted?.(submission.submissionId);
128501
+ });
128502
+ }
128503
+ async function recoveredAssessmentAdministration(context2, result) {
128504
+ const verified = await verifyAssessmentChildResultCommand(result.metadata);
128505
+ if (!verified.ok) {
128506
+ return null;
128449
128507
  }
128450
- try {
128451
- return await classifyOccupant();
128452
- } catch (error88) {
128453
- if (!hasApiStatus(error88, 404)) {
128454
- throw error88;
128508
+ const { command } = verified;
128509
+ const { attempt, administration, responses, context: flow } = command.payload;
128510
+ 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)) {
128511
+ return null;
128512
+ }
128513
+ const disposition = await classifyAssessmentChildResult({
128514
+ result,
128515
+ command,
128516
+ childLineItemId: await assessmentTranscriptChildLineItemId(context2, administration.itemIdentifier)
128517
+ });
128518
+ if (disposition.outcome !== "replay") {
128519
+ return null;
128520
+ }
128521
+ return {
128522
+ sequence: administration.responseVersion,
128523
+ entry: {
128524
+ submissionId: administration.submissionId,
128525
+ itemIdentifier: administration.itemIdentifier,
128526
+ responses,
128527
+ ...flow.kind === "platform-routed-diagnostic" ? { routingNodeKey: flow.transition.routingNodeKey } : {}
128528
+ }
128529
+ };
128530
+ }
128531
+ async function recoverAssessmentTranscript(context2, results) {
128532
+ const entries = new Map;
128533
+ for (const result of results) {
128534
+ const recovered = await recoveredAssessmentAdministration(context2, result);
128535
+ if (recovered) {
128536
+ const atPosition = entries.get(recovered.sequence) ?? new Map;
128537
+ atPosition.set(recovered.entry.submissionId, recovered.entry);
128538
+ entries.set(recovered.sequence, atPosition);
128455
128539
  }
128456
128540
  }
128457
- try {
128458
- await assessmentResults.create({
128459
- sourcedId: resultId,
128460
- ...buildAssessmentChildResultUpdate(command, childLineItemId)
128461
- });
128462
- return { outcome: "created", resultId };
128463
- } catch (error88) {
128464
- if (!hasApiStatus(error88, 409)) {
128465
- throw error88;
128541
+ const transcript = emptyAssessmentTranscript(context2.attemptId, context2.metadata.selectedTest.contentRevision);
128542
+ for (let sequence2 = 1;sequence2 <= entries.size; sequence2 += 1) {
128543
+ const candidates = entries.get(sequence2);
128544
+ if (!candidates || candidates.size !== 1) {
128545
+ break;
128466
128546
  }
128547
+ transcript.administrations.push(candidates.values().next().value);
128467
128548
  }
128468
- return classifyOccupant();
128549
+ return transcript;
128469
128550
  }
128470
- var init_timeback_assessment_child_result_util = __esm(async () => {
128551
+ var init_timeback_assessment_child_result_util = __esm(() => {
128471
128552
  init_src();
128472
128553
  init_assessment_runtime2();
128473
- await init_errors8();
128554
+ init_timeback_assessment_runtime_util();
128555
+ init_timeback_assessment_scoring_util();
128474
128556
  });
128475
128557
 
128476
128558
  class AssessmentPreparationFlights {
@@ -128574,6 +128656,7 @@ function buildPreparedAssessmentMetadata(plan, timestamp6, testName) {
128574
128656
  version: PLAYCADEMY_ASSESSMENT_RESULT_METADATA_VERSION,
128575
128657
  activityId: plan.activityId,
128576
128658
  courseId: plan.courseId,
128659
+ courseName: plan.courseName,
128577
128660
  integrationId: plan.integrationId,
128578
128661
  enrollmentId: plan.enrollmentId,
128579
128662
  selectedTest: plan.selectedTest,
@@ -128604,7 +128687,6 @@ function buildPreparedAssessmentMetadata(plan, timestamp6, testName) {
128604
128687
  testName,
128605
128688
  routingRevision: plan.diagnostic.routingRevision,
128606
128689
  ledger: [],
128607
- routingSnapshots: [],
128608
128690
  state: plan.diagnostic.initialState
128609
128691
  }
128610
128692
  };
@@ -128900,123 +128982,6 @@ var init_timeback_assessment_publication_util = __esm(() => {
128900
128982
  ]);
128901
128983
  MANAGED_ASSET_URL = new RegExp(`https?://[^\\s"')]+/(${ASSESSMENT_ASSET_KEY_PREFIX}v1/sha256/[a-f0-9]{64}\\.[a-z0-9]+)`, "gi");
128902
128984
  });
128903
- function assessmentChildProjectionIsTerminal(result) {
128904
- const state = { inProgress: result.inProgress ?? "", scoreStatus: result.scoreStatus };
128905
- return isAssessmentAttemptAwaitingAward(state) || isAssessmentAttemptCompleted(state);
128906
- }
128907
- async function reconcileReviewChildResults({
128908
- initial,
128909
- writeOpen,
128910
- crossBarrier,
128911
- readLatest,
128912
- restoreTerminal
128913
- }) {
128914
- if (initial.metadata.purpose !== "review") {
128915
- throw new Error("A standards-review request selected a non-review attempt");
128916
- }
128917
- if (assessmentChildProjectionIsTerminal(initial.result)) {
128918
- const settled = { result: initial.result, metadata: initial.metadata };
128919
- await restoreTerminal(settled);
128920
- return settled;
128921
- }
128922
- await writeOpen();
128923
- await crossBarrier();
128924
- const current = await readLatest();
128925
- if (current.metadata.purpose !== "review") {
128926
- throw new Error("A standards-review request selected a non-review attempt");
128927
- }
128928
- const reviewed = { result: current.result, metadata: current.metadata };
128929
- if (assessmentChildProjectionIsTerminal(current.result)) {
128930
- await restoreTerminal(reviewed);
128931
- }
128932
- return reviewed;
128933
- }
128934
- var init_timeback_assessment_reconciliation_util = __esm(() => {
128935
- init_assessment_runtime2();
128936
- });
128937
- function resolveDiagnosticItemReplay(metadata2, input) {
128938
- const priorIndex = metadata2.itemSubmissions.findIndex((submission) => submission.submissionId === input.submissionId);
128939
- if (priorIndex === -1) {
128940
- return { action: "missing" };
128941
- }
128942
- const prior = metadata2.itemSubmissions[priorIndex];
128943
- const ledgerEntry = metadata2.diagnostic.ledger[priorIndex];
128944
- if (!prior || !ledgerEntry || prior.itemIdentifier !== input.itemIdentifier || ledgerEntry.routingNodeKey !== input.routingNodeKey || !itemResponseUpdateMatches(metadata2.responses[input.itemIdentifier], input.responses)) {
128945
- return {
128946
- action: "conflict",
128947
- failure: assessmentFlowViolation("This diagnostic submission ID was already used for a different request.", {
128948
- submissionId: input.submissionId,
128949
- routingNodeKey: input.routingNodeKey,
128950
- itemIdentifier: input.itemIdentifier
128951
- })
128952
- };
128953
- }
128954
- return {
128955
- action: "replay",
128956
- submission: prior,
128957
- ledgerEntry,
128958
- priorIndex,
128959
- routing: metadata2.diagnostic.routingSnapshots?.[priorIndex]
128960
- };
128961
- }
128962
- function buildAssessmentItemReplayResult(attemptId, metadata2, submission) {
128963
- return {
128964
- attemptId,
128965
- responseVersion: metadata2.responseVersion,
128966
- status: "in_progress",
128967
- responses: metadata2.responses,
128968
- itemSubmissions: metadata2.itemSubmissions,
128969
- submission
128970
- };
128971
- }
128972
- function diagnosticReceipt(submission, ledgerEntry) {
128973
- return {
128974
- submissionId: submission.submissionId,
128975
- routingNodeKey: ledgerEntry.routingNodeKey,
128976
- itemIdentifier: submission.itemIdentifier,
128977
- submittedAt: submission.submittedAt,
128978
- responseVersion: submission.responseVersion,
128979
- answered: submission.answered,
128980
- score: submission.score,
128981
- isCorrect: ledgerEntry.isCorrect
128982
- };
128983
- }
128984
- function buildDiagnosticItemReplayResult(attemptId, submission, ledgerEntry, routing) {
128985
- return {
128986
- attemptId,
128987
- responseVersion: submission.responseVersion,
128988
- status: "in_progress",
128989
- routing,
128990
- submission: diagnosticReceipt(submission, ledgerEntry)
128991
- };
128992
- }
128993
- function requireAssessmentMutationAccess(attemptId, integrationStatus, enrollment) {
128994
- if (!enrollment || integrationStatus === "deactivated") {
128995
- throw AssessmentRuntimeError.from(assessmentAttemptUnauthorized(attemptId));
128996
- }
128997
- return enrollment;
128998
- }
128999
- function committedReviewItemResults(metadata2, assessment) {
129000
- const available = new Set(assessment.items.map((item) => item.identifier));
129001
- const seen = new Set;
129002
- return metadata2.itemSubmissions.map((submission) => {
129003
- if (!available.has(submission.itemIdentifier) || seen.has(submission.itemIdentifier)) {
129004
- throw AssessmentRuntimeError.from(assessmentFlowViolation("The persisted review item-submission ledger is corrupt.", {
129005
- itemIdentifier: submission.itemIdentifier
129006
- }));
129007
- }
129008
- seen.add(submission.itemIdentifier);
129009
- return {
129010
- itemIdentifier: submission.itemIdentifier,
129011
- score: submission.score,
129012
- isCorrect: submission.isCorrect
129013
- };
129014
- });
129015
- }
129016
- var init_timeback_assessment_replay_util = __esm(() => {
129017
- init_assessment_runtime2();
129018
- init_errors2();
129019
- });
129020
128985
  function diagnosticRoutingFallbackReason(artifact, routingRevision) {
129021
128986
  if (!artifact) {
129022
128987
  return "artifact-unavailable";
@@ -129162,156 +129127,252 @@ function validateUniqueAssessmentIdentifiers(testIdentifiers) {
129162
129127
  var init_timeback_assessment_rules_util = __esm(() => {
129163
129128
  init_errors2();
129164
129129
  });
129165
- async function scoreAssessmentItems({
129166
- assessment,
129167
- responses,
129168
- artifactIdentity,
129169
- purpose,
129170
- itemIdentifiers,
129171
- artifact,
129172
- processResponse,
129173
- onEvent
129174
- }) {
129175
- const scoringItems = assessmentScoringRequests(assessment, responses).filter((item) => !itemIdentifiers || itemIdentifiers.has(item.itemIdentifier));
129176
- const artifactItems = new Map(artifact?.items.map((item) => [item.itemIdentifier, item]));
129177
- const scored = new Map;
129178
- const qti2 = [];
129179
- for (const item of scoringItems) {
129180
- const artifactItem = artifactItems.get(item.itemIdentifier);
129181
- let fallback2 = null;
129182
- if (!artifact) {
129183
- fallback2 = { reason: "artifact-unavailable" };
129184
- } else if (!artifactItem) {
129185
- fallback2 = { reason: "artifact-item-missing" };
129186
- } else if (artifactItem.strategy === "qti") {
129187
- fallback2 = { reason: "artifact-qti", artifactReason: artifactItem.reason };
129188
- } else {
129189
- const startedAt = Date.now();
129190
- try {
129191
- scored.set(item.itemIdentifier, {
129192
- ...scorePlatformAssessmentItem(artifactItem, responses[item.itemIdentifier]),
129193
- grading: PLATFORM_CHILD_GRADING
129194
- });
129195
- onEvent("assessment.item_scoring", {
129196
- "app.assessment.qti_item_identifier": item.itemIdentifier,
129197
- "app.assessment.purpose": purpose,
129198
- "app.assessment.artifact_identity_kind": artifactIdentity.kind,
129199
- "app.assessment.grading_source": "platform-artifact",
129200
- "app.assessment.grading_duration_ms": Date.now() - startedAt
129201
- });
129202
- } catch (error88) {
129203
- fallback2 = { reason: "platform-grader-error", error: error88 };
129130
+ function diagnosticReceipt(submission, ledgerEntry) {
129131
+ return {
129132
+ submissionId: submission.submissionId,
129133
+ routingNodeKey: ledgerEntry.routingNodeKey,
129134
+ itemIdentifier: submission.itemIdentifier,
129135
+ submittedAt: submission.submittedAt,
129136
+ responseVersion: submission.responseVersion,
129137
+ answered: submission.answered,
129138
+ score: submission.score,
129139
+ isCorrect: ledgerEntry.isCorrect
129140
+ };
129141
+ }
129142
+ function parseAssessmentTranscript(value) {
129143
+ const parsed = AssessmentTranscriptSchema.safeParse(value);
129144
+ if (!parsed.success) {
129145
+ throw new ValidationError("Invalid assessment transcript");
129146
+ }
129147
+ return parsed.data;
129148
+ }
129149
+ function assessmentTokenMetadata(metadata2) {
129150
+ const context2 = { ...metadata2, updatedAt: metadata2.startedAt };
129151
+ delete context2.transcript;
129152
+ return context2;
129153
+ }
129154
+ function requireAssessmentTokenContext(context2, scope) {
129155
+ if (!context2 || context2.gameId !== scope.gameId || context2.studentId !== scope.studentId || context2.attemptId !== scope.attemptId) {
129156
+ throw AssessmentRuntimeError.from(assessmentAttemptUnauthorized(scope.attemptId));
129157
+ }
129158
+ return context2;
129159
+ }
129160
+ function requireAssessmentFeedbackContext(tokenContext, scope) {
129161
+ const context2 = requireAssessmentTokenContext(tokenContext, scope);
129162
+ const metadata2 = context2.metadata;
129163
+ if (assessmentFlowForPurpose(metadata2.purpose) === "attempt-submit" && !(metadata2.purpose === "diagnostic" && metadata2.diagnostic)) {
129164
+ throw AssessmentRuntimeError.from(assessmentFlowViolation("This assessment accepts a complete attempt, not per-item feedback."));
129165
+ }
129166
+ return context2;
129167
+ }
129168
+ function prepareAssessmentProgress(tokenContext, scope, input) {
129169
+ const context2 = requireAssessmentTokenContext(tokenContext, scope);
129170
+ const transcript = parseAssessmentTranscript(input.transcript);
129171
+ const progress = assessmentProgressAdministration(context2.metadata.purpose, {
129172
+ ...input,
129173
+ transcript
129174
+ });
129175
+ if (!progress.ok) {
129176
+ throw AssessmentRuntimeError.from(progress.failure);
129177
+ }
129178
+ return { context: context2, transcript, submissionId: progress.submissionId };
129179
+ }
129180
+ function prepareAssessmentItemTranscript(assessment, input) {
129181
+ validateAssessmentResponseUpdate(assessment, { [input.itemIdentifier]: input.responses });
129182
+ const appended = appendAssessmentAdministration(input.transcript, input);
129183
+ if (!appended.ok) {
129184
+ throw AssessmentRuntimeError.from(appended.failure);
129185
+ }
129186
+ return appended;
129187
+ }
129188
+ function prepareAttemptAssessmentTranscript(context2, assessment, transcript, terminal = false) {
129189
+ return prepareAssessmentTranscript({
129190
+ transcript,
129191
+ attemptId: context2.attemptId,
129192
+ contentRevision: context2.metadata.selectedTest.contentRevision,
129193
+ assessment,
129194
+ routed: context2.metadata.purpose === "diagnostic" && context2.metadata.diagnostic !== undefined,
129195
+ allowIncomplete: !terminal && assessmentFlowForPurpose(context2.metadata.purpose) === "attempt-submit"
129196
+ });
129197
+ }
129198
+ function requireTranscriptWriteAccess(attemptId, integrationStatus) {
129199
+ if (integrationStatus === "deactivated") {
129200
+ throw AssessmentRuntimeError.from(assessmentAttemptUnauthorized(attemptId));
129201
+ }
129202
+ }
129203
+ function requireTranscriptVersion(transcript, expectedResponseVersion) {
129204
+ if (expectedResponseVersion !== transcript.administrations.length) {
129205
+ throw AssessmentRuntimeError.from(assessmentResponseVersionConflict(expectedResponseVersion, transcript.administrations.length));
129206
+ }
129207
+ }
129208
+ function requireAssessmentTranscriptCheckpoint(attempt, integrationStatus, input) {
129209
+ requireTranscriptWriteAccess(attempt.sourcedId, integrationStatus);
129210
+ requireTranscriptVersion(input.transcript, input.expectedResponseVersion);
129211
+ if (!isAssessmentAttemptOpen({ ...attempt, inProgress: attempt.inProgress ?? "" })) {
129212
+ throw AssessmentRuntimeError.from(assessmentAttemptNotOpen(attempt.sourcedId));
129213
+ }
129214
+ }
129215
+ function assessmentTranscriptRequestHash(input) {
129216
+ return sha256Hex(canonicalJson2({
129217
+ transcript: input.transcript,
129218
+ ...input.session ? { session: input.session } : {}
129219
+ }));
129220
+ }
129221
+ function resolveAssessmentTranscriptSubmission(attempt, metadata2, integrationStatus, input, requestHash) {
129222
+ const disposition = classifyAssessmentSubmission({
129223
+ inProgress: attempt.inProgress ?? "",
129224
+ scoreStatus: attempt.scoreStatus,
129225
+ submissionId: metadata2.submissionId
129226
+ }, input.submissionId);
129227
+ if (disposition === "replay" && metadata2.completion?.requestHash === requestHash) {
129228
+ return "replay";
129229
+ }
129230
+ if (disposition === "reject") {
129231
+ throw AssessmentRuntimeError.from(assessmentAlreadySubmitted(attempt.sourcedId));
129232
+ }
129233
+ if (disposition !== "submit") {
129234
+ throw AssessmentRuntimeError.from(assessmentFlowViolation("A different completion was already accepted for this attempt."));
129235
+ }
129236
+ requireTranscriptWriteAccess(attempt.sourcedId, integrationStatus);
129237
+ if (input.session && input.session.runId !== attempt.sourcedId) {
129238
+ throw new ValidationError("Assessment session runId must match the attemptId.");
129239
+ }
129240
+ requireTranscriptVersion(input.transcript, input.expectedResponseVersion);
129241
+ return "submit";
129242
+ }
129243
+ function buildGradedAssessmentTranscript(input) {
129244
+ const { context: context2, prepared, itemResults, assessment, submittedAt, manifest } = input;
129245
+ const completed = completeAssessmentTranscript({
129246
+ transcript: prepared.transcript,
129247
+ scores: itemResults,
129248
+ purpose: context2.metadata.purpose,
129249
+ itemCount: assessment.items.length,
129250
+ submittedAt,
129251
+ manifest,
129252
+ terminal: input.terminal
129253
+ });
129254
+ const common2 = {
129255
+ ...context2.metadata,
129256
+ responses: prepared.responses,
129257
+ responseVersion: completed.itemSubmissions.length,
129258
+ itemSubmissions: completed.itemSubmissions,
129259
+ updatedAt: submittedAt,
129260
+ transcript: prepared.transcript
129261
+ };
129262
+ const diagnostic = context2.metadata.purpose === "diagnostic" ? context2.metadata.diagnostic : undefined;
129263
+ const metadata2 = diagnostic ? {
129264
+ ...common2,
129265
+ purpose: "diagnostic",
129266
+ diagnostic: { ...diagnostic, ledger: completed.ledger, state: completed.state }
129267
+ } : common2;
129268
+ return {
129269
+ metadata: metadata2,
129270
+ transcript: prepared.transcript,
129271
+ assessment,
129272
+ itemResults,
129273
+ manifest,
129274
+ score: aggregateAssessmentScore(itemResults.map((item) => item.score.earned), itemResults.reduce((sum, item) => sum + item.score.possible, 0)),
129275
+ correctQuestions: countCorrectQuestions(itemResults.map((item) => item.isCorrect))
129276
+ };
129277
+ }
129278
+ function assessmentTranscriptRuntimeError(error88) {
129279
+ return error88 instanceof AssessmentTranscriptError ? AssessmentRuntimeError.from({
129280
+ code: error88.code,
129281
+ message: error88.message,
129282
+ details: { transcriptIndex: error88.index }
129283
+ }) : error88;
129284
+ }
129285
+ function buildAssessmentTranscriptFeedback(attemptId, graded, index2) {
129286
+ const { metadata: metadata2 } = graded;
129287
+ const submission = metadata2.itemSubmissions[index2];
129288
+ const common2 = {
129289
+ attemptId,
129290
+ responseVersion: metadata2.responseVersion,
129291
+ status: "in_progress",
129292
+ transcript: graded.transcript
129293
+ };
129294
+ return metadata2.purpose === "diagnostic" && metadata2.diagnostic ? {
129295
+ ...common2,
129296
+ routing: projectDiagnosticRoutingSnapshot(metadata2.diagnostic.routingRevision, metadata2.diagnostic.state),
129297
+ submission: diagnosticReceipt(submission, metadata2.diagnostic.ledger[index2])
129298
+ } : {
129299
+ ...common2,
129300
+ responses: metadata2.responses,
129301
+ itemSubmissions: metadata2.itemSubmissions,
129302
+ submission
129303
+ };
129304
+ }
129305
+ async function resumeAssessmentTranscript(input) {
129306
+ const retained = input.suppliedTranscript ?? input.checkpointTranscript;
129307
+ let transcript = retained ?? await input.recover();
129308
+ while (true) {
129309
+ try {
129310
+ return await input.grade(transcript);
129311
+ } catch (error88) {
129312
+ const index2 = error88 instanceof AssessmentRuntimeError && isRecord(error88.details) ? error88.details.transcriptIndex : undefined;
129313
+ if (retained !== undefined || typeof index2 !== "number" || index2 < 0 || index2 >= transcript.administrations.length) {
129314
+ throw error88;
129204
129315
  }
129205
- }
129206
- if (fallback2) {
129207
- qti2.push({ item, ...fallback2 });
129316
+ transcript = {
129317
+ ...transcript,
129318
+ administrations: transcript.administrations.slice(0, index2)
129319
+ };
129208
129320
  }
129209
129321
  }
129210
- if (qti2.length > 0) {
129211
- const fallbackTasks = qti2.flatMap(({ item, reason, artifactReason, error: graderError }) => {
129212
- const interactionResults = [];
129213
- let startedAt;
129214
- let completedInteractions = 0;
129215
- let failed = false;
129216
- const omittedResponseCount = item.requests.filter((request) => responses[item.itemIdentifier]?.[request.identifier] === undefined).length;
129217
- let qtiRemoteInteractionCount = 0;
129218
- let syntheticIncorrectInteractionCount = 0;
129219
- function fallbackEvent(outcome) {
129220
- onEvent("assessment.item_scoring_fallback", {
129221
- "app.assessment.qti_item_identifier": item.itemIdentifier,
129222
- "app.assessment.purpose": purpose,
129223
- "app.assessment.artifact_identity_kind": artifactIdentity.kind,
129224
- "app.assessment.grading_source": omittedResponseCount > 0 ? "platform-qti-adapter" : "timeback-qti",
129225
- "app.assessment.fallback_reason": reason,
129226
- "app.assessment.fallback_duration_ms": startedAt === undefined ? 0 : Date.now() - startedAt,
129227
- "app.assessment.qti_remote_interaction_count": qtiRemoteInteractionCount,
129228
- "app.assessment.synthetic_incorrect_interaction_count": syntheticIncorrectInteractionCount,
129229
- ...artifactReason ? { "app.assessment.artifact_fallback_reason": artifactReason } : {},
129230
- ...graderError ? {
129231
- "app.assessment.platform_grader_exception_type": errorType(graderError)
129232
- } : {},
129233
- ...outcome
129234
- });
129235
- }
129236
- function completeItem() {
129237
- const earned = assessmentItemEarnedScore(interactionResults.map((result) => Number.isFinite(result.score) ? result.score : 0), item.maxScore);
129238
- const possible = item.maxScore;
129239
- fallbackEvent({ "app.assessment.fallback_outcome": "succeeded" });
129240
- scored.set(item.itemIdentifier, {
129241
- itemIdentifier: item.itemIdentifier,
129242
- score: {
129243
- earned,
129244
- possible,
129245
- normalized: possible === 0 ? 0 : earned / possible
129246
- },
129247
- isCorrect: assessmentItemCorrectness(interactionResults.map((result) => result.isCorrect)),
129248
- grading: omittedResponseCount > 0 ? PLATFORM_QTI_ADAPTER_CHILD_GRADING : QTI_CHILD_GRADING
129249
- });
129250
- }
129251
- if (item.requests.length === 0) {
129252
- completeItem();
129253
- return [];
129322
+ }
129323
+ function assessmentReviewTranscriptOutcomes(input) {
129324
+ const resultByItem = new Map(input.itemResults.map((item) => [item.itemIdentifier, item]));
129325
+ const administered = administeredItemIdentifiers(input.metadata.itemSubmissions);
129326
+ return input.metadata.review.selections.filter((selection) => administered.has(selection.itemIdentifier)).map((selection) => {
129327
+ const result = resultByItem.get(selection.itemIdentifier);
129328
+ const submission = itemAdministration(input.metadata.itemSubmissions, selection.itemIdentifier);
129329
+ if (!result || !submission) {
129330
+ throw new Error(`Scoring omitted selected review item ${selection.itemIdentifier}`);
129331
+ }
129332
+ return {
129333
+ itemIdentifier: selection.itemIdentifier,
129334
+ standard: selection.standard,
129335
+ answered: submission.answered,
129336
+ score: result.score,
129337
+ isCorrect: result.isCorrect
129338
+ };
129339
+ });
129340
+ }
129341
+ function assessmentReviewTranscriptExposures(input) {
129342
+ const exposures = [];
129343
+ const authoritative = new Set;
129344
+ for (const { result, metadata: metadata2 } of input.attempts) {
129345
+ const state = { ...result, inProgress: result.inProgress ?? "" };
129346
+ const settled = isAssessmentAttemptAwaitingAward(state) || isAssessmentAttemptCompleted(state);
129347
+ if (metadata2.purpose === "review" && (metadata2.transcript !== undefined || settled)) {
129348
+ authoritative.add(result.sourcedId);
129349
+ if (metadata2.review.bankRevision === input.bankRevision) {
129350
+ const accepted = settled ? metadata2.itemSubmissions.map((submission) => ({
129351
+ itemIdentifier: submission.itemIdentifier,
129352
+ deliveredAt: submission.submittedAt
129353
+ })) : metadata2.transcript.administrations.map((entry2) => ({
129354
+ itemIdentifier: entry2.itemIdentifier,
129355
+ deliveredAt: metadata2.updatedAt
129356
+ }));
129357
+ exposures.push(...accepted);
129254
129358
  }
129255
- return item.requests.map((request, index2) => async () => {
129256
- startedAt ??= Date.now();
129257
- try {
129258
- const response = responses[item.itemIdentifier]?.[request.identifier];
129259
- if (response === undefined) {
129260
- syntheticIncorrectInteractionCount += 1;
129261
- interactionResults[index2] = { score: 0, isCorrect: false };
129262
- } else {
129263
- qtiRemoteInteractionCount += 1;
129264
- interactionResults[index2] = await processResponse(item.itemIdentifier, request);
129265
- }
129266
- completedInteractions += 1;
129267
- if (completedInteractions === item.requests.length && !failed) {
129268
- completeItem();
129269
- }
129270
- } catch (error88) {
129271
- if (!failed) {
129272
- failed = true;
129273
- fallbackEvent({
129274
- "app.assessment.fallback_outcome": "failed",
129275
- "exception.type": errorType(error88),
129276
- "app.error.message": errorMessage2(error88)
129277
- });
129278
- }
129279
- throw error88;
129280
- }
129281
- });
129282
- });
129283
- await runWithConcurrency(fallbackTasks, SCORING_CONCURRENCY, (task) => task());
129359
+ }
129284
129360
  }
129285
- return scoringItems.map((item) => {
129286
- const result = scored.get(item.itemIdentifier);
129287
- if (!result) {
129288
- throw new Error(`Assessment item ${item.itemIdentifier} was not scored`);
129361
+ for (const command of input.children) {
129362
+ const { attempt, administration, context: context2 } = command.payload;
129363
+ if (!authoritative.has(attempt.attemptId) && context2.kind === "review" && context2.bankRevision === input.bankRevision) {
129364
+ exposures.push({
129365
+ itemIdentifier: administration.itemIdentifier,
129366
+ deliveredAt: command.recordedAt
129367
+ });
129289
129368
  }
129290
- return result;
129291
- });
129369
+ }
129370
+ return exposures;
129292
129371
  }
129293
- var QTI_CHILD_GRADING;
129294
- var PLATFORM_QTI_ADAPTER_CHILD_GRADING;
129295
- var PLATFORM_CHILD_GRADING;
129296
- var SCORING_CONCURRENCY = 4;
129297
- var init_timeback_assessment_scoring_util = __esm(() => {
129372
+ var init_timeback_assessment_transcript_util = __esm(() => {
129298
129373
  init_assessment_runtime2();
129299
- init_assessment_artifact_envelope_util();
129374
+ init_errors2();
129300
129375
  init_timeback_assessment_runtime_util();
129301
- QTI_CHILD_GRADING = {
129302
- source: "timeback-qti",
129303
- graderVersion: "timeback-qti-process-response-v1"
129304
- };
129305
- PLATFORM_QTI_ADAPTER_CHILD_GRADING = {
129306
- source: "platform-qti-adapter",
129307
- graderVersion: "platform-qti-adapter-v1",
129308
- qtiGraderVersion: "timeback-qti-process-response-v1"
129309
- };
129310
- PLATFORM_CHILD_GRADING = {
129311
- source: "platform-artifact",
129312
- artifactVersion: `assessment-artifact-v${ASSESSMENT_ARTIFACT_SCHEMA_VERSION}`,
129313
- graderVersion: `platform-grader-v${ASSESSMENT_GRADER_PROTOCOL_VERSION}`
129314
- };
129315
129376
  });
129316
129377
  function qtiItemHref(client2, itemIdentifier) {
129317
129378
  return `${client2.getQtiBaseUrl()}/assessment-items/${itemIdentifier}`;
@@ -129395,20 +129456,19 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
129395
129456
  init_assessment_artifact_runtime_util();
129396
129457
  init_assessment_runtime_lock_util();
129397
129458
  init_timeback_assessment_artifacts_util();
129459
+ init_timeback_assessment_child_result_util();
129398
129460
  init_timeback_assessment_preparation_util();
129399
129461
  init_timeback_assessment_publication_util();
129400
- init_timeback_assessment_reconciliation_util();
129401
- init_timeback_assessment_replay_util();
129402
129462
  init_timeback_assessment_routing_util();
129403
129463
  init_timeback_assessment_rules_util();
129404
129464
  init_timeback_assessment_runtime_util();
129405
129465
  init_timeback_assessment_scoring_util();
129466
+ init_timeback_assessment_transcript_util();
129406
129467
  init_timeback_qti_hydration_util();
129407
129468
  await __promiseAll([
129408
129469
  init_dist5(),
129409
129470
  init_errors8(),
129410
- init_assessment_artifact_loader_util(),
129411
- init_timeback_assessment_child_result_util()
129471
+ init_assessment_artifact_loader_util()
129412
129472
  ]);
129413
129473
  AssessmentPreparationPrerequisiteError = class AssessmentPreparationPrerequisiteError2 extends Error {
129414
129474
  original;
@@ -129423,7 +129483,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
129423
129483
  static PREPARATION_IN_FLIGHT_LIMIT = 32;
129424
129484
  static ASSESSMENT_CACHE_TTL_MS = 60000;
129425
129485
  static EXPORT_CONCURRENCY = 4;
129426
- static ITEM_SUBMISSION_MAX_PREPARATION_ATTEMPTS = 3;
129427
129486
  static ASSESSMENT_VERSION_CONFIRMATION_DELAY_MS = 25;
129428
129487
  static SCORING_CONCURRENCY = 4;
129429
129488
  static STALE_AWARD_AGE_MS = 900000;
@@ -129441,6 +129500,14 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
129441
129500
  maxSize: TimebackAssessmentRuntimeService2.ASSESSMENT_CACHE_LIMIT
129442
129501
  });
129443
129502
  artifactLoader;
129503
+ itemScoreCache = new TimebackCache({
129504
+ defaultTTL: 300000,
129505
+ maxSize: 512
129506
+ });
129507
+ childLineItemCache = new TimebackCache({
129508
+ defaultTTL: 300000,
129509
+ maxSize: 512
129510
+ });
129444
129511
  preparations = new AssessmentPreparationFlights(TimebackAssessmentRuntimeService2.PREPARATION_IN_FLIGHT_LIMIT);
129445
129512
  constructor(deps) {
129446
129513
  this.deps = deps;
@@ -129567,6 +129634,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
129567
129634
  version: PLAYCADEMY_ASSESSMENT_RESULT_METADATA_VERSION,
129568
129635
  activityId: input.activityId,
129569
129636
  courseId: context2.integration.courseId,
129637
+ courseName: context2.enrollment.course.title,
129570
129638
  integrationId: context2.integration.id,
129571
129639
  enrollmentId: context2.enrollment.id,
129572
129640
  selectedTest: {
@@ -129623,7 +129691,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
129623
129691
  enrollmentId: context2.enrollment.id
129624
129692
  };
129625
129693
  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);
129626
- const listing = this.attemptListing(results, attemptScope);
129694
+ const listing = await this.attemptListing(results, attemptScope);
129627
129695
  const decision = selectRuntimeAssessment(tests, [
129628
129696
  ...this.attemptsMatchingRequest(listing.attempts, this.startRequestFingerprint(params.input)).values()
129629
129697
  ].map((entry2) => entry2.selection));
@@ -129646,6 +129714,18 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
129646
129714
  let prepared;
129647
129715
  try {
129648
129716
  prepared = await this.buildPreparationPlan(params, requestHash, preparation);
129717
+ const assessment = await this.loadPreparedPlanAssessment(prepared.plan);
129718
+ if (!assessment) {
129719
+ throw new Error("Prepared assessment artifact is unavailable");
129720
+ }
129721
+ await this.provisionPreparationPrerequisites(() => this.ensureTranscriptLineItems({
129722
+ version: 1,
129723
+ gameId: prepared.plan.gameId,
129724
+ studentId: prepared.plan.studentId,
129725
+ attemptId: prepared.plan.attemptId,
129726
+ parentLineItemId: prepared.plan.parentLineItemId,
129727
+ metadata: buildPreparedAssessmentMetadata(prepared.plan, new Date().toISOString(), assessment.title)
129728
+ }, assessment));
129649
129729
  } catch (error88) {
129650
129730
  if (error88 instanceof ValidationError || error88 instanceof AssessmentRuntimeError) {
129651
129731
  throw error88;
@@ -129731,6 +129811,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
129731
129811
  integrationId: context2.integration.id,
129732
129812
  enrollmentId: context2.enrollment.id,
129733
129813
  courseId: context2.integration.courseId,
129814
+ courseName: context2.enrollment.course.title,
129734
129815
  ...attempt,
129735
129816
  parentLineItemId,
129736
129817
  selectedTest: {
@@ -129771,7 +129852,9 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
129771
129852
  const selected = selectReviewItems({
129772
129853
  request: params.input,
129773
129854
  bank: catalog.bank,
129774
- exposures: this.reviewExposures(listing.reviewChildren.values(), {
129855
+ exposures: assessmentReviewTranscriptExposures({
129856
+ attempts: listing.attempts.values(),
129857
+ children: listing.reviewChildren.values(),
129775
129858
  bankRevision: catalog.bank.bankRevision
129776
129859
  })
129777
129860
  });
@@ -129820,6 +129903,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
129820
129903
  integrationId: context2.integration.id,
129821
129904
  enrollmentId: context2.enrollment.id,
129822
129905
  courseId: context2.integration.courseId,
129906
+ courseName: context2.enrollment.course.title,
129823
129907
  ...attempt,
129824
129908
  parentLineItemId,
129825
129909
  selectedTest: {
@@ -129971,6 +130055,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
129971
130055
  activityId: input.activityId,
129972
130056
  purpose: "diagnostic",
129973
130057
  courseId: context2.integration.courseId,
130058
+ courseName: context2.enrollment.course.title,
129974
130059
  integrationId: context2.integration.id,
129975
130060
  enrollmentId: context2.enrollment.id,
129976
130061
  selectedTest: {
@@ -129990,7 +130075,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
129990
130075
  testName: assessment.title,
129991
130076
  routingRevision: routing.routingRevision,
129992
130077
  ledger: [],
129993
- routingSnapshots: [],
129994
130078
  state: routing.state
129995
130079
  }
129996
130080
  };
@@ -130009,7 +130093,9 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
130009
130093
  })
130010
130094
  });
130011
130095
  await this.persistPendingSupersessionsBestEffort(db2, pendingSupersessions);
130012
- return this.diagnosticSnapshot(result, metadata2, assessment, context2.integration, routing.state);
130096
+ return this.diagnosticSnapshot(result, metadata2, assessment, context2.integration, {
130097
+ state: routing.state
130098
+ });
130013
130099
  });
130014
130100
  }
130015
130101
  async startReview({
@@ -130043,7 +130129,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
130043
130129
  this.listAttempts(attemptScope)
130044
130130
  ]);
130045
130131
  const attempts = listing.attempts;
130046
- const initialResolution = await this.resumeAvailableAttempt(tests, attempts, requestFingerprint, pendingSupersessions, (resumeDecision) => this.resumeReviewSnapshot(resumeDecision, attempts, listing.reviewChildren, context2));
130132
+ const initialResolution = await this.resumeAvailableAttempt(tests, attempts, requestFingerprint, pendingSupersessions, (resumeDecision) => this.resumeReviewSnapshot(resumeDecision, attempts, context2));
130047
130133
  if (initialResolution.kind === "resumed") {
130048
130134
  await this.persistPendingSupersessionsBestEffort(db2, pendingSupersessions);
130049
130135
  return initialResolution.snapshot;
@@ -130062,7 +130148,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
130062
130148
  });
130063
130149
  const refreshedListing = await this.listAttempts(attemptScope);
130064
130150
  const refreshed = refreshedListing.attempts;
130065
- const refreshedResolution = await this.resumeAvailableAttempt(tests, refreshed, requestFingerprint, pendingSupersessions, (resumeDecision) => this.resumeReviewSnapshot(resumeDecision, refreshed, refreshedListing.reviewChildren, context2));
130151
+ const refreshedResolution = await this.resumeAvailableAttempt(tests, refreshed, requestFingerprint, pendingSupersessions, (resumeDecision) => this.resumeReviewSnapshot(resumeDecision, refreshed, context2));
130066
130152
  if (refreshedResolution.kind === "resumed") {
130067
130153
  await this.persistPendingSupersessionsBestEffort(db2, pendingSupersessions);
130068
130154
  return refreshedResolution.snapshot;
@@ -130075,7 +130161,9 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
130075
130161
  if (catalog.test.identifier !== decision.test.qtiTestIdentifier) {
130076
130162
  catalog = await this.loadReviewBankCatalog(decision.test.qtiTestIdentifier);
130077
130163
  }
130078
- const exposures = this.reviewExposures(refreshedListing.reviewChildren.values(), {
130164
+ const exposures = assessmentReviewTranscriptExposures({
130165
+ attempts: refreshedListing.attempts.values(),
130166
+ children: refreshedListing.reviewChildren.values(),
130079
130167
  bankRevision: catalog.bank.bankRevision
130080
130168
  });
130081
130169
  const selected = selectReviewItems({
@@ -130114,6 +130202,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
130114
130202
  activityId: input.activityId,
130115
130203
  purpose: input.purpose,
130116
130204
  courseId: context2.integration.courseId,
130205
+ courseName: context2.enrollment.course.title,
130117
130206
  integrationId: context2.integration.id,
130118
130207
  enrollmentId: context2.enrollment.id,
130119
130208
  selectedTest: {
@@ -130160,8 +130249,14 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
130160
130249
  });
130161
130250
  }
130162
130251
  async get(params) {
130252
+ return this.resume({ ...params, input: {} });
130253
+ }
130254
+ async resume({
130255
+ input,
130256
+ ...params
130257
+ }) {
130163
130258
  const attempt = await this.authorizeAttemptRead(params);
130164
- return this.snapshotForResult(attempt.result, attempt.metadata, attempt.integration);
130259
+ return this.snapshotForResult(attempt.result, attempt.metadata, attempt.integration, input.transcript);
130165
130260
  }
130166
130261
  async latest({
130167
130262
  gameId,
@@ -130223,496 +130318,104 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
130223
130318
  }
130224
130319
  async save({ input, ...params }) {
130225
130320
  await this.deps.validateDeveloperAccess(params.user, params.gameId);
130226
- try {
130227
- const warmup = await this.peekAttempt(params);
130228
- if (this.isOpen(warmup.result) && !this.isPlatformRoutedDiagnosticMetadata(warmup.metadata)) {
130229
- await this.loadAttemptAssessment(warmup.metadata);
130230
- }
130231
- } catch (error88) {
130232
- addEvent("assessment.save_warmup_failed", {
130233
- "app.assessment.attempt_id": params.attemptId,
130234
- "exception.type": errorType(error88),
130235
- "app.error.message": errorMessage2(error88)
130236
- });
130237
- }
130238
130321
  return this.deps.assessmentRuntimeLock(this.deps.db, [assessmentRuntimeAttemptLockKey(params.attemptId)], async (db2) => {
130239
- const attempt = await this.authorizeAttempt(params, {
130322
+ const { attempt } = await this.resolveAttemptAuthorization(params, {
130240
130323
  db: db2,
130241
- developerAccessValidated: true
130324
+ developerAccessValidated: true,
130325
+ includeEnrollment: false
130242
130326
  });
130243
- if (this.isPlatformRoutedDiagnosticMetadata(attempt.metadata)) {
130244
- throw AssessmentRuntimeError.from(assessmentFlowViolation("Platform-routed diagnostics do not accept draft saves.", { flow: "platform-routed-item-submit" }));
130245
- }
130246
- this.assertInProgress(attempt.result);
130247
- this.assertResponseVersion(attempt.metadata, input.expectedResponseVersion);
130248
- const assessment = await this.loadAttemptAssessment(attempt.metadata);
130249
- const flowFailure = assessmentResponseUpdateFlowFailure(attempt.metadata.purpose, attempt.metadata.itemSubmissions, input.responses);
130250
- if (flowFailure) {
130251
- throw AssessmentRuntimeError.from(flowFailure);
130252
- }
130253
- validateAssessmentResponseUpdate(assessment, input.responses);
130254
- const responses = applyAssessmentResponseUpdate(attempt.metadata.responses, input.responses);
130255
- validateAssessmentDraftResponses(assessment, responses);
130256
- const metadata2 = {
130257
- ...attempt.metadata,
130258
- responses,
130259
- responseVersion: attempt.metadata.responseVersion + 1,
130260
- updatedAt: new Date().toISOString()
130261
- };
130262
- await this.putResultMetadata(attempt.result, metadata2);
130327
+ requireAssessmentTranscriptCheckpoint(attempt.result, attempt.integration.status, input);
130328
+ const graded = await this.gradeTranscript({
130329
+ attemptId: params.attemptId,
130330
+ metadata: attempt.metadata
130331
+ }, input.transcript, false, db2);
130332
+ await this.requireClient().api.oneroster.assessmentResults.upsert(params.attemptId, buildAssessmentResultCheckpoint({
130333
+ result: attempt.result,
130334
+ metadata: attempt.metadata,
130335
+ transcript: graded.transcript,
130336
+ timestamp: graded.metadata.updatedAt
130337
+ }));
130338
+ this.reconcileTranscriptChildren(this.tokenContext(attempt), graded).catch((error88) => this.recordTranscriptFailure("checkpoint_projection", params.attemptId, error88));
130263
130339
  return {
130264
- attemptId: attempt.result.sourcedId,
130265
- responseVersion: metadata2.responseVersion,
130340
+ attemptId: params.attemptId,
130341
+ responseVersion: graded.metadata.responseVersion,
130266
130342
  status: "in_progress",
130267
- responses
130343
+ responses: graded.metadata.responses,
130344
+ transcript: graded.transcript
130268
130345
  };
130269
130346
  });
130270
130347
  }
130271
- async submitItem({
130348
+ async recordProgress({
130272
130349
  input,
130273
130350
  ...params
130274
130351
  }) {
130275
130352
  await this.deps.validateDeveloperAccess(params.user, params.gameId);
130276
- if (input.routingNodeKey !== undefined) {
130277
- const identity = await this.peekAttempt(params);
130278
- if (this.isPlatformRoutedDiagnosticMetadata(identity.metadata)) {
130279
- return this.submitDiagnosticItem(params, input);
130280
- }
130281
- throw AssessmentRuntimeError.from(assessmentFlowViolation("routingNodeKey is not accepted by the hosted conventional item-submit flow.", { routingNodeKey: input.routingNodeKey }));
130282
- }
130283
- let preview = await this.prepareItemSubmission(params, input);
130284
- let preparationAttempts = 1;
130285
- while (true) {
130286
- const committed = await this.deps.assessmentRuntimeLock(this.deps.db, [assessmentRuntimeAttemptLockKey(params.attemptId)], async (db2) => {
130287
- const { attempt, enrollment } = await this.resolveAttemptAuthorization(params, {
130288
- db: db2,
130289
- developerAccessValidated: true
130290
- });
130291
- if (this.isPlatformRoutedDiagnosticMetadata(attempt.metadata)) {
130292
- this.requireActiveAttempt({ attempt, enrollment }, params.attemptId);
130293
- throw AssessmentRuntimeError.from(assessmentFlowViolation("Platform-routed diagnostic submissions require routingNodeKey.", { flow: "platform-routed-item-submit" }));
130294
- }
130295
- const replay = resolveAssessmentItemReplay(attempt.metadata, input);
130296
- if (replay.action === "conflict") {
130297
- this.requireActiveAttempt({ attempt, enrollment }, params.attemptId);
130298
- throw AssessmentRuntimeError.from(replay.failure);
130299
- }
130300
- if (replay.action === "replay") {
130301
- const prior = replay.submission;
130302
- const child2 = attempt.metadata.purpose === "review" ? await this.prepareReviewAssessmentChildResult({
130303
- attempt,
130304
- metadata: attempt.metadata,
130305
- submission: prior
130306
- }) : null;
130307
- return {
130308
- action: "complete",
130309
- response: buildAssessmentItemReplayResult(attempt.result.sourcedId, attempt.metadata, prior),
130310
- checkpoint: child2,
130311
- projection: attempt.metadata.purpose === "review" ? {
130312
- result: attempt.result,
130313
- metadata: attempt.metadata,
130314
- itemIdentifier: input.itemIdentifier,
130315
- integration: attempt.integration
130316
- } : null
130317
- };
130318
- }
130319
- this.requireActiveAttempt({ attempt, enrollment }, params.attemptId);
130320
- this.assertInProgress(attempt.result);
130321
- if (!preview || preview.responseVersion !== attempt.metadata.responseVersion || preview.submissionId !== input.submissionId || preview.itemIdentifier !== input.itemIdentifier) {
130322
- if (preparationAttempts >= TimebackAssessmentRuntimeService2.ITEM_SUBMISSION_MAX_PREPARATION_ATTEMPTS) {
130323
- throw assessmentPreparationContentionError(input.expectedResponseVersion, attempt.metadata.responseVersion, "assessment");
130324
- }
130325
- return { action: "prepare" };
130326
- }
130327
- const transition = prepareAssessmentItemSubmission({
130328
- purpose: attempt.metadata.purpose,
130329
- responseVersion: attempt.metadata.responseVersion,
130330
- responses: attempt.metadata.responses,
130331
- itemSubmissions: attempt.metadata.itemSubmissions,
130332
- assessment: preview.assessment
130333
- }, input);
130334
- if (transition.action === "reject") {
130335
- throw AssessmentRuntimeError.from(transition.failure);
130336
- }
130337
- if (transition.action === "replay") {
130338
- const child2 = attempt.metadata.purpose === "review" ? await this.prepareReviewAssessmentChildResult({
130339
- attempt,
130340
- metadata: attempt.metadata,
130341
- submission: transition.submission
130342
- }) : null;
130343
- return {
130344
- action: "complete",
130345
- response: {
130346
- attemptId: attempt.result.sourcedId,
130347
- responseVersion: transition.responseVersion,
130348
- status: "in_progress",
130349
- responses: transition.responses,
130350
- itemSubmissions: attempt.metadata.itemSubmissions,
130351
- submission: transition.submission
130352
- },
130353
- checkpoint: child2,
130354
- projection: attempt.metadata.purpose === "review" ? {
130355
- result: attempt.result,
130356
- metadata: attempt.metadata,
130357
- itemIdentifier: input.itemIdentifier,
130358
- integration: attempt.integration
130359
- } : null
130360
- };
130361
- }
130362
- if (!preview.scoring) {
130363
- throw new Error("Prepared item submission is missing its score");
130364
- }
130365
- const submittedAt = new Date().toISOString();
130366
- const completed = completeAssessmentItemSubmission(attempt.metadata, input, transition, preview.scoring, submittedAt);
130367
- const metadata2 = {
130368
- ...attempt.metadata,
130369
- responses: completed.responses,
130370
- responseVersion: completed.responseVersion,
130371
- itemSubmissions: completed.itemSubmissions,
130372
- updatedAt: submittedAt
130373
- };
130374
- const child = metadata2.purpose === "review" ? await this.prepareReviewAssessmentChildResult({
130375
- attempt,
130376
- metadata: metadata2,
130377
- submission: completed.submission
130378
- }) : null;
130379
- await this.putResultMetadata(attempt.result, metadata2);
130380
- return {
130381
- action: "complete",
130382
- response: {
130383
- attemptId: attempt.result.sourcedId,
130384
- responseVersion: completed.responseVersion,
130385
- status: "in_progress",
130386
- responses: completed.responses,
130387
- itemSubmissions: completed.itemSubmissions,
130388
- submission: completed.submission
130389
- },
130390
- checkpoint: child,
130391
- projection: metadata2.purpose === "review" ? {
130392
- result: attempt.result,
130393
- metadata: metadata2,
130394
- itemIdentifier: input.itemIdentifier,
130395
- integration: attempt.integration
130396
- } : null
130397
- };
130398
- });
130399
- if (committed.action === "prepare") {
130400
- preparationAttempts += 1;
130401
- preview = await this.scoreItemSubmission(params, input);
130402
- } else {
130403
- if (committed.projection) {
130404
- this.projectReviewItemResponse(params, committed.projection, committed.checkpoint);
130405
- }
130406
- return committed.response;
130407
- }
130408
- }
130409
- }
130410
- async submitDiagnosticItem(params, input) {
130411
- let preview = await this.prepareDiagnosticItemSubmission(params, input);
130412
- let preparationAttempts = 1;
130413
- while (true) {
130414
- const committed = await this.deps.assessmentRuntimeLock(this.deps.db, [assessmentRuntimeAttemptLockKey(params.attemptId)], async (db2) => {
130415
- const { attempt, enrollment } = await this.resolveAttemptAuthorization(params, { db: db2, developerAccessValidated: true });
130416
- if (!this.isPlatformRoutedDiagnosticMetadata(attempt.metadata)) {
130417
- this.requireActiveAttempt({ attempt, enrollment }, params.attemptId);
130418
- throw AssessmentRuntimeError.from(assessmentFlowViolation("routingNodeKey is accepted only for a platform-routed diagnostic.", { routingNodeKey: input.routingNodeKey }));
130419
- }
130420
- const metadata2 = attempt.metadata;
130421
- const replay = resolveDiagnosticItemReplay(metadata2, input);
130422
- if (replay.action === "conflict") {
130423
- this.requireActiveAttempt({ attempt, enrollment }, params.attemptId);
130424
- throw AssessmentRuntimeError.from(replay.failure);
130425
- }
130426
- if (replay.action === "replay") {
130427
- const { submission: prior, ledgerEntry, priorIndex } = replay;
130428
- let routing = replay.routing;
130429
- if (!routing) {
130430
- const assessment = preview?.assessment ?? await this.loadAttemptAssessment(metadata2);
130431
- const loaded2 = await this.loadAttemptDiagnosticRouting(metadata2, assessment, db2);
130432
- const replayed = replayDiagnosticRouting(loaded2.manifest, metadata2.diagnostic.ledger.slice(0, priorIndex + 1));
130433
- if (!replayed.ok) {
130434
- throw AssessmentRuntimeError.from(assessmentFlowViolation("The committed diagnostic routing state cannot be replayed.", { failure: replayed.failure }));
130435
- }
130436
- routing = this.diagnosticRoutingSnapshot(metadata2, replayed.state);
130437
- }
130438
- const child2 = await this.prepareDiagnosticAssessmentChildResult({
130439
- attempt,
130440
- metadata: metadata2,
130441
- submission: prior,
130442
- transition: ledgerEntry
130443
- });
130444
- return {
130445
- action: "complete",
130446
- response: buildDiagnosticItemReplayResult(attempt.result.sourcedId, prior, ledgerEntry, routing),
130447
- checkpoint: child2,
130448
- projection: {
130449
- result: attempt.result,
130450
- metadata: metadata2,
130451
- ledgerEntry,
130452
- integration: attempt.integration
130453
- }
130454
- };
130455
- }
130456
- this.requireActiveAttempt({ attempt, enrollment }, params.attemptId);
130457
- this.assertInProgress(attempt.result);
130458
- if (!preview || preview.responseVersion !== metadata2.responseVersion || preview.submissionId !== input.submissionId || preview.routingNodeKey !== input.routingNodeKey || preview.itemIdentifier !== input.itemIdentifier) {
130459
- if (preparationAttempts >= TimebackAssessmentRuntimeService2.ITEM_SUBMISSION_MAX_PREPARATION_ATTEMPTS) {
130460
- throw assessmentPreparationContentionError(input.expectedResponseVersion, metadata2.responseVersion, "diagnostic");
130461
- }
130462
- return { action: "prepare" };
130463
- }
130464
- this.assertResponseVersion(metadata2, input.expectedResponseVersion);
130465
- const loaded = await this.loadAttemptDiagnosticRouting(metadata2, preview.assessment, db2);
130466
- const expected = loaded.state.next;
130467
- if (loaded.state.status !== "in-progress" || !expected || expected.nodeKey !== input.routingNodeKey || expected.itemIdentifier !== input.itemIdentifier) {
130468
- throw AssessmentRuntimeError.from(assessmentFlowViolation(expected ? "Submitted diagnostic item is not the canonical next item." : "Diagnostic routing is already ready to complete.", {
130469
- expectedRoutingNodeKey: expected?.nodeKey ?? null,
130470
- routingNodeKey: input.routingNodeKey,
130471
- expectedItemIdentifier: expected?.itemIdentifier ?? null,
130472
- itemIdentifier: input.itemIdentifier
130473
- }));
130474
- }
130475
- const responses = prepareDiagnosticAssessmentResponses(preview.assessment, metadata2.responses, input);
130476
- const scoring = preview.scoring;
130477
- if (!scoring || typeof scoring.isCorrect !== "boolean") {
130478
- throw AssessmentRuntimeError.from(assessmentFlowViolation(`Diagnostic item ${input.itemIdentifier} did not produce determinate binary grading.`, { itemIdentifier: input.itemIdentifier }));
130479
- }
130480
- const advanced = advanceDiagnosticRouting(loaded.manifest, metadata2.diagnostic.ledger, {
130481
- routingNodeKey: input.routingNodeKey,
130482
- itemIdentifier: input.itemIdentifier,
130483
- isCorrect: scoring.isCorrect
130484
- });
130485
- if (!advanced.ok) {
130486
- throw AssessmentRuntimeError.from(assessmentFlowViolation("The diagnostic routing transition was rejected.", { failure: advanced.failure }));
130487
- }
130488
- const submittedAt = new Date().toISOString();
130489
- const responseVersion = metadata2.responseVersion + 1;
130490
- const routingSnapshot = this.diagnosticRoutingSnapshot(metadata2, advanced.state);
130491
- const routingSnapshots = metadata2.diagnostic.routingSnapshots ? [...metadata2.diagnostic.routingSnapshots, routingSnapshot] : undefined;
130492
- const submission = {
130493
- submissionId: input.submissionId,
130494
- itemIdentifier: input.itemIdentifier,
130495
- submittedAt,
130496
- responseVersion,
130497
- answered: Object.keys(responses[input.itemIdentifier] ?? {}).length > 0,
130498
- score: scoring.score,
130499
- isCorrect: scoring.isCorrect,
130500
- grading: scoring.grading
130501
- };
130502
- const nextMetadata = {
130503
- ...metadata2,
130504
- responses,
130505
- responseVersion,
130506
- itemSubmissions: [...metadata2.itemSubmissions, submission],
130507
- updatedAt: submittedAt,
130508
- diagnostic: {
130509
- ...metadata2.diagnostic,
130510
- ledger: [...metadata2.diagnostic.ledger, advanced.ledgerEntry],
130511
- ...routingSnapshots ? { routingSnapshots } : {},
130512
- state: advanced.state
130513
- }
130514
- };
130515
- const child = await this.prepareDiagnosticAssessmentChildResult({
130516
- attempt,
130517
- metadata: nextMetadata,
130518
- submission,
130519
- transition: advanced.ledgerEntry
130520
- });
130521
- await this.putResultMetadata(attempt.result, nextMetadata);
130522
- return {
130523
- action: "complete",
130524
- response: {
130525
- attemptId: attempt.result.sourcedId,
130526
- responseVersion,
130527
- status: "in_progress",
130528
- routing: routingSnapshot,
130529
- submission: diagnosticReceipt(submission, advanced.ledgerEntry)
130530
- },
130531
- checkpoint: child,
130532
- projection: {
130533
- result: attempt.result,
130534
- metadata: nextMetadata,
130535
- ledgerEntry: advanced.ledgerEntry,
130536
- integration: attempt.integration
130537
- }
130538
- };
130539
- });
130540
- if (committed.action === "prepare") {
130541
- preparationAttempts += 1;
130542
- preview = await this.scoreDiagnosticItemSubmission(params, input);
130543
- } else {
130544
- this.projectDiagnosticItemResponse(committed.projection, preview?.assessment, committed.checkpoint);
130545
- return committed.response;
130546
- }
130547
- }
130353
+ const { context: context2, transcript, submissionId } = prepareAssessmentProgress(await this.deps.verifyAssessmentToken(input.assessmentToken), params, input);
130354
+ const graded = await this.gradeTranscript(context2, transcript);
130355
+ await this.reconcileTranscriptChildren(context2, graded, new Set([submissionId]));
130356
+ return { attemptId: params.attemptId };
130548
130357
  }
130549
- async prepareDiagnosticItemSubmission(params, input) {
130358
+ async submitItem({
130359
+ input,
130360
+ ...params
130361
+ }) {
130362
+ await this.deps.validateDeveloperAccess(params.user, params.gameId);
130363
+ const context2 = requireAssessmentFeedbackContext(await this.deps.verifyAssessmentToken(input.assessmentToken), params);
130364
+ const transcript = parseAssessmentTranscript(input.transcript);
130365
+ const assessment = await this.loadAttemptAssessment(context2.metadata);
130366
+ const appended = prepareAssessmentItemTranscript(assessment, { ...input, transcript });
130367
+ const graded = await this.gradeTranscript(context2, appended.transcript);
130368
+ const submission = graded.metadata.itemSubmissions[appended.index];
130369
+ this.reconcileTranscriptChildren(context2, graded, new Set([submission.submissionId])).catch((error88) => this.recordTranscriptFailure("child_projection", params.attemptId, error88));
130370
+ return buildAssessmentTranscriptFeedback(params.attemptId, graded, appended.index);
130371
+ }
130372
+ async gradeTranscript(context2, transcript, terminal = false, db2 = this.deps.db) {
130373
+ const startedAt = Date.now();
130550
130374
  try {
130551
- return await this.scoreDiagnosticItemSubmission(params, input);
130552
- } catch (error88) {
130553
- addEvent("assessment.diagnostic_item_submission_preview_failed", {
130554
- "app.assessment.attempt_id": params.attemptId,
130555
- "exception.type": errorType(error88),
130556
- "app.error.message": errorMessage2(error88)
130557
- });
130558
- return null;
130559
- }
130560
- }
130561
- async scoreDiagnosticItemSubmission(params, input) {
130562
- const attempt = await this.peekAttempt(params);
130563
- if (!this.isPlatformRoutedDiagnosticMetadata(attempt.metadata)) {
130564
- return null;
130565
- }
130566
- const prior = attempt.metadata.itemSubmissions.find((submission) => submission.submissionId === input.submissionId);
130567
- if (prior || !this.isOpen(attempt.result)) {
130568
- return null;
130569
- }
130570
- const assessment = await this.loadAttemptAssessment(attempt.metadata);
130571
- const artifactIdentity = assessmentArtifactIdentityFromAttempt(attempt.metadata);
130572
- const scoringArtifact = await this.artifactLoader.loadMatchedScoring(assessment, artifactIdentity);
130573
- const loaded = await this.loadAttemptDiagnosticRouting(attempt.metadata, assessment, this.deps.db, scoringArtifact);
130574
- const expected = loaded.state.next;
130575
- let scoring = null;
130576
- if (loaded.state.status === "in-progress" && expected?.nodeKey === input.routingNodeKey && expected.itemIdentifier === input.itemIdentifier && attempt.metadata.responseVersion === input.expectedResponseVersion) {
130577
- const responses = prepareDiagnosticAssessmentResponses(assessment, attempt.metadata.responses, input);
130578
- scoring = await this.scoreItem({
130375
+ const assessment = await this.loadAttemptAssessment(context2.metadata);
130376
+ const prepared = prepareAttemptAssessmentTranscript(context2, assessment, transcript, terminal);
130377
+ const artifactIdentity = assessmentArtifactIdentityFromAttempt(context2.metadata);
130378
+ const artifact = await this.artifactLoader.loadMatchedScoring(assessment, artifactIdentity);
130379
+ const [itemResults, routing] = await Promise.all([
130380
+ scoreCachedAssessmentItems({
130381
+ assessment,
130382
+ responses: prepared.responses,
130383
+ artifactIdentity,
130384
+ purpose: context2.metadata.purpose,
130385
+ itemIdentifiers: new Set(prepared.transcript.administrations.map((entry2) => entry2.itemIdentifier)),
130386
+ artifact,
130387
+ cache: this.itemScoreCache,
130388
+ processResponse: (identifier, request) => this.requireClient().qtiApi.assessmentItems.processResponse(identifier, request),
130389
+ onEvent: addEvent
130390
+ }),
130391
+ this.isPlatformRoutedDiagnosticMetadata(context2.metadata) ? this.loadAttemptDiagnosticRouting(context2.metadata, assessment, db2, artifact) : undefined
130392
+ ]);
130393
+ const graded = buildGradedAssessmentTranscript({
130394
+ context: context2,
130579
130395
  assessment,
130580
- responses,
130581
- itemIdentifier: input.itemIdentifier,
130582
- artifactIdentity,
130583
- purpose: attempt.metadata.purpose,
130584
- preparedArtifact: scoringArtifact
130396
+ prepared,
130397
+ itemResults,
130398
+ submittedAt: new Date().toISOString(),
130399
+ manifest: routing?.manifest,
130400
+ terminal
130401
+ });
130402
+ addEvent("assessment.transcript_validated", {
130403
+ "app.assessment.attempt_id": context2.attemptId,
130404
+ "app.assessment.administration_count": graded.metadata.responseVersion,
130405
+ "app.assessment.duration_ms": Date.now() - startedAt
130585
130406
  });
130586
- }
130587
- return {
130588
- responseVersion: attempt.metadata.responseVersion,
130589
- submissionId: input.submissionId,
130590
- routingNodeKey: input.routingNodeKey,
130591
- itemIdentifier: input.itemIdentifier,
130592
- assessment,
130593
- scoring
130594
- };
130595
- }
130596
- async scoreItems({
130597
- preparedArtifact,
130598
- ...input
130599
- }) {
130600
- const artifact = preparedArtifact === undefined ? await this.artifactLoader.loadMatchedScoring(input.assessment, input.artifactIdentity) : preparedArtifact;
130601
- return scoreAssessmentItems({
130602
- ...input,
130603
- artifact,
130604
- processResponse: (identifier, request) => this.requireClient().qtiApi.assessmentItems.processResponse(identifier, request),
130605
- onEvent: addEvent
130606
- });
130607
- }
130608
- async scoreItem(input) {
130609
- const [scoring] = await this.scoreItems({
130610
- assessment: input.assessment,
130611
- responses: input.responses,
130612
- artifactIdentity: input.artifactIdentity,
130613
- purpose: input.purpose,
130614
- itemIdentifiers: new Set([input.itemIdentifier]),
130615
- preparedArtifact: input.preparedArtifact
130616
- });
130617
- return scoring;
130618
- }
130619
- async prepareItemSubmission(params, input) {
130620
- try {
130621
- return await this.scoreItemSubmission(params, input);
130407
+ return graded;
130622
130408
  } catch (error88) {
130623
- addEvent("assessment.item_submission_preview_failed", {
130624
- "app.assessment.attempt_id": params.attemptId,
130625
- "exception.type": errorType(error88),
130626
- "app.error.message": errorMessage2(error88)
130627
- });
130628
- return null;
130629
- }
130630
- }
130631
- async scoreItemSubmission(params, input) {
130632
- const attempt = await this.peekAttempt(params);
130633
- const prior = attempt.metadata.itemSubmissions.find((submission) => submission.submissionId === input.submissionId);
130634
- if (prior || !this.isOpen(attempt.result)) {
130635
- return null;
130409
+ throw assessmentTranscriptRuntimeError(error88);
130636
130410
  }
130637
- const assessment = await this.loadAttemptAssessment(attempt.metadata);
130638
- const transition = prepareAssessmentItemSubmission({
130639
- purpose: attempt.metadata.purpose,
130640
- responseVersion: attempt.metadata.responseVersion,
130641
- responses: attempt.metadata.responses,
130642
- itemSubmissions: attempt.metadata.itemSubmissions,
130643
- assessment
130644
- }, input);
130645
- const scoring = transition.action === "commit" ? await this.scoreItem({
130646
- assessment,
130647
- responses: transition.responses,
130648
- itemIdentifier: input.itemIdentifier,
130649
- artifactIdentity: assessmentArtifactIdentityFromAttempt(attempt.metadata),
130650
- purpose: attempt.metadata.purpose
130651
- }) : null;
130652
- return {
130653
- responseVersion: attempt.metadata.responseVersion,
130654
- submissionId: input.submissionId,
130655
- itemIdentifier: input.itemIdentifier,
130656
- assessment,
130657
- scoring
130658
- };
130659
130411
  }
130660
- async scoreSubmission(attempt) {
130661
- const assessment = await this.loadAttemptAssessment(attempt.metadata);
130662
- validateAssessmentResponses(assessment, attempt.metadata.responses);
130663
- const itemResults = attempt.metadata.purpose === "review" ? committedReviewItemResults(attempt.metadata, assessment) : await this.scoreItems({
130664
- assessment,
130665
- responses: attempt.metadata.responses,
130666
- artifactIdentity: assessmentArtifactIdentityFromAttempt(attempt.metadata),
130667
- purpose: attempt.metadata.purpose
130668
- });
130669
- return {
130670
- responseVersion: attempt.metadata.responseVersion,
130671
- assessment,
130672
- itemResults,
130673
- score: aggregateAssessmentScore(itemResults.map((result) => result.score.earned), itemResults.reduce((total, result) => total + result.score.possible, 0)),
130674
- correctQuestions: countCorrectQuestions(itemResults.map((result) => result.isCorrect))
130675
- };
130676
- }
130677
- async peekAttempt({
130678
- studentId,
130679
- attemptId
130680
- }) {
130681
- const result = await this.requireClient().api.oneroster.assessmentResults.get(attemptId).catch((error88) => {
130682
- if (isApiError(error88) && error88.statusCode === 404) {
130683
- throw this.unauthorizedAttempt(attemptId);
130684
- }
130685
- throw error88;
130412
+ recordTranscriptFailure(operation, attemptId, error88) {
130413
+ addEvent("assessment.transcript_operation_failed", {
130414
+ "app.assessment.operation": operation,
130415
+ "app.assessment.attempt_id": attemptId,
130416
+ "exception.type": errorType(error88),
130417
+ "app.error.message": errorMessage2(error88)
130686
130418
  });
130687
- const metadata2 = playcademyAssessmentResultMetadata(result.metadata);
130688
- if (!metadata2 || result.student.sourcedId !== studentId || result.status !== ONEROSTER_STATUS2.active) {
130689
- throw this.unauthorizedAttempt(attemptId);
130690
- }
130691
- return { result, metadata: metadata2 };
130692
- }
130693
- async prepareSubmission(params, input) {
130694
- try {
130695
- const attempt = await this.peekAttempt(params);
130696
- const disposition = classifyAssessmentSubmission({
130697
- inProgress: attempt.result.inProgress ?? "",
130698
- scoreStatus: attempt.result.scoreStatus,
130699
- submissionId: attempt.metadata.submissionId
130700
- }, input.submissionId);
130701
- if (disposition !== "submit") {
130702
- return null;
130703
- }
130704
- if (this.isPlatformRoutedDiagnosticMetadata(attempt.metadata)) {
130705
- return null;
130706
- }
130707
- return await this.scoreSubmission(attempt);
130708
- } catch (error88) {
130709
- addEvent("assessment.submission_preview_failed", {
130710
- "app.assessment.attempt_id": params.attemptId,
130711
- "exception.type": errorType(error88),
130712
- "app.error.message": errorMessage2(error88)
130713
- });
130714
- return null;
130715
- }
130716
130419
  }
130717
130420
  async submit({
130718
130421
  input,
@@ -130720,121 +130423,83 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
130720
130423
  ...params
130721
130424
  }) {
130722
130425
  await this.deps.validateDeveloperAccess(params.user, params.gameId);
130723
- const preview = await this.prepareSubmission(params, input);
130724
- const submission = await this.deps.assessmentRuntimeLock(this.deps.db, [assessmentRuntimeAttemptLockKey(params.attemptId)], async (db2) => {
130725
- const { attempt: identity, enrollment } = await this.resolveAttemptAuthorization(params, { db: db2, developerAccessValidated: true });
130726
- const submissionDisposition = classifyAssessmentSubmission({
130727
- inProgress: identity.result.inProgress ?? "",
130728
- scoreStatus: identity.result.scoreStatus,
130729
- submissionId: identity.metadata.submissionId
130730
- }, input.submissionId);
130731
- if (submissionDisposition === "replay") {
130426
+ const transcript = parseAssessmentTranscript(input.transcript);
130427
+ const request = { ...input, transcript };
130428
+ const requestHash = await assessmentTranscriptRequestHash(request);
130429
+ const accepted = await this.deps.assessmentRuntimeLock(this.deps.db, [assessmentRuntimeAttemptLockKey(params.attemptId)], async (db2) => {
130430
+ const { attempt } = await this.resolveAttemptAuthorization(params, {
130431
+ db: db2,
130432
+ developerAccessValidated: true,
130433
+ includeEnrollment: false
130434
+ });
130435
+ const disposition = resolveAssessmentTranscriptSubmission(attempt.result, attempt.metadata, attempt.integration.status, request, requestHash);
130436
+ if (disposition === "replay") {
130732
130437
  return {
130733
- response: this.submittedResult(identity.result, identity.metadata),
130734
- emission: this.replayCompletion(identity, input.submissionId, params, game2)
130438
+ response: this.submittedResult(attempt.result, attempt.metadata),
130439
+ emission: this.replayCompletion(attempt, input.submissionId, params, game2)
130735
130440
  };
130736
130441
  }
130737
- const attempt = this.requireActiveAttempt({ attempt: identity, enrollment }, params.attemptId);
130738
- if (submissionDisposition === "reject") {
130739
- throw AssessmentRuntimeError.from(assessmentAlreadySubmitted(attempt.result.sourcedId));
130740
- }
130741
- if (input.session && input.session.runId !== attempt.result.sourcedId) {
130742
- throw new ValidationError("Assessment session runId must match the attemptId.");
130743
- }
130744
- this.assertResponseVersion(attempt.metadata, input.expectedResponseVersion);
130745
- if (this.isPlatformRoutedDiagnosticMetadata(attempt.metadata)) {
130746
- return this.finalizeDiagnosticSubmission({
130747
- attempt: { ...attempt, metadata: attempt.metadata },
130748
- input,
130749
- params,
130750
- game: game2,
130751
- db: db2
130752
- });
130753
- }
130754
- if (assessmentFlowForPurpose(attempt.metadata.purpose) === "item-submit") {
130755
- const assessment2 = await this.loadAttemptAssessment(attempt.metadata);
130756
- const flowFailure = assessmentCompletionFlowFailure(attempt.metadata.purpose, attempt.metadata.itemSubmissions, assessment2.items.length);
130757
- if (flowFailure) {
130758
- throw AssessmentRuntimeError.from(flowFailure);
130759
- }
130760
- }
130761
- const scored = preview && preview.responseVersion === attempt.metadata.responseVersion ? preview : await this.scoreSubmission(attempt);
130762
- const { assessment, itemResults, score, correctQuestions } = scored;
130763
- const client2 = this.requireClient();
130442
+ const graded = await this.gradeTranscript({
130443
+ attemptId: params.attemptId,
130444
+ metadata: attempt.metadata
130445
+ }, transcript, true, db2);
130446
+ const diagnostic = this.isPlatformRoutedDiagnosticMetadata(graded.metadata) ? summarizeDiagnosticSubmission(graded.metadata, graded.manifest) : null;
130447
+ const itemOutcomes = graded.metadata.purpose === "review" ? assessmentReviewTranscriptOutcomes({
130448
+ metadata: graded.metadata,
130449
+ itemResults: graded.itemResults
130450
+ }) : undefined;
130451
+ const courseName = attempt.metadata.courseName ?? "Game Course";
130764
130452
  const timestamp6 = new Date().toISOString();
130765
- const reviewOutcomes = attempt.metadata.purpose === "review" ? this.reviewItemOutcomes({
130766
- metadata: attempt.metadata,
130767
- itemResults
130768
- }) : null;
130769
130453
  const finalization = buildAssessmentResultSubmission({
130770
130454
  result: attempt.result,
130771
- metadata: attempt.metadata,
130772
- score,
130455
+ metadata: graded.metadata,
130456
+ score: graded.score,
130773
130457
  submissionId: input.submissionId,
130774
130458
  timestamp: timestamp6,
130775
- testName: assessment.title,
130776
- courseName: attempt.enrollment.course.title,
130777
- totalQuestions: itemResults.length,
130778
- correctQuestions,
130459
+ testName: graded.assessment.title,
130460
+ courseName,
130461
+ totalQuestions: graded.itemResults.length,
130462
+ correctQuestions: graded.correctQuestions,
130779
130463
  ...input.session ? { session: input.session } : {},
130780
- ...reviewOutcomes ? { itemOutcomes: reviewOutcomes } : {}
130464
+ ...itemOutcomes ? { itemOutcomes } : {},
130465
+ ...diagnostic ? { diagnosticTracks: diagnostic.tracks } : {}
130781
130466
  });
130782
- const metadata2 = finalization.metadata;
130783
- const submitted = await client2.api.oneroster.assessmentResults.upsert(attempt.result.sourcedId, finalization.resultUpdate);
130784
- const submittedForEmission = {
130785
- ...submitted,
130786
- score: score.normalized * 100,
130787
- scoreDate: timestamp6,
130788
- ...ASSESSMENT_ATTEMPT_AWAITING_AWARD,
130789
- metadata: finalization.resultUpdate.metadata
130467
+ const metadata2 = {
130468
+ ...finalization.metadata,
130469
+ completion: { ...finalization.metadata.completion, requestHash }
130470
+ };
130471
+ const resultUpdate = {
130472
+ ...finalization.resultUpdate,
130473
+ metadata: mergeAssessmentRuntimeMetadata(attempt.result.metadata, metadata2)
130474
+ };
130475
+ await this.requireClient().api.oneroster.assessmentResults.upsert(params.attemptId, resultUpdate);
130476
+ const result = { ...attempt.result, ...resultUpdate };
130477
+ const emission = {
130478
+ attempt: { ...attempt, result, metadata: metadata2 },
130479
+ testIdentifier: graded.assessment.identifier,
130480
+ testName: graded.assessment.title,
130481
+ score: graded.score,
130482
+ totalQuestions: graded.itemResults.length,
130483
+ correctQuestions: graded.correctQuestions,
130484
+ submissionId: input.submissionId,
130485
+ gameId: params.gameId,
130486
+ studentId: params.studentId,
130487
+ courseName,
130488
+ game: game2
130790
130489
  };
130791
- addEvent("assessment.awaiting_award_created", {
130792
- "app.assessment.attempt_id": submittedForEmission.sourcedId,
130793
- "app.assessment.submission_id": input.submissionId,
130794
- "app.assessment.purpose": metadata2.purpose,
130795
- "app.assessment.response_version": metadata2.responseVersion
130796
- });
130797
130490
  return {
130798
- response: {
130799
- attemptId: submittedForEmission.sourcedId,
130800
- submissionId: input.submissionId,
130801
- responseVersion: metadata2.responseVersion,
130802
- status: "awaiting_award",
130803
- responses: metadata2.responses,
130804
- score,
130805
- ...metadata2.purpose === "review" ? {
130806
- purpose: metadata2.purpose,
130807
- itemOutcomes: reviewOutcomes
130808
- } : { purpose: metadata2.purpose }
130809
- },
130810
- emission: {
130811
- attempt: { ...attempt, result: submittedForEmission, metadata: metadata2 },
130812
- testIdentifier: assessment.identifier,
130813
- testName: assessment.title,
130814
- score,
130815
- totalQuestions: itemResults.length,
130816
- correctQuestions,
130817
- submissionId: input.submissionId,
130818
- gameId: params.gameId,
130819
- studentId: params.studentId,
130820
- courseName: attempt.enrollment.course.title,
130821
- game: game2
130822
- }
130491
+ response: this.submittedResult(result, metadata2),
130492
+ emission,
130493
+ repair: { context: this.tokenContext(attempt), graded }
130823
130494
  };
130824
130495
  });
130825
- if (submission.emission) {
130826
- const diagnosticCompletion = this.isPlatformRoutedDiagnosticMetadata(submission.emission.attempt.metadata) ? {
130827
- ...submission.emission.attempt,
130828
- metadata: submission.emission.attempt.metadata
130829
- } : null;
130830
- const { result, metadata: metadata2 } = submission.emission.attempt;
130831
- await Promise.allSettled([
130832
- this.emitSessionBestEffort(submission.emission),
130833
- ...diagnosticCompletion ? [this.restoreDiagnosticChildResults(diagnosticCompletion)] : [],
130834
- ...metadata2.purpose === "review" ? [this.restoreAllCompletedReviewChildResponses(result, metadata2)] : []
130835
- ]);
130496
+ if (accepted.repair) {
130497
+ this.reconcileTranscriptChildren(accepted.repair.context, accepted.repair.graded).catch((error88) => this.recordTranscriptFailure("completion_repair", params.attemptId, error88));
130498
+ }
130499
+ if (accepted.emission) {
130500
+ this.emitSessionBestEffort(accepted.emission);
130836
130501
  }
130837
- return submission.response;
130502
+ return accepted.response;
130838
130503
  }
130839
130504
  async finalize({
130840
130505
  input,
@@ -130978,58 +130643,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
130978
130643
  });
130979
130644
  }
130980
130645
  }
130981
- async finalizeDiagnosticSubmission(input) {
130982
- const { attempt } = input;
130983
- const routing = await this.loadAttemptDiagnosticManifest(attempt.metadata, input.db);
130984
- const { tracks, score, correctQuestions } = summarizeDiagnosticSubmission(attempt.metadata, routing.manifest);
130985
- const timestamp6 = new Date().toISOString();
130986
- const finalization = buildAssessmentResultSubmission({
130987
- result: attempt.result,
130988
- metadata: attempt.metadata,
130989
- score,
130990
- submissionId: input.input.submissionId,
130991
- timestamp: timestamp6,
130992
- testName: attempt.metadata.diagnostic.testName,
130993
- courseName: attempt.enrollment.course.title,
130994
- totalQuestions: attempt.metadata.itemSubmissions.length,
130995
- correctQuestions,
130996
- ...input.input.session ? { session: input.input.session } : {},
130997
- diagnosticTracks: tracks
130998
- });
130999
- const metadata2 = finalization.metadata;
131000
- const submitted = await this.requireClient().api.oneroster.assessmentResults.upsert(attempt.result.sourcedId, finalization.resultUpdate);
131001
- const submittedForEmission = {
131002
- ...submitted,
131003
- score: score.normalized * 100,
131004
- scoreDate: timestamp6,
131005
- ...ASSESSMENT_ATTEMPT_AWAITING_AWARD,
131006
- metadata: finalization.resultUpdate.metadata
131007
- };
131008
- return {
131009
- response: {
131010
- purpose: "diagnostic",
131011
- attemptId: submittedForEmission.sourcedId,
131012
- submissionId: input.input.submissionId,
131013
- diagnosticKey: metadata2.diagnostic.diagnosticKey,
131014
- responseVersion: metadata2.responseVersion,
131015
- status: "awaiting_award",
131016
- tracks
131017
- },
131018
- emission: {
131019
- attempt: { ...attempt, result: submittedForEmission, metadata: metadata2 },
131020
- testIdentifier: attempt.metadata.selectedTest.identifier,
131021
- testName: attempt.metadata.diagnostic.testName,
131022
- score,
131023
- totalQuestions: attempt.metadata.itemSubmissions.length,
131024
- correctQuestions,
131025
- submissionId: input.input.submissionId,
131026
- gameId: input.params.gameId,
131027
- studentId: input.params.studentId,
131028
- courseName: attempt.enrollment.course.title,
131029
- game: input.game
131030
- }
131031
- };
131032
- }
131033
130646
  async exportFixture({
131034
130647
  gameId,
131035
130648
  user
@@ -131229,14 +130842,32 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
131229
130842
  async listAttempts(scope) {
131230
130843
  return this.attemptListing(await this.studentAssessmentResults(scope.studentId), scope);
131231
130844
  }
131232
- attemptListing(results, scope) {
130845
+ async attemptListing(results, scope) {
131233
130846
  const attempts = new Map;
131234
130847
  const reviewChildren = new Map;
131235
130848
  for (const result of results) {
131236
130849
  const metadata2 = playcademyAssessmentResultMetadata(result.metadata);
131237
- const childMetadata = scope.purpose === "review" ? playcademyAssessmentItemResultMetadata(result.metadata) : null;
131238
- if (childMetadata && childMetadata.activityId === scope.activityId && childMetadata.integrationId === scope.integrationId && childMetadata.enrollmentId === scope.enrollmentId && result.student.sourcedId === scope.studentId) {
131239
- reviewChildren.set(result.sourcedId, result);
130850
+ if (scope.purpose === "review") {
130851
+ const verified = await verifyAssessmentChildResultCommand(result.metadata);
130852
+ if (verified.ok && verified.command.payload.context.kind === "review") {
130853
+ const { command } = verified;
130854
+ const { attempt, administration } = command.payload;
130855
+ const parent = await reviewQuestionLineItemId(await deterministicUUID([
130856
+ "playcademy:review-bank-line-item:v1",
130857
+ scope.integrationId,
130858
+ scope.activityId,
130859
+ attempt.selectedTest.identifier,
130860
+ verified.command.payload.context.bankRevision
130861
+ ].join("\x00")), administration.itemIdentifier);
130862
+ const projection = await classifyAssessmentChildResult({
130863
+ result,
130864
+ command,
130865
+ childLineItemId: parent
130866
+ });
130867
+ 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") {
130868
+ reviewChildren.set(result.sourcedId, command);
130869
+ }
130870
+ }
131240
130871
  }
131241
130872
  if (metadata2 && metadata2.activityId === scope.activityId && metadata2.purpose === scope.purpose && metadata2.courseId === scope.courseId && metadata2.integrationId === scope.integrationId && result.student.sourcedId === scope.studentId) {
131242
130873
  const selectedAssessmentKey = metadata2.selectedTest.assessmentKey ?? assessmentKeyFromManagedQtiIdentifier(metadata2.selectedTest.identifier);
@@ -131287,19 +130918,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
131287
130918
  }
131288
130919
  return true;
131289
130920
  }
131290
- reviewExposures(results, scope) {
131291
- const exposures = [];
131292
- for (const result of results) {
131293
- const metadata2 = playcademyAssessmentItemResultMetadata(result.metadata);
131294
- if (metadata2?.bankRevision === scope.bankRevision) {
131295
- exposures.push({
131296
- itemIdentifier: metadata2.itemIdentifier,
131297
- deliveredAt: metadata2.deliveredAt
131298
- });
131299
- }
131300
- }
131301
- return exposures;
131302
- }
131303
130921
  requireDecision(decision, purpose) {
131304
130922
  if (!decision) {
131305
130923
  throw AssessmentRuntimeError.from(assessmentNoEligibleTests(purpose));
@@ -131316,29 +130934,9 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
131316
130934
  const resumed = attempts.get(decision.attempt.attemptId);
131317
130935
  return this.snapshotForResult(resumed.result, resumed.metadata, integration);
131318
130936
  }
131319
- async resumeDiagnosticSnapshot(decision, attempts, integration, db2 = this.deps.db) {
131320
- if (decision.additionalResumableAttemptIds.length > 0) {
131321
- addEvent("assessment.multiple_resumable_attempts", {
131322
- "app.assessment.attempt_id": decision.attempt.attemptId,
131323
- "app.assessment.additional_attempt_ids": decision.additionalResumableAttemptIds.join(",")
131324
- });
131325
- }
130937
+ async resumeDiagnosticSnapshot(decision, attempts, integration, _db = this.deps.db) {
131326
130938
  const resumed = attempts.get(decision.attempt.attemptId);
131327
- if (this.isSettled(resumed.result)) {
131328
- return this.snapshotForResult(resumed.result, resumed.metadata, integration);
131329
- }
131330
- if (!this.isPlatformRoutedDiagnosticMetadata(resumed.metadata)) {
131331
- throw new Error("A keyed diagnostic request selected a legacy fixed attempt");
131332
- }
131333
- const assessment = await this.loadAttemptAssessment(resumed.metadata);
131334
- const routing = await this.loadAttemptDiagnosticRouting(resumed.metadata, assessment, db2);
131335
- await this.restoreDiagnosticChildResults({
131336
- result: resumed.result,
131337
- metadata: resumed.metadata,
131338
- integration,
131339
- assessment
131340
- });
131341
- return this.diagnosticSnapshot(resumed.result, resumed.metadata, assessment, integration, routing.state);
130939
+ return this.snapshotForResult(resumed.result, resumed.metadata, integration);
131342
130940
  }
131343
130941
  async resumeAvailableAttempt(tests, attempts, requestFingerprint, pendingSupersessions, resume) {
131344
130942
  const select3 = () => selectRuntimeAssessment(tests, [...this.attemptsMatchingRequest(attempts, requestFingerprint).values()].map((entry2) => entry2.selection));
@@ -131409,42 +131007,9 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
131409
131007
  });
131410
131008
  }
131411
131009
  }
131412
- async resumeReviewSnapshot(decision, attempts, existingChildResults, context2) {
131413
- if (decision.additionalResumableAttemptIds.length > 0) {
131414
- addEvent("assessment.multiple_resumable_attempts", {
131415
- "app.assessment.attempt_id": decision.attempt.attemptId,
131416
- "app.assessment.additional_attempt_ids": decision.additionalResumableAttemptIds.join(",")
131417
- });
131418
- }
131010
+ async resumeReviewSnapshot(decision, attempts, context2) {
131419
131011
  const resumed = attempts.get(decision.attempt.attemptId);
131420
- if (resumed.metadata.purpose !== "review") {
131421
- throw new Error("A standards-review request selected a non-review attempt");
131422
- }
131423
- const reviewMetadata = resumed.metadata;
131424
- let assessment;
131425
- const current = await reconcileReviewChildResults({
131426
- initial: resumed,
131427
- writeOpen: async () => {
131428
- const source = await this.loadPinnedReviewBankSource(reviewMetadata);
131429
- assessment = projectReviewAssessment(source.assessment, source.bank, reviewMetadata.review.selections);
131430
- await this.ensureReviewChildResults({
131431
- result: resumed.result,
131432
- metadata: reviewMetadata,
131433
- integration: context2.integration,
131434
- source,
131435
- assessment,
131436
- existingChildResults,
131437
- lookupMissingResults: true
131438
- });
131439
- },
131440
- crossBarrier: () => crossAssessmentAttemptLockBarrier(this.deps.assessmentRuntimeLock, this.deps.db, resumed.result.sourcedId),
131441
- readLatest: () => this.peekAttempt({
131442
- studentId: resumed.result.student.sourcedId,
131443
- attemptId: resumed.result.sourcedId
131444
- }),
131445
- restoreTerminal: (latest) => this.restoreAllCompletedReviewChildResponses(latest.result, latest.metadata)
131446
- });
131447
- return assessmentChildProjectionIsTerminal(current.result) ? this.snapshotForResult(current.result, current.metadata, context2.integration) : this.snapshot(current.result, current.metadata, assessment, context2.integration);
131012
+ return this.snapshotForResult(resumed.result, resumed.metadata, context2.integration);
131448
131013
  }
131449
131014
  recordReviewPreparationPhase(phase, startedAt, counts = {}) {
131450
131015
  addEvent("assessment.review_preparation_phase", {
@@ -131852,135 +131417,82 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
131852
131417
  }
131853
131418
  return lineItemId;
131854
131419
  }
131855
- assessmentChildAttempt(attempt) {
131856
- const { metadata: metadata2 } = attempt;
131857
- const assessmentKey = metadata2.selectedTest.assessmentKey ?? assessmentKeyFromManagedQtiIdentifier(metadata2.selectedTest.identifier);
131858
- if (!assessmentKey) {
131859
- addEvent("assessment.child_result_legacy_attempt_skipped", {
131860
- "app.assessment.attempt_id": attempt.result.sourcedId
131861
- });
131862
- return null;
131863
- }
131420
+ tokenContext(attempt) {
131864
131421
  return {
131865
- attemptId: attempt.result.sourcedId,
131422
+ version: 1,
131866
131423
  gameId: attempt.integration.gameId,
131867
131424
  studentId: attempt.result.student.sourcedId,
131868
- activityId: metadata2.activityId,
131869
- courseId: metadata2.courseId,
131870
- integrationId: metadata2.integrationId,
131871
- enrollmentId: metadata2.enrollmentId,
131872
- selectedTest: {
131873
- assessmentKey,
131874
- identifier: metadata2.selectedTest.identifier,
131875
- contentRevision: metadata2.selectedTest.contentRevision
131876
- }
131877
- };
131878
- }
131879
- reviewChildResultContext(metadata2, submission) {
131880
- const selection = metadata2.review.selections.find((candidate) => candidate.itemIdentifier === submission.itemIdentifier);
131881
- if (!selection) {
131882
- throw new Error(`Committed review item ${submission.itemIdentifier} is not in its pinned selection`);
131883
- }
131884
- return {
131885
- kind: "review",
131886
- bankRevision: metadata2.review.bankRevision,
131887
- standard: selection.standard
131425
+ attemptId: attempt.result.sourcedId,
131426
+ parentLineItemId: attempt.result.assessmentLineItem.sourcedId,
131427
+ metadata: assessmentTokenMetadata(attempt.metadata)
131888
131428
  };
131889
131429
  }
131890
- prepareReviewAssessmentChildResult(input) {
131891
- return this.prepareAssessmentChildResult({
131892
- attempt: input.attempt,
131893
- metadata: input.metadata,
131894
- submission: input.submission,
131895
- context: () => this.reviewChildResultContext(input.metadata, input.submission)
131430
+ async ensureTranscriptLineItems(context2, assessment) {
131431
+ await runWithConcurrency(assessment.items, TimebackAssessmentRuntimeService2.SCORING_CONCURRENCY, async (item) => {
131432
+ const id = await assessmentTranscriptChildLineItemId(context2, item.identifier);
131433
+ if (this.childLineItemCache.get(id)) {
131434
+ return;
131435
+ }
131436
+ const metadata2 = context2.metadata;
131437
+ const common2 = {
131438
+ parentLineItemId: context2.parentLineItemId,
131439
+ integration: { courseId: metadata2.courseId },
131440
+ item
131441
+ };
131442
+ if (metadata2.purpose === "review") {
131443
+ const standard = metadata2.review.selections.find((selection) => selection.itemIdentifier === item.identifier)?.standard;
131444
+ if (!standard) {
131445
+ throw new Error("Review candidate is not in the pinned selection");
131446
+ }
131447
+ await this.ensureReviewQuestionLineItem({
131448
+ ...common2,
131449
+ bankRevision: metadata2.review.bankRevision,
131450
+ standards: [standard]
131451
+ });
131452
+ } else if (this.isPlatformRoutedDiagnosticMetadata(metadata2)) {
131453
+ await this.ensureDiagnosticQuestionLineItem({ ...common2, metadata: metadata2 });
131454
+ } else {
131455
+ const expected = {
131456
+ status: "active",
131457
+ parentAssessmentLineItem: { sourcedId: context2.parentLineItemId },
131458
+ course: { sourcedId: metadata2.courseId },
131459
+ resultValueMin: 0,
131460
+ resultValueMax: item.maxScore,
131461
+ metadata: {
131462
+ playcademyAssessmentQuestionDefinition: {
131463
+ version: 1,
131464
+ itemIdentifier: item.identifier,
131465
+ contentRevision: metadata2.selectedTest.contentRevision
131466
+ }
131467
+ }
131468
+ };
131469
+ const store = this.requireClient().api.oneroster.assessmentLineItems;
131470
+ try {
131471
+ await store.create({ sourcedId: id, title: item.title, ...expected });
131472
+ } catch (error88) {
131473
+ if (!isApiError(error88) || error88.statusCode !== 409) {
131474
+ throw error88;
131475
+ }
131476
+ this.assertAssessmentChildLineItem(id, await store.get(id), expected);
131477
+ }
131478
+ }
131479
+ this.childLineItemCache.set(id, true);
131896
131480
  });
131897
131481
  }
131898
- prepareDiagnosticAssessmentChildResult(input) {
131899
- return this.prepareAssessmentChildResult({
131900
- attempt: input.attempt,
131901
- metadata: input.metadata,
131902
- submission: input.submission,
131903
- context: () => ({
131904
- kind: "platform-routed-diagnostic",
131905
- definitionId: input.metadata.diagnostic.definitionId,
131906
- diagnosticKey: input.metadata.diagnostic.diagnosticKey,
131907
- routingRevision: input.metadata.diagnostic.routingRevision,
131908
- transition: input.transition
131482
+ async reconcileTranscriptChildren(context2, graded, only) {
131483
+ return reconcileAssessmentTranscriptChildren({
131484
+ context: context2,
131485
+ graded,
131486
+ only,
131487
+ assessmentResults: this.requireClient().api.oneroster.assessmentResults,
131488
+ ensureLineItems: (assessment) => this.ensureTranscriptLineItems(context2, assessment),
131489
+ onPersisted: (submissionId) => addEvent("assessment.child_result_persisted", {
131490
+ "app.assessment.attempt_id": context2.attemptId,
131491
+ "app.assessment.submission_id": submissionId,
131492
+ "app.assessment.child_result_outcome": "upserted"
131909
131493
  })
131910
131494
  });
131911
131495
  }
131912
- async prepareAssessmentChildResult(input) {
131913
- try {
131914
- const attemptContext = this.assessmentChildAttempt({
131915
- ...input.attempt,
131916
- metadata: input.metadata
131917
- });
131918
- if (!attemptContext) {
131919
- return null;
131920
- }
131921
- const parentLineItemId = input.attempt.result.assessmentLineItem.sourcedId;
131922
- const { itemIdentifier } = input.submission;
131923
- const context2 = input.context();
131924
- const [command, childLineItemId] = await Promise.all([
131925
- buildSubmissionChildResultCommand({
131926
- attempt: attemptContext,
131927
- submission: input.submission,
131928
- responses: input.metadata.responses[itemIdentifier],
131929
- grading: input.submission.grading ?? QTI_CHILD_GRADING,
131930
- context: context2
131931
- }),
131932
- context2.kind === "review" ? reviewQuestionLineItemId(parentLineItemId, itemIdentifier) : diagnosticQuestionLineItemId(parentLineItemId, itemIdentifier)
131933
- ]);
131934
- return { command, childLineItemId };
131935
- } catch (error88) {
131936
- addEvent("assessment.child_result_prepare_failed", {
131937
- "app.assessment.attempt_id": input.attempt.result.sourcedId,
131938
- "app.assessment.submission_id": input.submission.submissionId,
131939
- "app.assessment.qti_item_identifier": input.submission.itemIdentifier,
131940
- "exception.type": errorType(error88),
131941
- "app.error.message": errorMessage2(error88)
131942
- });
131943
- return null;
131944
- }
131945
- }
131946
- async projectPreparedAssessmentChildResult(prepared) {
131947
- if (!prepared) {
131948
- return;
131949
- }
131950
- const startedAt = Date.now();
131951
- const identity = {
131952
- "app.assessment.attempt_id": prepared.command.payload.attempt.attemptId,
131953
- "app.assessment.submission_id": prepared.command.payload.administration.submissionId
131954
- };
131955
- try {
131956
- const disposition = await persistAssessmentChildResult({
131957
- assessmentResults: this.requireClient().api.oneroster.assessmentResults,
131958
- command: prepared.command,
131959
- childLineItemId: prepared.childLineItemId
131960
- });
131961
- if (disposition.outcome === "conflict") {
131962
- addEvent("assessment.child_result_conflict", {
131963
- ...identity,
131964
- "app.assessment.child_result_id": disposition.resultId,
131965
- "app.assessment.child_result_conflict_reason": disposition.reason
131966
- });
131967
- return;
131968
- }
131969
- addEvent("assessment.child_result_persisted", {
131970
- ...identity,
131971
- "app.assessment.child_result_id": disposition.resultId,
131972
- "app.assessment.child_result_outcome": disposition.outcome,
131973
- "app.assessment.duration_ms": Date.now() - startedAt
131974
- });
131975
- } catch (error88) {
131976
- addEvent("assessment.child_result_persist_failed", {
131977
- ...identity,
131978
- "app.assessment.duration_ms": Date.now() - startedAt,
131979
- "exception.type": errorType(error88),
131980
- "app.error.message": errorMessage2(error88)
131981
- });
131982
- }
131983
- }
131984
131496
  assertAssessmentChildLineItem(lineItemId, stored, expected) {
131985
131497
  if (assessmentChildLineItemMatches(stored, expected)) {
131986
131498
  return;
@@ -132038,91 +131550,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
132038
131550
  }
132039
131551
  return lineItemId;
132040
131552
  }
132041
- async projectDiagnosticItemResponse(projection, assessment, checkpoint = null) {
132042
- try {
132043
- const loadedAssessment = assessment ?? await this.loadAttemptAssessment(projection.metadata);
132044
- const item = loadedAssessment.items.find((candidate) => candidate.identifier === projection.ledgerEntry.itemIdentifier);
132045
- if (!item) {
132046
- throw new Error(`Diagnostic assessment is missing item ${projection.ledgerEntry.itemIdentifier}`);
132047
- }
132048
- const childLineItemId = await this.ensureDiagnosticQuestionLineItem({
132049
- parentLineItemId: projection.result.assessmentLineItem.sourcedId,
132050
- integration: projection.integration,
132051
- metadata: projection.metadata,
132052
- item
132053
- });
132054
- if (checkpoint && checkpoint.childLineItemId !== childLineItemId) {
132055
- throw new Error("Prepared diagnostic checkpoint resolved a different line item");
132056
- }
132057
- const childResultId = await diagnosticQuestionResultId(projection.result.sourcedId, childLineItemId);
132058
- const finalized = buildFinalizedDiagnosticChildResult({
132059
- childLineItemId,
132060
- student: projection.result.student,
132061
- parentAttemptId: projection.result.sourcedId,
132062
- metadata: projection.metadata,
132063
- ledgerEntry: projection.ledgerEntry
132064
- });
132065
- await Promise.all([
132066
- this.projectPreparedAssessmentChildResult(checkpoint),
132067
- this.requireClient().api.oneroster.assessmentResults.upsert(childResultId, finalized.resultUpdate)
132068
- ]);
132069
- } catch (error88) {
132070
- addEvent("assessment.diagnostic_child_projection_failed", {
132071
- "app.assessment.attempt_id": projection.result.sourcedId,
132072
- "app.assessment.qti_item_identifier": projection.ledgerEntry.itemIdentifier,
132073
- "exception.type": errorType(error88),
132074
- "app.error.message": errorMessage2(error88)
132075
- });
132076
- }
132077
- }
132078
- async restoreDiagnosticChildResults(attempt) {
132079
- const assessment = attempt.assessment ?? await this.loadAttemptAssessment(attempt.metadata);
132080
- await runWithConcurrency(attempt.metadata.diagnostic.ledger, TimebackAssessmentRuntimeService2.SCORING_CONCURRENCY, async (ledgerEntry) => {
132081
- try {
132082
- const childLineItemId = await diagnosticQuestionLineItemId(attempt.result.assessmentLineItem.sourcedId, ledgerEntry.itemIdentifier);
132083
- const childResultId = await diagnosticQuestionResultId(attempt.result.sourcedId, childLineItemId);
132084
- let childResult;
132085
- try {
132086
- childResult = await this.requireClient().api.oneroster.assessmentResults.get(childResultId);
132087
- } catch (error88) {
132088
- if (!isApiError(error88) || error88.statusCode !== 404) {
132089
- throw error88;
132090
- }
132091
- }
132092
- const resolution = resolveDiagnosticChildResult({
132093
- childResult,
132094
- childLineItemId,
132095
- parentResult: attempt.result,
132096
- parentMetadata: attempt.metadata,
132097
- ledgerEntry
132098
- });
132099
- if (resolution.action === "reuse") {
132100
- return;
132101
- }
132102
- if (resolution.action === "restore") {
132103
- addEvent("assessment.diagnostic_child_result_restore", {
132104
- "app.assessment.diagnostic_child_restore_reason": resolution.reason,
132105
- "app.assessment.attempt_id": attempt.result.sourcedId,
132106
- "app.assessment.child_result_id": childResultId,
132107
- "app.assessment.qti_item_identifier": ledgerEntry.itemIdentifier
132108
- });
132109
- }
132110
- await this.projectDiagnosticItemResponse({
132111
- result: attempt.result,
132112
- metadata: attempt.metadata,
132113
- ledgerEntry,
132114
- integration: attempt.integration
132115
- }, assessment);
132116
- } catch (error88) {
132117
- addEvent("assessment.diagnostic_child_reconciliation_failed", {
132118
- "app.assessment.attempt_id": attempt.result.sourcedId,
132119
- "app.assessment.qti_item_identifier": ledgerEntry.itemIdentifier,
132120
- "exception.type": errorType(error88),
132121
- "app.error.message": errorMessage2(error88)
132122
- });
132123
- }
132124
- });
132125
- }
132126
131553
  async ensureReviewQuestionLineItem(input) {
132127
131554
  const client2 = this.requireClient();
132128
131555
  const lineItemId = await reviewQuestionLineItemId(input.parentLineItemId, input.item.identifier);
@@ -132169,220 +131596,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
132169
131596
  }
132170
131597
  return lineItemId;
132171
131598
  }
132172
- async putOpenReviewChildResult(input) {
132173
- const metadata2 = buildReviewItemResultMetadata({
132174
- metadata: input.parentMetadata,
132175
- selection: input.selection,
132176
- parentAttemptId: input.parentResult.sourcedId
132177
- });
132178
- await this.requireClient().api.oneroster.assessmentResults.upsert(input.childResultId, buildOpenReviewChildResult({
132179
- childLineItemId: input.childLineItemId,
132180
- student: input.parentResult.student,
132181
- metadata: metadata2
132182
- }));
132183
- }
132184
- async ensureReviewChildResults(input) {
132185
- const itemByIdentifier = new Map(input.assessment.items.map((item) => [item.identifier, item]));
132186
- const bankItemByIdentifier = new Map(input.source.bank.items.map((item) => [item.itemIdentifier, item]));
132187
- const physicalItems = new Set(input.metadata.review.selections.map((selection) => selection.itemIdentifier));
132188
- if (physicalItems.size !== input.metadata.review.selections.length) {
132189
- throw new Error("MVP review selection must use each physical item at most once");
132190
- }
132191
- const administeredItems = administeredItemIdentifiers(input.metadata.itemSubmissions);
132192
- await runWithConcurrency(input.metadata.review.selections, TimebackAssessmentRuntimeService2.SCORING_CONCURRENCY, async (selection) => {
132193
- const item = itemByIdentifier.get(selection.itemIdentifier);
132194
- const indexed = bankItemByIdentifier.get(selection.itemIdentifier);
132195
- if (!item || !indexed) {
132196
- throw new Error(`Review assessment or bank is missing selected item ${selection.itemIdentifier}`);
132197
- }
132198
- const childLineItemId = await this.ensureReviewQuestionLineItem({
132199
- parentLineItemId: input.result.assessmentLineItem.sourcedId,
132200
- integration: input.integration,
132201
- bankRevision: input.metadata.review.bankRevision,
132202
- standards: [...indexed.standards],
132203
- item
132204
- });
132205
- if (!administeredItems.has(selection.itemIdentifier)) {
132206
- return;
132207
- }
132208
- const childResultId = await reviewQuestionResultId(input.result.sourcedId, childLineItemId);
132209
- const streamedChildResult = input.existingChildResults.get(childResultId);
132210
- let childResult = streamedChildResult;
132211
- if (!childResult && input.lookupMissingResults) {
132212
- try {
132213
- childResult = await this.requireClient().api.oneroster.assessmentResults.get(childResultId);
132214
- } catch (error88) {
132215
- if (!isApiError(error88) || error88.statusCode !== 404) {
132216
- throw error88;
132217
- }
132218
- }
132219
- }
132220
- const resolution = resolveReviewChildResult({
132221
- childResult,
132222
- childLineItemId,
132223
- parentResult: input.result,
132224
- parentMetadata: input.metadata,
132225
- selection
132226
- });
132227
- if (resolution.action === "reuse") {
132228
- return;
132229
- }
132230
- if (resolution.action === "restore") {
132231
- addEvent("assessment.review_child_result_restore", {
132232
- "app.assessment.review_child_restore_reason": resolution.reason,
132233
- "app.assessment.attempt_id": input.result.sourcedId,
132234
- "app.assessment.child_result_id": childResultId,
132235
- "app.assessment.qti_item_identifier": selection.itemIdentifier
132236
- });
132237
- }
132238
- await this.putOpenReviewChildResult({
132239
- childResultId,
132240
- childLineItemId,
132241
- parentResult: input.result,
132242
- parentMetadata: input.metadata,
132243
- selection
132244
- });
132245
- });
132246
- }
132247
- async putReviewChildResponse(projection, checkpoint) {
132248
- const { result, metadata: metadata2, itemIdentifier, integration } = projection;
132249
- const selection = metadata2.review.selections.find((candidate) => candidate.itemIdentifier === itemIdentifier);
132250
- if (!selection) {
132251
- return;
132252
- }
132253
- try {
132254
- const source = await this.loadPinnedReviewBankSource(metadata2);
132255
- const item = source.assessment.items.find((candidate) => candidate.identifier === selection.itemIdentifier);
132256
- const indexed = source.bank.items.find((candidate) => candidate.itemIdentifier === selection.itemIdentifier);
132257
- if (!item || !indexed) {
132258
- throw new Error(`Review assessment or bank is missing selected item ${selection.itemIdentifier}`);
132259
- }
132260
- const childLineItemId = await this.ensureReviewQuestionLineItem({
132261
- parentLineItemId: result.assessmentLineItem.sourcedId,
132262
- integration,
132263
- bankRevision: metadata2.review.bankRevision,
132264
- standards: [...indexed.standards],
132265
- item
132266
- });
132267
- if (checkpoint && checkpoint.childLineItemId !== childLineItemId) {
132268
- throw new Error("Prepared review checkpoint resolved a different line item");
132269
- }
132270
- const childResultId = await reviewQuestionResultId(result.sourcedId, childLineItemId);
132271
- const childMetadata = buildReviewItemResultMetadata({
132272
- metadata: metadata2,
132273
- selection,
132274
- parentAttemptId: result.sourcedId
132275
- });
132276
- await Promise.all([
132277
- this.projectPreparedAssessmentChildResult(checkpoint),
132278
- this.requireClient().api.oneroster.assessmentResults.upsert(childResultId, buildOpenReviewChildResult({
132279
- childLineItemId,
132280
- student: result.student,
132281
- metadata: childMetadata
132282
- }))
132283
- ]);
132284
- } catch (error88) {
132285
- addEvent("assessment.review_child_response_projection_failed", {
132286
- "app.assessment.attempt_id": result.sourcedId,
132287
- "app.assessment.qti_item_identifier": selection.itemIdentifier,
132288
- "exception.type": errorType(error88),
132289
- "app.error.message": errorMessage2(error88)
132290
- });
132291
- }
132292
- }
132293
- async projectReviewItemResponse(params, projection, checkpoint) {
132294
- const terminal = assessmentChildProjectionIsTerminal(projection.result);
132295
- try {
132296
- await reconcileReviewChildResults({
132297
- initial: projection,
132298
- writeOpen: () => this.putReviewChildResponse(projection, checkpoint),
132299
- crossBarrier: () => crossAssessmentAttemptLockBarrier(this.deps.assessmentRuntimeLock, this.deps.db, params.attemptId),
132300
- readLatest: () => this.peekAttempt(params),
132301
- restoreTerminal: async (latest) => {
132302
- const repair = this.restoreCompletedReviewChildResponses(latest.result, latest.metadata, new Set([projection.itemIdentifier]));
132303
- if (terminal) {
132304
- await Promise.allSettled([
132305
- this.projectPreparedAssessmentChildResult(checkpoint),
132306
- repair
132307
- ]);
132308
- } else {
132309
- await repair;
132310
- }
132311
- }
132312
- });
132313
- } catch (error88) {
132314
- addEvent("assessment.review_child_projection_barrier_failed", {
132315
- "app.assessment.attempt_id": params.attemptId,
132316
- "app.assessment.qti_item_identifier": projection.itemIdentifier,
132317
- "exception.type": errorType(error88),
132318
- "app.error.message": errorMessage2(error88)
132319
- });
132320
- }
132321
- }
132322
- restoreAllCompletedReviewChildResponses(result, metadata2) {
132323
- return this.restoreCompletedReviewChildResponses(result, metadata2, new Set(metadata2.itemSubmissions.map((item) => item.itemIdentifier)));
132324
- }
132325
- async restoreCompletedReviewChildResponses(result, metadata2, changedItemIdentifiers) {
132326
- const outcomes = metadata2.completion?.itemOutcomes;
132327
- const submissionId = metadata2.submissionId;
132328
- if (!submissionId || !outcomes) {
132329
- addEvent("assessment.review_child_terminal_restore_unavailable", {
132330
- "app.assessment.attempt_id": result.sourcedId
132331
- });
132332
- return;
132333
- }
132334
- const outcomeByItem = new Map(outcomes.map((outcome) => [outcome.itemIdentifier, outcome]));
132335
- await runWithConcurrency(metadata2.review.selections.filter((selection) => changedItemIdentifiers.has(selection.itemIdentifier)), TimebackAssessmentRuntimeService2.SCORING_CONCURRENCY, async (selection) => {
132336
- try {
132337
- const outcome = outcomeByItem.get(selection.itemIdentifier);
132338
- if (!outcome) {
132339
- throw new Error(`Completed review omitted item ${selection.itemIdentifier}`);
132340
- }
132341
- const childLineItemId = await reviewQuestionLineItemId(result.assessmentLineItem.sourcedId, selection.itemIdentifier);
132342
- const childResultId = await reviewQuestionResultId(result.sourcedId, childLineItemId);
132343
- const finalized = buildFinalizedReviewChildResult({
132344
- childLineItemId,
132345
- student: result.student,
132346
- parentAttemptId: result.sourcedId,
132347
- metadata: metadata2,
132348
- selection,
132349
- itemResult: {
132350
- itemIdentifier: outcome.itemIdentifier,
132351
- score: outcome.score,
132352
- isCorrect: outcome.isCorrect
132353
- },
132354
- submissionId,
132355
- timestamp: result.scoreDate
132356
- });
132357
- await this.requireClient().api.oneroster.assessmentResults.upsert(childResultId, finalized.resultUpdate);
132358
- } catch (error88) {
132359
- addEvent("assessment.review_child_terminal_restore_failed", {
132360
- "app.assessment.attempt_id": result.sourcedId,
132361
- "app.assessment.qti_item_identifier": selection.itemIdentifier,
132362
- "exception.type": errorType(error88),
132363
- "app.error.message": errorMessage2(error88)
132364
- });
132365
- }
132366
- });
132367
- }
132368
- reviewItemOutcomes(input) {
132369
- const resultByItem = new Map(input.itemResults.map((itemResult) => [itemResult.itemIdentifier, itemResult]));
132370
- const administeredItems = administeredItemIdentifiers(input.metadata.itemSubmissions);
132371
- return input.metadata.review.selections.filter((selection) => administeredItems.has(selection.itemIdentifier)).map((selection) => {
132372
- const itemResult = resultByItem.get(selection.itemIdentifier);
132373
- const submission = itemAdministration(input.metadata.itemSubmissions, selection.itemIdentifier);
132374
- if (!itemResult || !submission) {
132375
- throw new Error(`Scoring omitted selected review item ${selection.itemIdentifier}`);
132376
- }
132377
- return {
132378
- itemIdentifier: selection.itemIdentifier,
132379
- standard: selection.standard,
132380
- answered: submission.answered,
132381
- score: itemResult.score,
132382
- isCorrect: itemResult.isCorrect
132383
- };
132384
- });
132385
- }
132386
131599
  async resolveAttemptAuthorization({ gameId, studentId, attemptId, user }, options = {}) {
132387
131600
  if (!options.developerAccessValidated) {
132388
131601
  await this.deps.validateDeveloperAccess(user, gameId);
@@ -132411,14 +131624,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
132411
131624
  const enrollment = enrollments.find((candidate) => candidate.id === metadata2.enrollmentId && candidate.course.id === integration.courseId);
132412
131625
  return { attempt: { result, metadata: metadata2, integration }, enrollment };
132413
131626
  }
132414
- async authorizeAttempt(params, options = {}) {
132415
- const authorization = await this.resolveAttemptAuthorization(params, options);
132416
- return this.requireActiveAttempt(authorization, params.attemptId);
132417
- }
132418
- requireActiveAttempt(authorization, attemptId) {
132419
- const enrollment = requireAssessmentMutationAccess(attemptId, authorization.attempt.integration.status, authorization.enrollment);
132420
- return { ...authorization.attempt, enrollment };
132421
- }
132422
131627
  async authorizeAttemptRead(params) {
132423
131628
  const { attempt, enrollment } = await this.resolveAttemptAuthorization(params);
132424
131629
  if ((!enrollment || attempt.integration.status === "deactivated") && !this.isSettled(attempt.result)) {
@@ -132429,7 +131634,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
132429
131634
  unauthorizedAttempt(attemptId) {
132430
131635
  return AssessmentRuntimeError.from(assessmentAttemptUnauthorized(attemptId));
132431
131636
  }
132432
- async snapshotForResult(result, metadata2, integration) {
131637
+ async snapshotForResult(result, metadata2, integration, suppliedTranscript) {
132433
131638
  if (isAssessmentAttemptSuperseded({
132434
131639
  inProgress: result.inProgress ?? "",
132435
131640
  scoreStatus: result.scoreStatus
@@ -132465,38 +131670,59 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
132465
131670
  result: buildCompletedAssessmentSubmitResult({ result, metadata: metadata2 })
132466
131671
  };
132467
131672
  }
132468
- const assessment = await this.loadAttemptAssessment(metadata2);
132469
- if (this.isPlatformRoutedDiagnosticMetadata(metadata2)) {
132470
- const routing = await this.loadAttemptDiagnosticRouting(metadata2, assessment);
132471
- await this.restoreDiagnosticChildResults({
132472
- result,
132473
- metadata: metadata2,
132474
- integration,
132475
- assessment
132476
- });
132477
- return this.diagnosticSnapshot(result, metadata2, assessment, integration, routing.state);
132478
- }
132479
- return this.snapshot(result, metadata2, assessment, integration);
132480
- }
132481
- diagnosticSnapshot(result, metadata2, assessment, integration, state) {
132482
- return buildDiagnosticAssessmentSnapshot({
132483
- attemptId: result.sourcedId,
132484
- metadata: metadata2,
132485
- assessment,
132486
- state,
132487
- activityData: this.activityData(metadata2, assessment, integration)
131673
+ const context2 = this.tokenContext({ result, metadata: metadata2, integration });
131674
+ const graded = await resumeAssessmentTranscript({
131675
+ suppliedTranscript,
131676
+ checkpointTranscript: metadata2.transcript,
131677
+ recover: async () => recoverAssessmentTranscript(context2, await this.studentAssessmentResults(context2.studentId)),
131678
+ grade: (transcript) => this.gradeTranscript(context2, transcript)
132488
131679
  });
131680
+ const session2 = await this.assessmentSession(context2, graded.transcript);
131681
+ const common2 = {
131682
+ attemptId: result.sourcedId,
131683
+ assessment: graded.assessment,
131684
+ activityData: this.activityData(metadata2, graded.assessment, integration)
131685
+ };
131686
+ return this.isPlatformRoutedDiagnosticMetadata(graded.metadata) ? {
131687
+ ...buildDiagnosticAssessmentSnapshot({
131688
+ ...common2,
131689
+ metadata: graded.metadata,
131690
+ state: graded.metadata.diagnostic.state
131691
+ }),
131692
+ ...session2
131693
+ } : {
131694
+ ...buildConventionalAssessmentSnapshot({ ...common2, metadata: graded.metadata }),
131695
+ ...session2
131696
+ };
131697
+ }
131698
+ async assessmentSession(context2, transcript = emptyAssessmentTranscript(context2.attemptId, context2.metadata.selectedTest.contentRevision)) {
131699
+ return { transcript, assessmentToken: await this.deps.mintAssessmentToken(context2) };
131700
+ }
131701
+ async diagnosticSnapshot(result, metadata2, assessment, integration, options) {
131702
+ return {
131703
+ ...buildDiagnosticAssessmentSnapshot({
131704
+ attemptId: result.sourcedId,
131705
+ metadata: metadata2,
131706
+ assessment,
131707
+ state: options.state,
131708
+ activityData: this.activityData(metadata2, assessment, integration)
131709
+ }),
131710
+ ...await this.assessmentSession(this.tokenContext({ result, metadata: metadata2, integration }))
131711
+ };
132489
131712
  }
132490
131713
  diagnosticRoutingSnapshot(metadata2, state) {
132491
131714
  return projectDiagnosticRoutingSnapshot(metadata2.diagnostic.routingRevision, state);
132492
131715
  }
132493
- snapshot(result, metadata2, assessment, integration) {
132494
- return buildConventionalAssessmentSnapshot({
132495
- attemptId: result.sourcedId,
132496
- metadata: metadata2,
132497
- assessment,
132498
- activityData: this.activityData(metadata2, assessment, integration)
132499
- });
131716
+ async snapshot(result, metadata2, assessment, integration) {
131717
+ return {
131718
+ ...buildConventionalAssessmentSnapshot({
131719
+ attemptId: result.sourcedId,
131720
+ metadata: metadata2,
131721
+ assessment,
131722
+ activityData: this.activityData(metadata2, assessment, integration)
131723
+ }),
131724
+ ...await this.assessmentSession(this.tokenContext({ result, metadata: metadata2, integration }))
131725
+ };
132500
131726
  }
132501
131727
  activityData(metadata2, assessment, integration) {
132502
131728
  const course = this.trackableCourse(integration, "assessment.activity_tracking_invalid_course_metadata");
@@ -132557,16 +131783,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
132557
131783
  isSettled(result) {
132558
131784
  return this.isAwaitingAward(result) || this.isCompleted(result);
132559
131785
  }
132560
- assertInProgress(result) {
132561
- if (!this.isOpen(result)) {
132562
- throw AssessmentRuntimeError.from(assessmentAttemptNotOpen(result.sourcedId));
132563
- }
132564
- }
132565
- assertResponseVersion(metadata2, expectedResponseVersion) {
132566
- if (metadata2.responseVersion !== expectedResponseVersion) {
132567
- throw AssessmentRuntimeError.from(assessmentResponseVersionConflict(expectedResponseVersion, metadata2.responseVersion));
132568
- }
132569
- }
132570
131786
  scoreFromResult(result, possible, metadata2) {
132571
131787
  return metadata2.score ?? assessmentScoreFromPercentage(result.score, possible);
132572
131788
  }
@@ -132634,7 +131850,9 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
132634
131850
  grade: integration.grade
132635
131851
  })
132636
131852
  });
132637
- return this.isPlatformRoutedDiagnosticMetadata(metadata2) ? this.diagnosticSnapshot(result, metadata2, assessment, integration, metadata2.diagnostic.state) : this.snapshot(result, metadata2, assessment, integration);
131853
+ return this.isPlatformRoutedDiagnosticMetadata(metadata2) ? this.diagnosticSnapshot(result, metadata2, assessment, integration, {
131854
+ state: metadata2.diagnostic.state
131855
+ }) : this.snapshot(result, metadata2, assessment, integration);
132638
131856
  }
132639
131857
  async claimAttemptId(scope, from) {
132640
131858
  for (let attemptNumber = from;attemptNumber < from + 20; attemptNumber += 1) {
@@ -132679,18 +131897,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
132679
131897
  game: game2
132680
131898
  };
132681
131899
  }
132682
- async putResultMetadata(result, metadata2) {
132683
- await this.requireClient().api.oneroster.assessmentResults.upsert(result.sourcedId, {
132684
- status: ONEROSTER_STATUS2.active,
132685
- assessmentLineItem: result.assessmentLineItem,
132686
- student: result.student,
132687
- score: result.score ?? 0,
132688
- scoreDate: result.scoreDate,
132689
- scoreStatus: result.scoreStatus,
132690
- ...result.inProgress === undefined || result.inProgress === null ? {} : { inProgress: result.inProgress },
132691
- metadata: mergeAssessmentRuntimeMetadata(result.metadata, metadata2)
132692
- });
132693
- }
132694
131900
  async emitCompletionBestEffort(input) {
132695
131901
  const { attempt } = input;
132696
131902
  const integration = attempt.integration;
@@ -136251,6 +135457,8 @@ function createPlatformServices(deps) {
136251
135457
  mintPlatformServiceToken,
136252
135458
  mintAssessmentPreparationReceipt,
136253
135459
  verifyAssessmentPreparationReceipt,
135460
+ mintAssessmentToken,
135461
+ verifyAssessmentToken,
136254
135462
  validateDeveloperAccessBySlug,
136255
135463
  validateDeveloperAccess,
136256
135464
  validateGameManagementAccess
@@ -136325,7 +135533,9 @@ function createPlatformServices(deps) {
136325
135533
  validateDeveloperAccess,
136326
135534
  assessmentRuntimeLock,
136327
135535
  mintAssessmentPreparationReceipt,
136328
- verifyAssessmentPreparationReceipt
135536
+ verifyAssessmentPreparationReceipt,
135537
+ mintAssessmentToken,
135538
+ verifyAssessmentToken
136329
135539
  });
136330
135540
  return {
136331
135541
  assessmentAssets,
@@ -137060,6 +136270,8 @@ function createServices(ctx) {
137060
136270
  mintPlatformServiceToken: auth2.mintPlatformServiceToken.bind(auth2),
137061
136271
  mintAssessmentPreparationReceipt: auth2.mintAssessmentPreparationReceipt.bind(auth2),
137062
136272
  verifyAssessmentPreparationReceipt: auth2.verifyAssessmentPreparationReceipt.bind(auth2),
136273
+ mintAssessmentToken: auth2.mintAssessmentToken.bind(auth2),
136274
+ verifyAssessmentToken: auth2.verifyAssessmentToken.bind(auth2),
137063
136275
  ...gaming.validators
137064
136276
  });
137065
136277
  const standalone = createStandaloneServices({ db: db2, auth: auth2, timeback: timeback2 });
@@ -137562,6 +136774,15 @@ function createSandboxAuthProvider() {
137562
136774
  });
137563
136775
  return { receipt, expiresAt: new Date(expiresAt).toISOString() };
137564
136776
  },
136777
+ async mintAssessmentToken(context2) {
136778
+ const token = `sandbox_assessment_${crypto.randomUUID()}`;
136779
+ sandboxAssessmentTokens.set(token, structuredClone(context2));
136780
+ return token;
136781
+ },
136782
+ async verifyAssessmentToken(token) {
136783
+ const context2 = sandboxAssessmentTokens.get(token);
136784
+ return context2 ? structuredClone(context2) : null;
136785
+ },
137565
136786
  async verifyAssessmentPreparationReceipt(receipt) {
137566
136787
  const prepared = sandboxAssessmentPreparationReceipts.get(receipt);
137567
136788
  if (!prepared) {
@@ -137617,12 +136838,18 @@ function createSandboxAuthProvider() {
137617
136838
  var sandboxApiKeys;
137618
136839
  var sandboxAssessmentPreparationReceipts;
137619
136840
  var ASSESSMENT_PREPARATION_RECEIPT_LIFETIME_MS;
137620
- var init_auth_provider = __esm(() => {
136841
+ var sandboxAssessmentTokens;
136842
+ var init_auth_provider = __esm(async () => {
137621
136843
  init_src6();
137622
136844
  init_constants();
136845
+ await init_dist5();
137623
136846
  sandboxApiKeys = new Map;
137624
136847
  sandboxAssessmentPreparationReceipts = new Map;
137625
136848
  ASSESSMENT_PREPARATION_RECEIPT_LIFETIME_MS = 300000;
136849
+ sandboxAssessmentTokens = new TimebackCache({
136850
+ defaultTTL: 7200000,
136851
+ maxSize: 1000
136852
+ });
137626
136853
  });
137627
136854
  function createSandboxCacheProvider() {
137628
136855
  return {
@@ -137775,10 +137002,10 @@ var init_storage_provider = __esm(() => {
137775
137002
  init_src6();
137776
137003
  storage = new Map;
137777
137004
  });
137778
- var init_providers = __esm(() => {
137779
- init_auth_provider();
137005
+ var init_providers = __esm(async () => {
137780
137006
  init_cache_provider();
137781
137007
  init_storage_provider();
137008
+ await init_auth_provider();
137782
137009
  });
137783
137010
  function buildConfig(options) {
137784
137011
  const baseUrl = `http://localhost:${options.port ?? 3000}`;
@@ -137847,10 +137074,10 @@ var init_context = __esm(async () => {
137847
137074
  init_config2();
137848
137075
  init_utils7();
137849
137076
  init_src6();
137850
- init_providers();
137851
137077
  await __promiseAll([
137852
137078
  init_factory(),
137853
- init_clients()
137079
+ init_clients(),
137080
+ init_providers()
137854
137081
  ]);
137855
137082
  });
137856
137083
  function resolveUser(c) {
@@ -196925,7 +196152,9 @@ var prepareRuntimeAssessment;
196925
196152
  var startRuntimeAssessment;
196926
196153
  var getRuntimeAssessment;
196927
196154
  var getLatestRuntimeAssessment;
196155
+ var resumeRuntimeAssessment;
196928
196156
  var saveRuntimeAssessment;
196157
+ var recordRuntimeAssessmentProgress;
196929
196158
  var submitRuntimeAssessmentItem;
196930
196159
  var submitRuntimeAssessment;
196931
196160
  var finalizeRuntimeAssessment;
@@ -197266,6 +196495,20 @@ var init_timeback_controller = __esm(() => {
197266
196495
  user: ctx.user
197267
196496
  });
197268
196497
  });
196498
+ resumeRuntimeAssessment = requireDeveloper(async (ctx) => {
196499
+ const attemptId = ctx.params.attemptId;
196500
+ const body2 = await parseRequestBody(ctx.request, ResumeRuntimeAssessmentRequestSchema);
196501
+ if (!attemptId) {
196502
+ throw ApiError.badRequest("Missing attemptId");
196503
+ }
196504
+ return ctx.services.timebackAssessmentRuntime.resume({
196505
+ gameId: body2.gameId,
196506
+ studentId: body2.studentId,
196507
+ attemptId,
196508
+ input: { transcript: body2.transcript },
196509
+ user: ctx.user
196510
+ });
196511
+ });
197269
196512
  saveRuntimeAssessment = requireDeveloper(async (ctx) => {
197270
196513
  const attemptId = ctx.params.attemptId;
197271
196514
  const body2 = await parseRequestBody(ctx.request, SaveRuntimeAssessmentRequestSchema);
@@ -197278,7 +196521,26 @@ var init_timeback_controller = __esm(() => {
197278
196521
  attemptId,
197279
196522
  input: {
197280
196523
  expectedResponseVersion: body2.expectedResponseVersion,
197281
- responses: body2.responses
196524
+ transcript: body2.transcript
196525
+ },
196526
+ user: ctx.user
196527
+ });
196528
+ });
196529
+ recordRuntimeAssessmentProgress = requireDeveloper(async (ctx) => {
196530
+ const attemptId = ctx.params.attemptId;
196531
+ const body2 = await parseRequestBody(ctx.request, RecordRuntimeAssessmentProgressRequestSchema);
196532
+ if (!attemptId) {
196533
+ throw ApiError.badRequest("Missing attemptId");
196534
+ }
196535
+ return ctx.services.timebackAssessmentRuntime.recordProgress({
196536
+ gameId: body2.gameId,
196537
+ studentId: body2.studentId,
196538
+ attemptId,
196539
+ input: {
196540
+ assessmentToken: body2.assessmentToken,
196541
+ transcript: body2.transcript,
196542
+ expectedResponseVersion: body2.expectedResponseVersion,
196543
+ itemIdentifier: body2.itemIdentifier
197282
196544
  },
197283
196545
  user: ctx.user
197284
196546
  });
@@ -197294,6 +196556,8 @@ var init_timeback_controller = __esm(() => {
197294
196556
  studentId: body2.studentId,
197295
196557
  attemptId,
197296
196558
  input: {
196559
+ assessmentToken: body2.assessmentToken,
196560
+ transcript: body2.transcript,
197297
196561
  expectedResponseVersion: body2.expectedResponseVersion,
197298
196562
  submissionId: body2.submissionId,
197299
196563
  itemIdentifier: body2.itemIdentifier,
@@ -197316,6 +196580,7 @@ var init_timeback_controller = __esm(() => {
197316
196580
  input: {
197317
196581
  expectedResponseVersion: body2.expectedResponseVersion,
197318
196582
  submissionId: body2.submissionId,
196583
+ transcript: body2.transcript,
197319
196584
  ...body2.session ? { session: body2.session } : {}
197320
196585
  },
197321
196586
  game: {
@@ -197793,6 +197058,8 @@ var init_timeback_controller = __esm(() => {
197793
197058
  getLatestRuntimeAssessment,
197794
197059
  getRuntimeAssessment,
197795
197060
  saveRuntimeAssessment,
197061
+ resumeRuntimeAssessment,
197062
+ recordRuntimeAssessmentProgress,
197796
197063
  submitRuntimeAssessmentItem,
197797
197064
  submitRuntimeAssessment,
197798
197065
  finalizeRuntimeAssessment,
@@ -198746,6 +198013,8 @@ var init_timeback8 = __esm(async () => {
198746
198013
  timebackRouter.post("/assessments/start", handle2(timeback2.startRuntimeAssessment));
198747
198014
  timebackRouter.get("/assessments/latest", handle2(timeback2.getLatestRuntimeAssessment));
198748
198015
  timebackRouter.get("/assessments/:attemptId", handle2(timeback2.getRuntimeAssessment));
198016
+ timebackRouter.post("/assessments/:attemptId/resume", handle2(timeback2.resumeRuntimeAssessment));
198017
+ timebackRouter.post("/assessments/:attemptId/progress", handle2(timeback2.recordRuntimeAssessmentProgress));
198749
198018
  timebackRouter.post("/assessments/:attemptId/save", handle2(timeback2.saveRuntimeAssessment));
198750
198019
  timebackRouter.post("/assessments/:attemptId/submit-item", handle2(timeback2.submitRuntimeAssessmentItem));
198751
198020
  timebackRouter.post("/assessments/:attemptId/submit", handle2(timeback2.submitRuntimeAssessment));
@@ -199147,13 +198416,13 @@ var init_server = __esm(async () => {
199147
198416
  init_port();
199148
198417
  init_package();
199149
198418
  init_config();
199150
- init_providers();
199151
198419
  init_app();
199152
198420
  init_database2();
199153
198421
  init_options();
199154
198422
  await __promiseAll([
199155
198423
  init_api3(),
199156
- init_context()
198424
+ init_context(),
198425
+ init_providers()
199157
198426
  ]);
199158
198427
  version5 = package_default2.version;
199159
198428
  });