@playcademy/vite-plugin 1.2.1-beta.10 → 1.2.1-beta.12

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 +575 -162
  2. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -24315,7 +24315,7 @@ import path2 from "node:path";
24315
24315
  // package.json
24316
24316
  var package_default = {
24317
24317
  name: "@playcademy/vite-plugin",
24318
- version: "1.2.1-beta.10",
24318
+ version: "1.2.1-beta.12",
24319
24319
  type: "module",
24320
24320
  exports: {
24321
24321
  ".": {
@@ -25921,7 +25921,7 @@ var package_default2;
25921
25921
  var init_package = __esm(() => {
25922
25922
  package_default2 = {
25923
25923
  name: "@playcademy/sandbox",
25924
- version: "0.7.1-beta.10",
25924
+ version: "0.7.1-beta.12",
25925
25925
  description: "Local development server for Playcademy game development",
25926
25926
  type: "module",
25927
25927
  exports: {
@@ -34346,6 +34346,13 @@ function classifyAssessmentSubmission(attempt, submissionId) {
34346
34346
  }
34347
34347
  return isAssessmentAttemptOpen(attempt) ? "submit" : "reject";
34348
34348
  }
34349
+ function assessmentFlowViolation(message, details) {
34350
+ return {
34351
+ code: "ASSESSMENT_FLOW_VIOLATION",
34352
+ message,
34353
+ ...details ? { details } : {}
34354
+ };
34355
+ }
34349
34356
  function assessmentAttemptUnauthorized(attemptId) {
34350
34357
  return {
34351
34358
  code: "UNAUTHORIZED_ATTEMPT",
@@ -34387,139 +34394,8 @@ function assessmentReviewBankShortage(fulfillment) {
34387
34394
  details: { fulfillment }
34388
34395
  };
34389
34396
  }
34390
- function selectAssessmentCourseCandidate(candidates, subjectSpecified) {
34391
- if (candidates.length === 0) {
34392
- return { outcome: "none" };
34393
- }
34394
- const serviceable = candidates.filter((candidate) => candidate.hasLiveTests || candidate.hasOpenAttempt);
34395
- if (serviceable.length === 0) {
34396
- return { outcome: "no_eligible_tests" };
34397
- }
34398
- const withOpenAttempt = serviceable.filter((candidate) => candidate.hasOpenAttempt);
34399
- const eligible = withOpenAttempt.length > 0 ? withOpenAttempt : serviceable;
34400
- const subjects = new Set(eligible.map((candidate) => candidate.subject)).size;
34401
- if (!subjectSpecified && subjects > 1) {
34402
- return { outcome: "ambiguous", subjects };
34403
- }
34404
- const selected = [...eligible].toSorted((left, right) => left.grade - right.grade || left.id.localeCompare(right.id))[0];
34405
- return { outcome: "selected", selected };
34406
- }
34407
- function assessmentAttemptTimestamp(value) {
34408
- const parsed = Date.parse(value);
34409
- return Number.isNaN(parsed) ? 0 : parsed;
34410
- }
34411
- function newestAttempt(left, right, field) {
34412
- const dateDifference = assessmentAttemptTimestamp(right[field]) - assessmentAttemptTimestamp(left[field]);
34413
- return dateDifference || left.attemptId.localeCompare(right.attemptId);
34414
- }
34415
- function compareNewestCompletedAssessmentAttempt(left, right) {
34416
- return newestAttempt(left, right, "scoreDate");
34417
- }
34418
- function compareAssessmentCatalogOrder(left, right) {
34419
- if (left.sortOrder !== null || right.sortOrder !== null) {
34420
- if (left.sortOrder === null) {
34421
- return 1;
34422
- }
34423
- if (right.sortOrder === null) {
34424
- return -1;
34425
- }
34426
- const orderDifference = left.sortOrder - right.sortOrder;
34427
- if (orderDifference) {
34428
- return orderDifference;
34429
- }
34430
- }
34431
- const leftUpdatedAt = Date.parse(left.updatedAt);
34432
- const rightUpdatedAt = Date.parse(right.updatedAt);
34433
- const leftHasValidDate = Number.isFinite(leftUpdatedAt);
34434
- const rightHasValidDate = Number.isFinite(rightUpdatedAt);
34435
- if (leftHasValidDate && rightHasValidDate) {
34436
- const updatedDifference = leftUpdatedAt - rightUpdatedAt;
34437
- if (updatedDifference) {
34438
- return updatedDifference;
34439
- }
34440
- } else if (leftHasValidDate !== rightHasValidDate) {
34441
- return leftHasValidDate ? -1 : 1;
34442
- }
34443
- return left.qtiTestIdentifier.localeCompare(right.qtiTestIdentifier);
34444
- }
34445
- function orderRuntimeAssessmentTests(tests) {
34446
- return [...tests].toSorted(compareAssessmentCatalogOrder);
34447
- }
34448
- function selectRuntimeAssessment(liveTests, attempts) {
34449
- const resumable = attempts.filter((attempt) => attempt.resumable !== false && isAssessmentAttemptOpen(attempt)).toSorted((left, right) => newestAttempt(left, right, "updatedAt"));
34450
- if (resumable.length > 0) {
34451
- return {
34452
- kind: "resume",
34453
- attempt: resumable[0],
34454
- additionalResumableAttemptIds: resumable.slice(1).map((candidate) => candidate.attemptId)
34455
- };
34456
- }
34457
- const orderedTests = orderRuntimeAssessmentTests(liveTests);
34458
- if (orderedTests.length === 0) {
34459
- return null;
34460
- }
34461
- const completed = attempts.filter(isAssessmentAttemptCompleted);
34462
- const exposedTestIds = new Set(attempts.filter((attempt) => isAssessmentAttemptOpen(attempt) || isAssessmentAttemptCompleted(attempt)).map((attempt) => attempt.selectedTestIdentifier));
34463
- const unattempted = orderedTests.find((test) => !exposedTestIds.has(test.qtiTestIdentifier));
34464
- if (unattempted) {
34465
- return { kind: "start", test: unattempted, reason: "unattempted" };
34466
- }
34467
- const latestCompleted = completed.toSorted(compareNewestCompletedAssessmentAttempt)[0];
34468
- const latestTestIndex = latestCompleted ? orderedTests.findIndex((test) => test.qtiTestIdentifier === latestCompleted.selectedTestIdentifier) : -1;
34469
- if (latestTestIndex === -1) {
34470
- return { kind: "start", test: orderedTests[0], reason: "first_live" };
34471
- }
34472
- return {
34473
- kind: "start",
34474
- test: orderedTests[(latestTestIndex + 1) % orderedTests.length],
34475
- reason: "round_robin"
34476
- };
34477
- }
34478
- function presentationHash(value) {
34479
- let hash = 2166136261;
34480
- for (let index = 0;index < value.length; index++) {
34481
- hash ^= value.charCodeAt(index);
34482
- hash = Math.imul(hash, 16777619);
34483
- }
34484
- return hash >>> 0;
34485
- }
34486
- function comparePresentationText(left, right) {
34487
- if (left < right) {
34488
- return -1;
34489
- }
34490
- if (left > right) {
34491
- return 1;
34492
- }
34493
- return 0;
34494
- }
34495
- function presentationChoices(choices, attemptId, itemIdentifier, domain) {
34496
- const seed = `${attemptId}\x00${itemIdentifier}\x00${domain}\x00`;
34497
- return [...choices].toSorted((left, right) => {
34498
- const hashDifference = presentationHash(`${seed}${left.identifier}`) - presentationHash(`${seed}${right.identifier}`);
34499
- return hashDifference || comparePresentationText(left.identifier, right.identifier);
34500
- });
34501
- }
34502
- function assessmentPresentationForAttempt(assessment, attemptId) {
34503
- const items = assessment.items.map((item) => {
34504
- const interactions = item.interactions.map((interaction) => {
34505
- if (interaction.type === "order" && interaction.shuffle) {
34506
- return {
34507
- ...interaction,
34508
- choices: presentationChoices(interaction.choices, attemptId, item.identifier, `order\x00${interaction.responseIdentifier}`)
34509
- };
34510
- }
34511
- if (interaction.type === "match" && interaction.shuffle) {
34512
- return {
34513
- ...interaction,
34514
- sourceChoices: presentationChoices(interaction.sourceChoices, attemptId, item.identifier, `match-source\x00${interaction.responseIdentifier}`),
34515
- targetChoices: presentationChoices(interaction.targetChoices, attemptId, item.identifier, `match-target\x00${interaction.responseIdentifier}`)
34516
- };
34517
- }
34518
- return interaction;
34519
- });
34520
- return { ...item, interactions };
34521
- });
34522
- return { ...assessment, items };
34397
+ function assessmentFlowForPurpose(purpose) {
34398
+ return purpose === "review" || purpose === "mastery" ? "item-submit" : "attempt-submit";
34523
34399
  }
34524
34400
  function isTimebackGrade(value) {
34525
34401
  return typeof value === "number" && Number.isInteger(value) && GRADE_VALUES.includes(value);
@@ -34531,18 +34407,22 @@ function isAssessmentResponseValue(value) {
34531
34407
  const entries = Array.isArray(value) ? value : [value];
34532
34408
  return entries.length > 0 && entries.every((entry2) => typeof entry2 === "string" && entry2.length > 0 && entry2.length <= TIMEBACK_ASSESSMENT_RESPONSE_VALUE_MAX_LENGTH);
34533
34409
  }
34410
+ function applyAssessmentItemResponseUpdate(current, update) {
34411
+ const next = { ...current };
34412
+ for (const [responseId, value] of Object.entries(update)) {
34413
+ if (value === null) {
34414
+ delete next[responseId];
34415
+ } else {
34416
+ next[responseId] = value;
34417
+ }
34418
+ }
34419
+ return Object.keys(next).length === 0 ? undefined : next;
34420
+ }
34534
34421
  function applyAssessmentResponseUpdate(current, update) {
34535
34422
  const next = structuredClone(current);
34536
34423
  for (const [questionId, questionUpdate] of Object.entries(update)) {
34537
- const questionResponses = { ...next[questionId] };
34538
- for (const [responseId, value] of Object.entries(questionUpdate)) {
34539
- if (value === null) {
34540
- delete questionResponses[responseId];
34541
- } else {
34542
- questionResponses[responseId] = value;
34543
- }
34544
- }
34545
- if (Object.keys(questionResponses).length === 0) {
34424
+ const questionResponses = applyAssessmentItemResponseUpdate(next[questionId], questionUpdate);
34425
+ if (questionResponses === undefined) {
34546
34426
  delete next[questionId];
34547
34427
  } else {
34548
34428
  next[questionId] = questionResponses;
@@ -34703,6 +34583,256 @@ function interactionResponseValidationMessage(itemIdentifier, interaction, respo
34703
34583
  }
34704
34584
  return null;
34705
34585
  }
34586
+ function invalidResponse(message) {
34587
+ return { code: "INVALID_RESPONSE", message };
34588
+ }
34589
+ function responseValuesEqual(left, right) {
34590
+ if (left === undefined || right === undefined) {
34591
+ return left === right;
34592
+ }
34593
+ if (Array.isArray(left) || Array.isArray(right)) {
34594
+ return Array.isArray(left) && Array.isArray(right) && left.length === right.length && left.every((value, index) => value === right[index]);
34595
+ }
34596
+ return left === right;
34597
+ }
34598
+ function itemResponsesEqual(left, right) {
34599
+ const leftEntries = Object.entries(left ?? {});
34600
+ const rightEntries = Object.entries(right ?? {});
34601
+ return leftEntries.length === rightEntries.length && leftEntries.every(([identifier, value]) => responseValuesEqual(value, right?.[identifier]));
34602
+ }
34603
+ function prepareAssessmentItemSubmission(state, input) {
34604
+ const flow = assessmentFlowForPurpose(state.purpose);
34605
+ if (flow !== "item-submit") {
34606
+ return {
34607
+ action: "reject",
34608
+ failure: assessmentFlowViolation("Items cannot be submitted individually in this flow.", { flow })
34609
+ };
34610
+ }
34611
+ const priorSubmission = state.itemSubmissions.find((submission) => submission.submissionId === input.submissionId);
34612
+ if (priorSubmission) {
34613
+ const replayedItemResponses = applyAssessmentItemResponseUpdate(state.responses[input.itemIdentifier], input.responses);
34614
+ const sameRequest = priorSubmission.itemIdentifier === input.itemIdentifier && itemResponsesEqual(state.responses[input.itemIdentifier], replayedItemResponses);
34615
+ if (!sameRequest) {
34616
+ return {
34617
+ action: "reject",
34618
+ failure: assessmentFlowViolation("This item submission ID was already used for a different request.", {
34619
+ submissionId: input.submissionId,
34620
+ itemIdentifier: input.itemIdentifier,
34621
+ committedItemIdentifier: priorSubmission.itemIdentifier,
34622
+ flow
34623
+ })
34624
+ };
34625
+ }
34626
+ return {
34627
+ action: "replay",
34628
+ responseVersion: state.responseVersion,
34629
+ responses: state.responses,
34630
+ submission: priorSubmission
34631
+ };
34632
+ }
34633
+ if (state.responseVersion !== input.expectedResponseVersion) {
34634
+ return {
34635
+ action: "reject",
34636
+ failure: assessmentResponseVersionConflict(input.expectedResponseVersion, state.responseVersion)
34637
+ };
34638
+ }
34639
+ const committed = new Set(state.itemSubmissions.map((submission) => submission.itemIdentifier));
34640
+ const itemIndex = state.assessment.items.findIndex((item) => !committed.has(item.identifier));
34641
+ const nextItem = state.assessment.items[itemIndex];
34642
+ if (nextItem?.identifier !== input.itemIdentifier) {
34643
+ return {
34644
+ action: "reject",
34645
+ failure: assessmentFlowViolation("Items must be submitted once and in presentation order.", {
34646
+ itemIdentifier: input.itemIdentifier,
34647
+ expectedItemIdentifier: nextItem?.identifier ?? null,
34648
+ flow
34649
+ })
34650
+ };
34651
+ }
34652
+ const update = { [input.itemIdentifier]: input.responses };
34653
+ const updateMessage = assessmentResponseUpdateValidationMessage(state.assessment, update);
34654
+ if (updateMessage) {
34655
+ return { action: "reject", failure: invalidResponse(updateMessage) };
34656
+ }
34657
+ const responses = applyAssessmentResponseUpdate(state.responses, update);
34658
+ const validationMessage = assessmentResponseValidationMessage(state.assessment, responses);
34659
+ if (validationMessage) {
34660
+ return { action: "reject", failure: invalidResponse(validationMessage) };
34661
+ }
34662
+ return {
34663
+ action: "commit",
34664
+ itemIndex,
34665
+ responseVersion: state.responseVersion + 1,
34666
+ responses,
34667
+ answered: Object.keys(responses[input.itemIdentifier] ?? {}).length > 0
34668
+ };
34669
+ }
34670
+ function completeAssessmentItemSubmission(state, input, prepared, scoring) {
34671
+ const submission = {
34672
+ submissionId: input.submissionId,
34673
+ itemIdentifier: input.itemIdentifier,
34674
+ responseVersion: prepared.responseVersion,
34675
+ answered: prepared.answered,
34676
+ score: scoring.score,
34677
+ isCorrect: scoring.isCorrect
34678
+ };
34679
+ return {
34680
+ responseVersion: prepared.responseVersion,
34681
+ responses: prepared.responses,
34682
+ itemSubmissions: [...state.itemSubmissions, submission],
34683
+ submission
34684
+ };
34685
+ }
34686
+ function assessmentResponseUpdateFlowFailure(purpose, itemSubmissions, update) {
34687
+ const committed = new Set(itemSubmissions.map((submission) => submission.itemIdentifier));
34688
+ const committedUpdate = Object.keys(update).find((identifier) => committed.has(identifier));
34689
+ return committedUpdate ? assessmentFlowViolation("A submitted item response cannot be edited.", {
34690
+ itemIdentifier: committedUpdate,
34691
+ flow: assessmentFlowForPurpose(purpose)
34692
+ }) : null;
34693
+ }
34694
+ function assessmentCompletionFlowFailure(purpose, itemSubmissions, itemCount) {
34695
+ const flow = assessmentFlowForPurpose(purpose);
34696
+ return flow === "item-submit" && itemSubmissions.length !== itemCount ? assessmentFlowViolation("Every item must be submitted before the attempt can be completed.", {
34697
+ flow,
34698
+ submittedItemCount: itemSubmissions.length,
34699
+ itemCount
34700
+ }) : null;
34701
+ }
34702
+ function selectAssessmentCourseCandidate(candidates, subjectSpecified) {
34703
+ if (candidates.length === 0) {
34704
+ return { outcome: "none" };
34705
+ }
34706
+ const serviceable = candidates.filter((candidate) => candidate.hasLiveTests || candidate.hasOpenAttempt);
34707
+ if (serviceable.length === 0) {
34708
+ return { outcome: "no_eligible_tests" };
34709
+ }
34710
+ const withOpenAttempt = serviceable.filter((candidate) => candidate.hasOpenAttempt);
34711
+ const eligible = withOpenAttempt.length > 0 ? withOpenAttempt : serviceable;
34712
+ const subjects = new Set(eligible.map((candidate) => candidate.subject)).size;
34713
+ if (!subjectSpecified && subjects > 1) {
34714
+ return { outcome: "ambiguous", subjects };
34715
+ }
34716
+ const selected = [...eligible].toSorted((left, right) => left.grade - right.grade || left.id.localeCompare(right.id))[0];
34717
+ return { outcome: "selected", selected };
34718
+ }
34719
+ function assessmentAttemptTimestamp(value) {
34720
+ const parsed = Date.parse(value);
34721
+ return Number.isNaN(parsed) ? 0 : parsed;
34722
+ }
34723
+ function newestAttempt(left, right, field) {
34724
+ const dateDifference = assessmentAttemptTimestamp(right[field]) - assessmentAttemptTimestamp(left[field]);
34725
+ return dateDifference || left.attemptId.localeCompare(right.attemptId);
34726
+ }
34727
+ function compareNewestCompletedAssessmentAttempt(left, right) {
34728
+ return newestAttempt(left, right, "scoreDate");
34729
+ }
34730
+ function compareAssessmentCatalogOrder(left, right) {
34731
+ if (left.sortOrder !== null || right.sortOrder !== null) {
34732
+ if (left.sortOrder === null) {
34733
+ return 1;
34734
+ }
34735
+ if (right.sortOrder === null) {
34736
+ return -1;
34737
+ }
34738
+ const orderDifference = left.sortOrder - right.sortOrder;
34739
+ if (orderDifference) {
34740
+ return orderDifference;
34741
+ }
34742
+ }
34743
+ const leftUpdatedAt = Date.parse(left.updatedAt);
34744
+ const rightUpdatedAt = Date.parse(right.updatedAt);
34745
+ const leftHasValidDate = Number.isFinite(leftUpdatedAt);
34746
+ const rightHasValidDate = Number.isFinite(rightUpdatedAt);
34747
+ if (leftHasValidDate && rightHasValidDate) {
34748
+ const updatedDifference = leftUpdatedAt - rightUpdatedAt;
34749
+ if (updatedDifference) {
34750
+ return updatedDifference;
34751
+ }
34752
+ } else if (leftHasValidDate !== rightHasValidDate) {
34753
+ return leftHasValidDate ? -1 : 1;
34754
+ }
34755
+ return left.qtiTestIdentifier.localeCompare(right.qtiTestIdentifier);
34756
+ }
34757
+ function orderRuntimeAssessmentTests(tests) {
34758
+ return [...tests].toSorted(compareAssessmentCatalogOrder);
34759
+ }
34760
+ function selectRuntimeAssessment(liveTests, attempts) {
34761
+ const resumable = attempts.filter((attempt) => attempt.resumable !== false && isAssessmentAttemptOpen(attempt)).toSorted((left, right) => newestAttempt(left, right, "updatedAt"));
34762
+ if (resumable.length > 0) {
34763
+ return {
34764
+ kind: "resume",
34765
+ attempt: resumable[0],
34766
+ additionalResumableAttemptIds: resumable.slice(1).map((candidate) => candidate.attemptId)
34767
+ };
34768
+ }
34769
+ const orderedTests = orderRuntimeAssessmentTests(liveTests);
34770
+ if (orderedTests.length === 0) {
34771
+ return null;
34772
+ }
34773
+ const completed = attempts.filter(isAssessmentAttemptCompleted);
34774
+ const exposedTestIds = new Set(attempts.filter((attempt) => isAssessmentAttemptOpen(attempt) || isAssessmentAttemptCompleted(attempt)).map((attempt) => attempt.selectedTestIdentifier));
34775
+ const unattempted = orderedTests.find((test) => !exposedTestIds.has(test.qtiTestIdentifier));
34776
+ if (unattempted) {
34777
+ return { kind: "start", test: unattempted, reason: "unattempted" };
34778
+ }
34779
+ const latestCompleted = completed.toSorted(compareNewestCompletedAssessmentAttempt)[0];
34780
+ const latestTestIndex = latestCompleted ? orderedTests.findIndex((test) => test.qtiTestIdentifier === latestCompleted.selectedTestIdentifier) : -1;
34781
+ if (latestTestIndex === -1) {
34782
+ return { kind: "start", test: orderedTests[0], reason: "first_live" };
34783
+ }
34784
+ return {
34785
+ kind: "start",
34786
+ test: orderedTests[(latestTestIndex + 1) % orderedTests.length],
34787
+ reason: "round_robin"
34788
+ };
34789
+ }
34790
+ function presentationHash(value) {
34791
+ let hash = 2166136261;
34792
+ for (let index = 0;index < value.length; index++) {
34793
+ hash ^= value.charCodeAt(index);
34794
+ hash = Math.imul(hash, 16777619);
34795
+ }
34796
+ return hash >>> 0;
34797
+ }
34798
+ function comparePresentationText(left, right) {
34799
+ if (left < right) {
34800
+ return -1;
34801
+ }
34802
+ if (left > right) {
34803
+ return 1;
34804
+ }
34805
+ return 0;
34806
+ }
34807
+ function presentationChoices(choices, attemptId, itemIdentifier, domain) {
34808
+ const seed = `${attemptId}\x00${itemIdentifier}\x00${domain}\x00`;
34809
+ return [...choices].toSorted((left, right) => {
34810
+ const hashDifference = presentationHash(`${seed}${left.identifier}`) - presentationHash(`${seed}${right.identifier}`);
34811
+ return hashDifference || comparePresentationText(left.identifier, right.identifier);
34812
+ });
34813
+ }
34814
+ function assessmentPresentationForAttempt(assessment, attemptId) {
34815
+ const items = assessment.items.map((item) => {
34816
+ const interactions = item.interactions.map((interaction) => {
34817
+ if (interaction.type === "order" && interaction.shuffle) {
34818
+ return {
34819
+ ...interaction,
34820
+ choices: presentationChoices(interaction.choices, attemptId, item.identifier, `order\x00${interaction.responseIdentifier}`)
34821
+ };
34822
+ }
34823
+ if (interaction.type === "match" && interaction.shuffle) {
34824
+ return {
34825
+ ...interaction,
34826
+ sourceChoices: presentationChoices(interaction.sourceChoices, attemptId, item.identifier, `match-source\x00${interaction.responseIdentifier}`),
34827
+ targetChoices: presentationChoices(interaction.targetChoices, attemptId, item.identifier, `match-target\x00${interaction.responseIdentifier}`)
34828
+ };
34829
+ }
34830
+ return interaction;
34831
+ });
34832
+ return { ...item, interactions };
34833
+ });
34834
+ return { ...assessment, items };
34835
+ }
34706
34836
  function compareCodeUnits(left, right) {
34707
34837
  if (left < right) {
34708
34838
  return -1;
@@ -35094,6 +35224,9 @@ function isReviewFulfillment(value) {
35094
35224
  function isReviewItemOutcome(value) {
35095
35225
  return isRecord(value) && typeof value.itemIdentifier === "string" && value.itemIdentifier.length > 0 && isAssessmentStandardRef(value.standard) && typeof value.answered === "boolean" && isAssessmentScore(value.score) && (value.isCorrect === null || typeof value.isCorrect === "boolean");
35096
35226
  }
35227
+ function isAssessmentItemSubmission(value) {
35228
+ return isRecord(value) && typeof value.submissionId === "string" && value.submissionId.length > 0 && typeof value.itemIdentifier === "string" && value.itemIdentifier.length > 0 && Number.isInteger(value.responseVersion) && typeof value.answered === "boolean" && isAssessmentScore(value.score) && (value.isCorrect === null || typeof value.isCorrect === "boolean");
35229
+ }
35097
35230
  function isReviewAttemptMetadata(value) {
35098
35231
  return isRecord(value) && typeof value.requestFingerprint === "string" && typeof value.bankRevision === "string" && Array.isArray(value.standards) && value.standards.every(isAssessmentStandardRef) && Number.isInteger(value.itemsPerStandard) && Array.isArray(value.selections) && value.selections.every((selection) => isRecord(selection) && isAssessmentStandardRef(selection.standard) && typeof selection.itemIdentifier === "string") && isReviewFulfillment(value.fulfillment);
35099
35232
  }
@@ -35108,7 +35241,7 @@ function isPlaycademyAssessmentResultMetadataV1(value) {
35108
35241
  const selectedTest = metadata2.selectedTest;
35109
35242
  const mastery = metadata2.mastery;
35110
35243
  const review = metadata2.review;
35111
- 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) && typeof selectedTest?.identifier === "string" && typeof selectedTest.contentRevision === "string" && Number.isInteger(metadata2.attemptNumber) && Number.isInteger(metadata2.responseVersion) && Boolean(metadata2.responses) && typeof metadata2.responses === "object" && typeof metadata2.startedAt === "string" && typeof metadata2.updatedAt === "string" && (metadata2.purpose === "mastery" ? isMasteryAttemptMetadata(mastery) : mastery === undefined) && (metadata2.purpose === "review" ? isReviewAttemptMetadata(review) : review === undefined) && (metadata2.submissionId === undefined || typeof metadata2.submissionId === "string") && (metadata2.score === undefined || isAssessmentScore(metadata2.score)) && (metadata2.completion === undefined || isRecord(metadata2.completion) && typeof metadata2.completion.testName === "string" && Number.isInteger(metadata2.completion.correctQuestions) && (metadata2.completion.totalQuestions === undefined || Number.isInteger(metadata2.completion.totalQuestions) && metadata2.completion.totalQuestions >= 0) && (metadata2.completion.itemOutcomes === undefined || metadata2.purpose === "review" && Array.isArray(metadata2.completion.itemOutcomes) && metadata2.completion.itemOutcomes.every(isReviewItemOutcome)));
35244
+ 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) && 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.purpose === "mastery" ? isMasteryAttemptMetadata(mastery) : mastery === undefined) && (metadata2.purpose === "review" ? isReviewAttemptMetadata(review) : review === undefined) && (metadata2.submissionId === undefined || typeof metadata2.submissionId === "string") && (metadata2.score === undefined || isAssessmentScore(metadata2.score)) && (metadata2.completion === undefined || isRecord(metadata2.completion) && typeof metadata2.completion.testName === "string" && Number.isInteger(metadata2.completion.correctQuestions) && (metadata2.completion.totalQuestions === undefined || Number.isInteger(metadata2.completion.totalQuestions) && metadata2.completion.totalQuestions >= 0) && (metadata2.completion.itemOutcomes === undefined || metadata2.purpose === "review" && Array.isArray(metadata2.completion.itemOutcomes) && metadata2.completion.itemOutcomes.every(isReviewItemOutcome)));
35112
35245
  }
35113
35246
  function isPlaycademyAssessmentItemResultMetadataV1(value) {
35114
35247
  if (!isRecord(value) || !isRecord(value.responses)) {
@@ -35125,6 +35258,9 @@ function playcademyAssessmentResultMetadata(value) {
35125
35258
  return null;
35126
35259
  }
35127
35260
  let normalized = metadata2.responses === undefined ? { ...metadata2, responses: {} } : metadata2;
35261
+ if (normalized.itemSubmissions === undefined) {
35262
+ normalized = { ...normalized, itemSubmissions: [] };
35263
+ }
35128
35264
  const review = normalized.review;
35129
35265
  if (isRecord(review) && isRecord(review.fulfillment) && review.fulfillment.status === "complete" && review.fulfillment.shortages === undefined) {
35130
35266
  normalized = {
@@ -35206,9 +35342,11 @@ var LatestAssessmentFiltersSchema;
35206
35342
  var LatestRuntimeAssessmentQuerySchema;
35207
35343
  var StartAssessmentBodySchema;
35208
35344
  var SaveAssessmentBodySchema;
35345
+ var SubmitAssessmentItemBodySchema;
35209
35346
  var SubmitAssessmentBodySchema;
35210
35347
  var StartRuntimeAssessmentRequestSchema;
35211
35348
  var SaveRuntimeAssessmentRequestSchema;
35349
+ var SubmitAssessmentItemRuntimeRequestSchema;
35212
35350
  var SubmitRuntimeAssessmentRequestSchema;
35213
35351
  var init_assessment_runtime = __esm(() => {
35214
35352
  init_timeback3();
@@ -35397,7 +35535,8 @@ var init_assessment_runtime = __esm(() => {
35397
35535
  RESPONSE_VERSION_CONFLICT: 409,
35398
35536
  INVALID_RESPONSE: 422,
35399
35537
  UNAUTHORIZED_ATTEMPT: 403,
35400
- ATTEMPT_ALREADY_SUBMITTED: 409
35538
+ ATTEMPT_ALREADY_SUBMITTED: 409,
35539
+ ASSESSMENT_FLOW_VIOLATION: 409
35401
35540
  };
35402
35541
  GRADE_VALUES = TIMEBACK_GRADES;
35403
35542
  POINT_RESPONSE_PATTERN = /^-?\d+(?:\.\d+)? -?\d+(?:\.\d+)?$/;
@@ -35468,12 +35607,19 @@ var init_assessment_runtime = __esm(() => {
35468
35607
  expectedResponseVersion: exports_external.number().int().nonnegative(),
35469
35608
  responses: exports_external.record(ResponseKeySchema, exports_external.record(ResponseKeySchema, AssessmentResponseValueSchema.nullable()))
35470
35609
  });
35610
+ SubmitAssessmentItemBodySchema = exports_external.object({
35611
+ expectedResponseVersion: exports_external.number().int().nonnegative(),
35612
+ submissionId: exports_external.string().trim().min(1),
35613
+ itemIdentifier: ResponseKeySchema,
35614
+ responses: exports_external.record(ResponseKeySchema, AssessmentResponseValueSchema.nullable())
35615
+ });
35471
35616
  SubmitAssessmentBodySchema = exports_external.object({
35472
35617
  expectedResponseVersion: exports_external.number().int().nonnegative(),
35473
35618
  submissionId: exports_external.string().trim().min(1)
35474
35619
  });
35475
35620
  StartRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.and(StartAssessmentBodySchema);
35476
35621
  SaveRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.extend(SaveAssessmentBodySchema.shape);
35622
+ SubmitAssessmentItemRuntimeRequestSchema = AssessmentRuntimeIdentitySchema.extend(SubmitAssessmentItemBodySchema.shape);
35477
35623
  SubmitRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.extend(SubmitAssessmentBodySchema.shape).extend({
35478
35624
  appName: exports_external.string().trim().min(1),
35479
35625
  sensorUrl: exports_external.string().url()
@@ -117029,6 +117175,9 @@ class TimebackAssessmentRuntimeService {
117029
117175
  static ASSESSMENT_CACHE_LIMIT = 32;
117030
117176
  static ASSESSMENT_CACHE_TTL_MS = 60000;
117031
117177
  static EXPORT_CONCURRENCY = 4;
117178
+ static ITEM_SUBMISSION_MAX_PREPARATION_ATTEMPTS = 3;
117179
+ static PROJECTION_BARRIER_MAX_ATTEMPTS = 5;
117180
+ static PROJECTION_BARRIER_RETRY_DELAY_MS = 25;
117032
117181
  static SCORING_CONCURRENCY = 4;
117033
117182
  deps;
117034
117183
  assessmentCache = new TimebackCache({
@@ -117121,6 +117270,7 @@ class TimebackAssessmentRuntimeService {
117121
117270
  attemptNumber,
117122
117271
  responseVersion: 0,
117123
117272
  responses: {},
117273
+ itemSubmissions: [],
117124
117274
  startedAt: timestamp6,
117125
117275
  updatedAt: timestamp6
117126
117276
  };
@@ -117246,6 +117396,7 @@ class TimebackAssessmentRuntimeService {
117246
117396
  attemptNumber,
117247
117397
  responseVersion: 0,
117248
117398
  responses: {},
117399
+ itemSubmissions: [],
117249
117400
  startedAt: timestamp6,
117250
117401
  updatedAt: timestamp6,
117251
117402
  review: {
@@ -117370,6 +117521,10 @@ class TimebackAssessmentRuntimeService {
117370
117521
  this.assertInProgress(attempt.result);
117371
117522
  this.assertResponseVersion(attempt.metadata, input.expectedResponseVersion);
117372
117523
  const assessment = await this.loadAttemptAssessment(attempt.metadata);
117524
+ const flowFailure = assessmentResponseUpdateFlowFailure(attempt.metadata.purpose, attempt.metadata.itemSubmissions, input.responses);
117525
+ if (flowFailure) {
117526
+ throw AssessmentRuntimeError.from(flowFailure);
117527
+ }
117373
117528
  validateAssessmentResponseUpdate(assessment, input.responses);
117374
117529
  const responses = applyAssessmentResponseUpdate(attempt.metadata.responses, input.responses);
117375
117530
  validateAssessmentResponses(assessment, responses);
@@ -117391,11 +117546,105 @@ class TimebackAssessmentRuntimeService {
117391
117546
  };
117392
117547
  });
117393
117548
  }
117394
- async scoreSubmission(attempt) {
117549
+ async submitItem({
117550
+ input,
117551
+ ...params
117552
+ }) {
117553
+ await this.deps.validateDeveloperAccess(params.user, params.gameId);
117554
+ let preview = await this.prepareItemSubmission(params, input);
117555
+ let preparationAttempts = 1;
117556
+ while (true) {
117557
+ const committed = await this.deps.assessmentRuntimeLock(this.deps.db, assessmentRuntimeAttemptLockKey(params.attemptId), async (db2) => {
117558
+ const attempt = await this.authorizeAttempt(params, {
117559
+ db: db2,
117560
+ developerAccessValidated: true
117561
+ });
117562
+ this.assertInProgress(attempt.result);
117563
+ if (!preview || preview.responseVersion !== attempt.metadata.responseVersion || preview.submissionId !== input.submissionId || preview.itemIdentifier !== input.itemIdentifier) {
117564
+ if (preparationAttempts >= TimebackAssessmentRuntimeService.ITEM_SUBMISSION_MAX_PREPARATION_ATTEMPTS) {
117565
+ if (input.expectedResponseVersion === attempt.metadata.responseVersion) {
117566
+ throw new ServiceUnavailableError("The assessment response state changed repeatedly while this item was being prepared. Try again shortly.", {
117567
+ retryable: true,
117568
+ reason: "PREPARATION_CONTENTION",
117569
+ expectedResponseVersion: input.expectedResponseVersion,
117570
+ responseVersion: attempt.metadata.responseVersion
117571
+ });
117572
+ }
117573
+ throw AssessmentRuntimeError.from(assessmentResponseVersionConflict(input.expectedResponseVersion, attempt.metadata.responseVersion));
117574
+ }
117575
+ return { action: "prepare" };
117576
+ }
117577
+ const transition = prepareAssessmentItemSubmission({
117578
+ purpose: attempt.metadata.purpose,
117579
+ responseVersion: attempt.metadata.responseVersion,
117580
+ responses: attempt.metadata.responses,
117581
+ itemSubmissions: attempt.metadata.itemSubmissions,
117582
+ assessment: preview.assessment
117583
+ }, input);
117584
+ if (transition.action === "reject") {
117585
+ throw AssessmentRuntimeError.from(transition.failure);
117586
+ }
117587
+ if (transition.action === "replay") {
117588
+ return {
117589
+ action: "complete",
117590
+ response: {
117591
+ attemptId: attempt.result.sourcedId,
117592
+ responseVersion: transition.responseVersion,
117593
+ status: "in_progress",
117594
+ responses: transition.responses,
117595
+ itemSubmissions: attempt.metadata.itemSubmissions,
117596
+ submission: transition.submission
117597
+ },
117598
+ projection: attempt.metadata.purpose === "review" ? {
117599
+ result: attempt.result,
117600
+ metadata: attempt.metadata,
117601
+ itemIdentifier: input.itemIdentifier
117602
+ } : null
117603
+ };
117604
+ }
117605
+ if (!preview.scoring) {
117606
+ throw new Error("Prepared item submission is missing its score");
117607
+ }
117608
+ const completed = completeAssessmentItemSubmission(attempt.metadata, input, transition, preview.scoring);
117609
+ const metadata2 = {
117610
+ ...attempt.metadata,
117611
+ responses: completed.responses,
117612
+ responseVersion: completed.responseVersion,
117613
+ itemSubmissions: completed.itemSubmissions,
117614
+ updatedAt: new Date().toISOString()
117615
+ };
117616
+ await this.putResultMetadata(attempt.result, metadata2);
117617
+ return {
117618
+ action: "complete",
117619
+ response: {
117620
+ attemptId: attempt.result.sourcedId,
117621
+ responseVersion: completed.responseVersion,
117622
+ status: "in_progress",
117623
+ responses: completed.responses,
117624
+ itemSubmissions: completed.itemSubmissions,
117625
+ submission: completed.submission
117626
+ },
117627
+ projection: metadata2.purpose === "review" ? {
117628
+ result: attempt.result,
117629
+ metadata: metadata2,
117630
+ itemIdentifier: input.itemIdentifier
117631
+ } : null
117632
+ };
117633
+ });
117634
+ if (committed.action === "prepare") {
117635
+ preparationAttempts += 1;
117636
+ preview = await this.scoreItemSubmission(params, input);
117637
+ } else {
117638
+ if (committed.projection) {
117639
+ await this.projectReviewItemResponse(params, committed.projection);
117640
+ }
117641
+ return committed.response;
117642
+ }
117643
+ }
117644
+ }
117645
+ async scoreItems(assessment, responses, itemIdentifiers) {
117395
117646
  const client2 = this.requireClient();
117396
- const assessment = await this.loadAttemptAssessment(attempt.metadata);
117397
- validateAssessmentResponses(assessment, attempt.metadata.responses);
117398
- const scoringItems = assessmentScoringRequests(assessment, attempt.metadata.responses);
117647
+ const scoringItems = assessmentScoringRequests(assessment, responses).filter((item) => !itemIdentifiers || itemIdentifiers.has(item.itemIdentifier));
117399
117648
  const scoringTasks = scoringItems.flatMap((item, itemIndex) => item.requests.map((request) => ({
117400
117649
  itemIndex,
117401
117650
  itemIdentifier: item.itemIdentifier,
@@ -117409,7 +117658,7 @@ class TimebackAssessmentRuntimeService {
117409
117658
  scoresByItem[task.itemIndex].push(Number.isFinite(processed.score) ? processed.score : 0);
117410
117659
  verdictsByItem[task.itemIndex].push(processed.isCorrect);
117411
117660
  });
117412
- const itemResults = scoringItems.map((item, itemIndex) => {
117661
+ return scoringItems.map((item, itemIndex) => {
117413
117662
  const earned = assessmentItemEarnedScore(scoresByItem[itemIndex], item.maxScore);
117414
117663
  const possible = item.maxScore;
117415
117664
  return {
@@ -117422,6 +117671,52 @@ class TimebackAssessmentRuntimeService {
117422
117671
  isCorrect: assessmentItemCorrectness(verdictsByItem[itemIndex])
117423
117672
  };
117424
117673
  });
117674
+ }
117675
+ async scoreItem(assessment, responses, itemIdentifier) {
117676
+ const [scoring] = await this.scoreItems(assessment, responses, new Set([itemIdentifier]));
117677
+ return scoring;
117678
+ }
117679
+ async prepareItemSubmission(params, input) {
117680
+ try {
117681
+ return await this.scoreItemSubmission(params, input);
117682
+ } catch (error88) {
117683
+ addEvent("assessment.item_submission_preview_failed", {
117684
+ "app.assessment.attempt_id": params.attemptId,
117685
+ "exception.type": errorType(error88),
117686
+ "app.error.message": errorMessage2(error88)
117687
+ });
117688
+ return null;
117689
+ }
117690
+ }
117691
+ async scoreItemSubmission(params, input) {
117692
+ const attempt = await this.peekAttempt(params);
117693
+ if (!isAssessmentAttemptOpen({
117694
+ inProgress: attempt.result.inProgress ?? "",
117695
+ scoreStatus: attempt.result.scoreStatus
117696
+ })) {
117697
+ return null;
117698
+ }
117699
+ const assessment = await this.loadAttemptAssessment(attempt.metadata);
117700
+ const transition = prepareAssessmentItemSubmission({
117701
+ purpose: attempt.metadata.purpose,
117702
+ responseVersion: attempt.metadata.responseVersion,
117703
+ responses: attempt.metadata.responses,
117704
+ itemSubmissions: attempt.metadata.itemSubmissions,
117705
+ assessment
117706
+ }, input);
117707
+ const scoring = transition.action === "commit" ? await this.scoreItem(assessment, transition.responses, input.itemIdentifier) : null;
117708
+ return {
117709
+ responseVersion: attempt.metadata.responseVersion,
117710
+ submissionId: input.submissionId,
117711
+ itemIdentifier: input.itemIdentifier,
117712
+ assessment,
117713
+ scoring
117714
+ };
117715
+ }
117716
+ async scoreSubmission(attempt) {
117717
+ const assessment = await this.loadAttemptAssessment(attempt.metadata);
117718
+ validateAssessmentResponses(assessment, attempt.metadata.responses);
117719
+ const itemResults = await this.scoreItems(assessment, attempt.metadata.responses);
117425
117720
  return {
117426
117721
  responseVersion: attempt.metadata.responseVersion,
117427
117722
  assessment,
@@ -117491,6 +117786,13 @@ class TimebackAssessmentRuntimeService {
117491
117786
  throw AssessmentRuntimeError.from(assessmentAlreadySubmitted(attempt.result.sourcedId));
117492
117787
  }
117493
117788
  this.assertResponseVersion(attempt.metadata, input.expectedResponseVersion);
117789
+ if (assessmentFlowForPurpose(attempt.metadata.purpose) === "item-submit") {
117790
+ const assessment2 = await this.loadAttemptAssessment(attempt.metadata);
117791
+ const flowFailure = assessmentCompletionFlowFailure(attempt.metadata.purpose, attempt.metadata.itemSubmissions, assessment2.items.length);
117792
+ if (flowFailure) {
117793
+ throw AssessmentRuntimeError.from(flowFailure);
117794
+ }
117795
+ }
117494
117796
  const scored = preview && preview.responseVersion === attempt.metadata.responseVersion ? preview : await this.scoreSubmission(attempt);
117495
117797
  const { assessment, itemResults, score, correctQuestions } = scored;
117496
117798
  const client2 = this.requireClient();
@@ -118133,18 +118435,105 @@ class TimebackAssessmentRuntimeService {
118133
118435
  }
118134
118436
  async putReviewChildResponses(result, metadata2, changedItemIdentifiers) {
118135
118437
  await runWithConcurrency(metadata2.review.selections.filter((selection) => changedItemIdentifiers.has(selection.itemIdentifier)), TimebackAssessmentRuntimeService.SCORING_CONCURRENCY, async (selection) => {
118136
- const childLineItemId = await reviewQuestionLineItemId(result.assessmentLineItem.sourcedId, selection.itemIdentifier);
118137
- const childResultId = await reviewQuestionResultId(result.sourcedId, childLineItemId);
118138
- const childMetadata = buildReviewItemResultMetadata({
118139
- metadata: metadata2,
118140
- selection,
118141
- parentAttemptId: result.sourcedId
118438
+ try {
118439
+ const childLineItemId = await reviewQuestionLineItemId(result.assessmentLineItem.sourcedId, selection.itemIdentifier);
118440
+ const childResultId = await reviewQuestionResultId(result.sourcedId, childLineItemId);
118441
+ const childMetadata = buildReviewItemResultMetadata({
118442
+ metadata: metadata2,
118443
+ selection,
118444
+ parentAttemptId: result.sourcedId
118445
+ });
118446
+ await this.requireClient().api.oneroster.assessmentResults.upsert(childResultId, buildOpenReviewChildResult({
118447
+ childLineItemId,
118448
+ student: result.student,
118449
+ metadata: childMetadata
118450
+ }));
118451
+ } catch (error88) {
118452
+ addEvent("assessment.review_child_response_projection_failed", {
118453
+ "app.assessment.attempt_id": result.sourcedId,
118454
+ "app.assessment.qti_item_identifier": selection.itemIdentifier,
118455
+ "exception.type": errorType(error88),
118456
+ "app.error.message": errorMessage2(error88)
118457
+ });
118458
+ }
118459
+ });
118460
+ }
118461
+ async projectReviewItemResponse(params, projection) {
118462
+ await this.putReviewChildResponses(projection.result, projection.metadata, new Set([projection.itemIdentifier]));
118463
+ try {
118464
+ await this.crossAttemptLockBarrier(params.attemptId);
118465
+ const latest = await this.peekAttempt(params);
118466
+ if (isAssessmentAttemptCompleted({
118467
+ inProgress: latest.result.inProgress ?? "",
118468
+ scoreStatus: latest.result.scoreStatus
118469
+ }) && latest.metadata.purpose === "review") {
118470
+ await this.restoreCompletedReviewChildResponses(latest.result, latest.metadata, new Set([projection.itemIdentifier]));
118471
+ }
118472
+ } catch (error88) {
118473
+ addEvent("assessment.review_child_projection_barrier_failed", {
118474
+ "app.assessment.attempt_id": params.attemptId,
118475
+ "app.assessment.qti_item_identifier": projection.itemIdentifier,
118476
+ "exception.type": errorType(error88),
118477
+ "app.error.message": errorMessage2(error88)
118142
118478
  });
118143
- await this.requireClient().api.oneroster.assessmentResults.upsert(childResultId, buildOpenReviewChildResult({
118144
- childLineItemId,
118145
- student: result.student,
118146
- metadata: childMetadata
118147
- }));
118479
+ }
118480
+ }
118481
+ async crossAttemptLockBarrier(attemptId) {
118482
+ for (let attempt = 1;attempt <= TimebackAssessmentRuntimeService.PROJECTION_BARRIER_MAX_ATTEMPTS; attempt += 1) {
118483
+ try {
118484
+ await this.deps.assessmentRuntimeLock(this.deps.db, assessmentRuntimeAttemptLockKey(attemptId), async () => {
118485
+ return;
118486
+ });
118487
+ return;
118488
+ } catch (error88) {
118489
+ if (!(error88 instanceof ServiceUnavailableError) || attempt === TimebackAssessmentRuntimeService.PROJECTION_BARRIER_MAX_ATTEMPTS) {
118490
+ throw error88;
118491
+ }
118492
+ await sleep(TimebackAssessmentRuntimeService.PROJECTION_BARRIER_RETRY_DELAY_MS);
118493
+ }
118494
+ }
118495
+ }
118496
+ async restoreCompletedReviewChildResponses(result, metadata2, changedItemIdentifiers) {
118497
+ const outcomes = metadata2.completion?.itemOutcomes;
118498
+ const submissionId = metadata2.submissionId;
118499
+ if (!submissionId || !outcomes) {
118500
+ addEvent("assessment.review_child_terminal_restore_unavailable", {
118501
+ "app.assessment.attempt_id": result.sourcedId
118502
+ });
118503
+ return;
118504
+ }
118505
+ const outcomeByItem = new Map(outcomes.map((outcome) => [outcome.itemIdentifier, outcome]));
118506
+ await runWithConcurrency(metadata2.review.selections.filter((selection) => changedItemIdentifiers.has(selection.itemIdentifier)), TimebackAssessmentRuntimeService.SCORING_CONCURRENCY, async (selection) => {
118507
+ try {
118508
+ const outcome = outcomeByItem.get(selection.itemIdentifier);
118509
+ if (!outcome) {
118510
+ throw new Error(`Completed review omitted item ${selection.itemIdentifier}`);
118511
+ }
118512
+ const childLineItemId = await reviewQuestionLineItemId(result.assessmentLineItem.sourcedId, selection.itemIdentifier);
118513
+ const childResultId = await reviewQuestionResultId(result.sourcedId, childLineItemId);
118514
+ const finalized = buildFinalizedReviewChildResult({
118515
+ childLineItemId,
118516
+ student: result.student,
118517
+ parentAttemptId: result.sourcedId,
118518
+ metadata: metadata2,
118519
+ selection,
118520
+ itemResult: {
118521
+ itemIdentifier: outcome.itemIdentifier,
118522
+ score: outcome.score,
118523
+ isCorrect: outcome.isCorrect
118524
+ },
118525
+ submissionId,
118526
+ timestamp: result.scoreDate
118527
+ });
118528
+ await this.requireClient().api.oneroster.assessmentResults.upsert(childResultId, finalized.resultUpdate);
118529
+ } catch (error88) {
118530
+ addEvent("assessment.review_child_terminal_restore_failed", {
118531
+ "app.assessment.attempt_id": result.sourcedId,
118532
+ "app.assessment.qti_item_identifier": selection.itemIdentifier,
118533
+ "exception.type": errorType(error88),
118534
+ "app.error.message": errorMessage2(error88)
118535
+ });
118536
+ }
118148
118537
  });
118149
118538
  }
118150
118539
  async finalizeReviewChildResults(input) {
@@ -118225,8 +118614,10 @@ class TimebackAssessmentRuntimeService {
118225
118614
  attemptId: result.sourcedId,
118226
118615
  responseVersion: metadata2.responseVersion,
118227
118616
  status: completed ? "completed" : "in_progress",
118617
+ flow: assessmentFlowForPurpose(metadata2.purpose),
118228
118618
  assessment: assessmentPresentationForAttempt(assessment, result.sourcedId),
118229
118619
  responses: metadata2.responses,
118620
+ itemSubmissions: metadata2.itemSubmissions,
118230
118621
  score: completed ? this.scoreFromResult(result, assessmentPossibleScore(assessment), metadata2) : null,
118231
118622
  selection
118232
118623
  };
@@ -182545,6 +182936,7 @@ var startRuntimeAssessment;
182545
182936
  var getRuntimeAssessment;
182546
182937
  var getLatestRuntimeAssessment;
182547
182938
  var saveRuntimeAssessment;
182939
+ var submitRuntimeAssessmentItem;
182548
182940
  var submitRuntimeAssessment;
182549
182941
  var exportRuntimeAssessmentFixture;
182550
182942
  var getStudentXp;
@@ -182888,6 +183280,25 @@ var init_timeback_controller = __esm(() => {
182888
183280
  user: ctx.user
182889
183281
  });
182890
183282
  });
183283
+ submitRuntimeAssessmentItem = requireDeveloper(async (ctx) => {
183284
+ const attemptId = ctx.params.attemptId;
183285
+ const body2 = await parseRequestBody(ctx.request, SubmitAssessmentItemRuntimeRequestSchema);
183286
+ if (!attemptId) {
183287
+ throw ApiError.badRequest("Missing attemptId");
183288
+ }
183289
+ return ctx.services.timebackAssessmentRuntime.submitItem({
183290
+ gameId: body2.gameId,
183291
+ studentId: body2.studentId,
183292
+ attemptId,
183293
+ input: {
183294
+ expectedResponseVersion: body2.expectedResponseVersion,
183295
+ submissionId: body2.submissionId,
183296
+ itemIdentifier: body2.itemIdentifier,
183297
+ responses: body2.responses
183298
+ },
183299
+ user: ctx.user
183300
+ });
183301
+ });
182891
183302
  submitRuntimeAssessment = requireDeveloper(async (ctx) => {
182892
183303
  const attemptId = ctx.params.attemptId;
182893
183304
  const body2 = await parseRequestBody(ctx.request, SubmitRuntimeAssessmentRequestSchema);
@@ -183339,6 +183750,7 @@ var init_timeback_controller = __esm(() => {
183339
183750
  getLatestRuntimeAssessment,
183340
183751
  getRuntimeAssessment,
183341
183752
  saveRuntimeAssessment,
183753
+ submitRuntimeAssessmentItem,
183342
183754
  submitRuntimeAssessment,
183343
183755
  exportRuntimeAssessmentFixture,
183344
183756
  getStudentXp,
@@ -184288,6 +184700,7 @@ var init_timeback8 = __esm(async () => {
184288
184700
  timebackRouter.get("/assessments/latest", handle2(timeback2.getLatestRuntimeAssessment));
184289
184701
  timebackRouter.get("/assessments/:attemptId", handle2(timeback2.getRuntimeAssessment));
184290
184702
  timebackRouter.post("/assessments/:attemptId/save", handle2(timeback2.saveRuntimeAssessment));
184703
+ timebackRouter.post("/assessments/:attemptId/submit-item", handle2(timeback2.submitRuntimeAssessmentItem));
184291
184704
  timebackRouter.post("/assessments/:attemptId/submit", handle2(timeback2.submitRuntimeAssessment));
184292
184705
  timebackRouter.get("/user", async (c2) => {
184293
184706
  const user = c2.get("user");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcademy/vite-plugin",
3
- "version": "1.2.1-beta.10",
3
+ "version": "1.2.1-beta.12",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -19,14 +19,14 @@
19
19
  "dependencies": {
20
20
  "archiver": "^7.0.1",
21
21
  "picocolors": "^1.1.1",
22
- "playcademy": "0.28.1-beta.10"
22
+ "playcademy": "0.28.1-beta.12"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@electric-sql/pglite": "^0.3.16",
26
26
  "@inquirer/prompts": "^7.8.6",
27
27
  "@playcademy/constants": "0.0.1",
28
- "@playcademy/sandbox": "0.7.1-beta.10",
29
- "@playcademy/sdk": "0.16.1-beta.10",
28
+ "@playcademy/sandbox": "0.7.1-beta.12",
29
+ "@playcademy/sdk": "0.16.1-beta.12",
30
30
  "@playcademy/types": "0.0.1",
31
31
  "@playcademy/utils": "0.0.1",
32
32
  "@types/archiver": "^6.0.3",