@playcademy/sandbox 0.7.1-beta.10 → 0.7.1-beta.11
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +573 -163
- package/dist/server.js +573 -163
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1123,7 +1123,7 @@ var package_default;
|
|
|
1123
1123
|
var init_package = __esm(() => {
|
|
1124
1124
|
package_default = {
|
|
1125
1125
|
name: "@playcademy/sandbox",
|
|
1126
|
-
version: "0.7.1-beta.
|
|
1126
|
+
version: "0.7.1-beta.11",
|
|
1127
1127
|
description: "Local development server for Playcademy game development",
|
|
1128
1128
|
type: "module",
|
|
1129
1129
|
exports: {
|
|
@@ -9424,6 +9424,13 @@ function classifyAssessmentSubmission(attempt, submissionId) {
|
|
|
9424
9424
|
}
|
|
9425
9425
|
return isAssessmentAttemptOpen(attempt) ? "submit" : "reject";
|
|
9426
9426
|
}
|
|
9427
|
+
function assessmentFlowViolation(message, details) {
|
|
9428
|
+
return {
|
|
9429
|
+
code: "ASSESSMENT_FLOW_VIOLATION",
|
|
9430
|
+
message,
|
|
9431
|
+
...details ? { details } : {}
|
|
9432
|
+
};
|
|
9433
|
+
}
|
|
9427
9434
|
function assessmentAttemptUnauthorized(attemptId) {
|
|
9428
9435
|
return {
|
|
9429
9436
|
code: "UNAUTHORIZED_ATTEMPT",
|
|
@@ -9465,139 +9472,8 @@ function assessmentReviewBankShortage(fulfillment) {
|
|
|
9465
9472
|
details: { fulfillment }
|
|
9466
9473
|
};
|
|
9467
9474
|
}
|
|
9468
|
-
function
|
|
9469
|
-
|
|
9470
|
-
return { outcome: "none" };
|
|
9471
|
-
}
|
|
9472
|
-
const serviceable = candidates.filter((candidate) => candidate.hasLiveTests || candidate.hasOpenAttempt);
|
|
9473
|
-
if (serviceable.length === 0) {
|
|
9474
|
-
return { outcome: "no_eligible_tests" };
|
|
9475
|
-
}
|
|
9476
|
-
const withOpenAttempt = serviceable.filter((candidate) => candidate.hasOpenAttempt);
|
|
9477
|
-
const eligible = withOpenAttempt.length > 0 ? withOpenAttempt : serviceable;
|
|
9478
|
-
const subjects = new Set(eligible.map((candidate) => candidate.subject)).size;
|
|
9479
|
-
if (!subjectSpecified && subjects > 1) {
|
|
9480
|
-
return { outcome: "ambiguous", subjects };
|
|
9481
|
-
}
|
|
9482
|
-
const selected = [...eligible].toSorted((left, right) => left.grade - right.grade || left.id.localeCompare(right.id))[0];
|
|
9483
|
-
return { outcome: "selected", selected };
|
|
9484
|
-
}
|
|
9485
|
-
function assessmentAttemptTimestamp(value) {
|
|
9486
|
-
const parsed = Date.parse(value);
|
|
9487
|
-
return Number.isNaN(parsed) ? 0 : parsed;
|
|
9488
|
-
}
|
|
9489
|
-
function newestAttempt(left, right, field) {
|
|
9490
|
-
const dateDifference = assessmentAttemptTimestamp(right[field]) - assessmentAttemptTimestamp(left[field]);
|
|
9491
|
-
return dateDifference || left.attemptId.localeCompare(right.attemptId);
|
|
9492
|
-
}
|
|
9493
|
-
function compareNewestCompletedAssessmentAttempt(left, right) {
|
|
9494
|
-
return newestAttempt(left, right, "scoreDate");
|
|
9495
|
-
}
|
|
9496
|
-
function compareAssessmentCatalogOrder(left, right) {
|
|
9497
|
-
if (left.sortOrder !== null || right.sortOrder !== null) {
|
|
9498
|
-
if (left.sortOrder === null) {
|
|
9499
|
-
return 1;
|
|
9500
|
-
}
|
|
9501
|
-
if (right.sortOrder === null) {
|
|
9502
|
-
return -1;
|
|
9503
|
-
}
|
|
9504
|
-
const orderDifference = left.sortOrder - right.sortOrder;
|
|
9505
|
-
if (orderDifference) {
|
|
9506
|
-
return orderDifference;
|
|
9507
|
-
}
|
|
9508
|
-
}
|
|
9509
|
-
const leftUpdatedAt = Date.parse(left.updatedAt);
|
|
9510
|
-
const rightUpdatedAt = Date.parse(right.updatedAt);
|
|
9511
|
-
const leftHasValidDate = Number.isFinite(leftUpdatedAt);
|
|
9512
|
-
const rightHasValidDate = Number.isFinite(rightUpdatedAt);
|
|
9513
|
-
if (leftHasValidDate && rightHasValidDate) {
|
|
9514
|
-
const updatedDifference = leftUpdatedAt - rightUpdatedAt;
|
|
9515
|
-
if (updatedDifference) {
|
|
9516
|
-
return updatedDifference;
|
|
9517
|
-
}
|
|
9518
|
-
} else if (leftHasValidDate !== rightHasValidDate) {
|
|
9519
|
-
return leftHasValidDate ? -1 : 1;
|
|
9520
|
-
}
|
|
9521
|
-
return left.qtiTestIdentifier.localeCompare(right.qtiTestIdentifier);
|
|
9522
|
-
}
|
|
9523
|
-
function orderRuntimeAssessmentTests(tests) {
|
|
9524
|
-
return [...tests].toSorted(compareAssessmentCatalogOrder);
|
|
9525
|
-
}
|
|
9526
|
-
function selectRuntimeAssessment(liveTests, attempts) {
|
|
9527
|
-
const resumable = attempts.filter((attempt) => attempt.resumable !== false && isAssessmentAttemptOpen(attempt)).toSorted((left, right) => newestAttempt(left, right, "updatedAt"));
|
|
9528
|
-
if (resumable.length > 0) {
|
|
9529
|
-
return {
|
|
9530
|
-
kind: "resume",
|
|
9531
|
-
attempt: resumable[0],
|
|
9532
|
-
additionalResumableAttemptIds: resumable.slice(1).map((candidate) => candidate.attemptId)
|
|
9533
|
-
};
|
|
9534
|
-
}
|
|
9535
|
-
const orderedTests = orderRuntimeAssessmentTests(liveTests);
|
|
9536
|
-
if (orderedTests.length === 0) {
|
|
9537
|
-
return null;
|
|
9538
|
-
}
|
|
9539
|
-
const completed = attempts.filter(isAssessmentAttemptCompleted);
|
|
9540
|
-
const exposedTestIds = new Set(attempts.filter((attempt) => isAssessmentAttemptOpen(attempt) || isAssessmentAttemptCompleted(attempt)).map((attempt) => attempt.selectedTestIdentifier));
|
|
9541
|
-
const unattempted = orderedTests.find((test) => !exposedTestIds.has(test.qtiTestIdentifier));
|
|
9542
|
-
if (unattempted) {
|
|
9543
|
-
return { kind: "start", test: unattempted, reason: "unattempted" };
|
|
9544
|
-
}
|
|
9545
|
-
const latestCompleted = completed.toSorted(compareNewestCompletedAssessmentAttempt)[0];
|
|
9546
|
-
const latestTestIndex = latestCompleted ? orderedTests.findIndex((test) => test.qtiTestIdentifier === latestCompleted.selectedTestIdentifier) : -1;
|
|
9547
|
-
if (latestTestIndex === -1) {
|
|
9548
|
-
return { kind: "start", test: orderedTests[0], reason: "first_live" };
|
|
9549
|
-
}
|
|
9550
|
-
return {
|
|
9551
|
-
kind: "start",
|
|
9552
|
-
test: orderedTests[(latestTestIndex + 1) % orderedTests.length],
|
|
9553
|
-
reason: "round_robin"
|
|
9554
|
-
};
|
|
9555
|
-
}
|
|
9556
|
-
function presentationHash(value) {
|
|
9557
|
-
let hash = 2166136261;
|
|
9558
|
-
for (let index = 0;index < value.length; index++) {
|
|
9559
|
-
hash ^= value.charCodeAt(index);
|
|
9560
|
-
hash = Math.imul(hash, 16777619);
|
|
9561
|
-
}
|
|
9562
|
-
return hash >>> 0;
|
|
9563
|
-
}
|
|
9564
|
-
function comparePresentationText(left, right) {
|
|
9565
|
-
if (left < right) {
|
|
9566
|
-
return -1;
|
|
9567
|
-
}
|
|
9568
|
-
if (left > right) {
|
|
9569
|
-
return 1;
|
|
9570
|
-
}
|
|
9571
|
-
return 0;
|
|
9572
|
-
}
|
|
9573
|
-
function presentationChoices(choices, attemptId, itemIdentifier, domain) {
|
|
9574
|
-
const seed = `${attemptId}\x00${itemIdentifier}\x00${domain}\x00`;
|
|
9575
|
-
return [...choices].toSorted((left, right) => {
|
|
9576
|
-
const hashDifference = presentationHash(`${seed}${left.identifier}`) - presentationHash(`${seed}${right.identifier}`);
|
|
9577
|
-
return hashDifference || comparePresentationText(left.identifier, right.identifier);
|
|
9578
|
-
});
|
|
9579
|
-
}
|
|
9580
|
-
function assessmentPresentationForAttempt(assessment, attemptId) {
|
|
9581
|
-
const items = assessment.items.map((item) => {
|
|
9582
|
-
const interactions = item.interactions.map((interaction) => {
|
|
9583
|
-
if (interaction.type === "order" && interaction.shuffle) {
|
|
9584
|
-
return {
|
|
9585
|
-
...interaction,
|
|
9586
|
-
choices: presentationChoices(interaction.choices, attemptId, item.identifier, `order\x00${interaction.responseIdentifier}`)
|
|
9587
|
-
};
|
|
9588
|
-
}
|
|
9589
|
-
if (interaction.type === "match" && interaction.shuffle) {
|
|
9590
|
-
return {
|
|
9591
|
-
...interaction,
|
|
9592
|
-
sourceChoices: presentationChoices(interaction.sourceChoices, attemptId, item.identifier, `match-source\x00${interaction.responseIdentifier}`),
|
|
9593
|
-
targetChoices: presentationChoices(interaction.targetChoices, attemptId, item.identifier, `match-target\x00${interaction.responseIdentifier}`)
|
|
9594
|
-
};
|
|
9595
|
-
}
|
|
9596
|
-
return interaction;
|
|
9597
|
-
});
|
|
9598
|
-
return { ...item, interactions };
|
|
9599
|
-
});
|
|
9600
|
-
return { ...assessment, items };
|
|
9475
|
+
function assessmentFlowForPurpose(purpose) {
|
|
9476
|
+
return purpose === "review" || purpose === "mastery" ? "item-submit" : "attempt-submit";
|
|
9601
9477
|
}
|
|
9602
9478
|
function isTimebackGrade(value) {
|
|
9603
9479
|
return typeof value === "number" && Number.isInteger(value) && GRADE_VALUES.includes(value);
|
|
@@ -9609,18 +9485,22 @@ function isAssessmentResponseValue(value) {
|
|
|
9609
9485
|
const entries = Array.isArray(value) ? value : [value];
|
|
9610
9486
|
return entries.length > 0 && entries.every((entry) => typeof entry === "string" && entry.length > 0 && entry.length <= TIMEBACK_ASSESSMENT_RESPONSE_VALUE_MAX_LENGTH);
|
|
9611
9487
|
}
|
|
9488
|
+
function applyAssessmentItemResponseUpdate(current, update) {
|
|
9489
|
+
const next = { ...current };
|
|
9490
|
+
for (const [responseId, value] of Object.entries(update)) {
|
|
9491
|
+
if (value === null) {
|
|
9492
|
+
delete next[responseId];
|
|
9493
|
+
} else {
|
|
9494
|
+
next[responseId] = value;
|
|
9495
|
+
}
|
|
9496
|
+
}
|
|
9497
|
+
return Object.keys(next).length === 0 ? undefined : next;
|
|
9498
|
+
}
|
|
9612
9499
|
function applyAssessmentResponseUpdate(current, update) {
|
|
9613
9500
|
const next = structuredClone(current);
|
|
9614
9501
|
for (const [questionId, questionUpdate] of Object.entries(update)) {
|
|
9615
|
-
const questionResponses =
|
|
9616
|
-
|
|
9617
|
-
if (value === null) {
|
|
9618
|
-
delete questionResponses[responseId];
|
|
9619
|
-
} else {
|
|
9620
|
-
questionResponses[responseId] = value;
|
|
9621
|
-
}
|
|
9622
|
-
}
|
|
9623
|
-
if (Object.keys(questionResponses).length === 0) {
|
|
9502
|
+
const questionResponses = applyAssessmentItemResponseUpdate(next[questionId], questionUpdate);
|
|
9503
|
+
if (questionResponses === undefined) {
|
|
9624
9504
|
delete next[questionId];
|
|
9625
9505
|
} else {
|
|
9626
9506
|
next[questionId] = questionResponses;
|
|
@@ -9781,6 +9661,256 @@ function interactionResponseValidationMessage(itemIdentifier, interaction, respo
|
|
|
9781
9661
|
}
|
|
9782
9662
|
return null;
|
|
9783
9663
|
}
|
|
9664
|
+
function invalidResponse(message) {
|
|
9665
|
+
return { code: "INVALID_RESPONSE", message };
|
|
9666
|
+
}
|
|
9667
|
+
function responseValuesEqual(left, right) {
|
|
9668
|
+
if (left === undefined || right === undefined) {
|
|
9669
|
+
return left === right;
|
|
9670
|
+
}
|
|
9671
|
+
if (Array.isArray(left) || Array.isArray(right)) {
|
|
9672
|
+
return Array.isArray(left) && Array.isArray(right) && left.length === right.length && left.every((value, index) => value === right[index]);
|
|
9673
|
+
}
|
|
9674
|
+
return left === right;
|
|
9675
|
+
}
|
|
9676
|
+
function itemResponsesEqual(left, right) {
|
|
9677
|
+
const leftEntries = Object.entries(left ?? {});
|
|
9678
|
+
const rightEntries = Object.entries(right ?? {});
|
|
9679
|
+
return leftEntries.length === rightEntries.length && leftEntries.every(([identifier, value]) => responseValuesEqual(value, right?.[identifier]));
|
|
9680
|
+
}
|
|
9681
|
+
function prepareAssessmentItemSubmission(state, input) {
|
|
9682
|
+
const flow = assessmentFlowForPurpose(state.purpose);
|
|
9683
|
+
if (flow !== "item-submit") {
|
|
9684
|
+
return {
|
|
9685
|
+
action: "reject",
|
|
9686
|
+
failure: assessmentFlowViolation("Items cannot be submitted individually in this flow.", { flow })
|
|
9687
|
+
};
|
|
9688
|
+
}
|
|
9689
|
+
const priorSubmission = state.itemSubmissions.find((submission) => submission.submissionId === input.submissionId);
|
|
9690
|
+
if (priorSubmission) {
|
|
9691
|
+
const replayedItemResponses = applyAssessmentItemResponseUpdate(state.responses[input.itemIdentifier], input.responses);
|
|
9692
|
+
const sameRequest = priorSubmission.itemIdentifier === input.itemIdentifier && itemResponsesEqual(state.responses[input.itemIdentifier], replayedItemResponses);
|
|
9693
|
+
if (!sameRequest) {
|
|
9694
|
+
return {
|
|
9695
|
+
action: "reject",
|
|
9696
|
+
failure: assessmentFlowViolation("This item submission ID was already used for a different request.", {
|
|
9697
|
+
submissionId: input.submissionId,
|
|
9698
|
+
itemIdentifier: input.itemIdentifier,
|
|
9699
|
+
committedItemIdentifier: priorSubmission.itemIdentifier,
|
|
9700
|
+
flow
|
|
9701
|
+
})
|
|
9702
|
+
};
|
|
9703
|
+
}
|
|
9704
|
+
return {
|
|
9705
|
+
action: "replay",
|
|
9706
|
+
responseVersion: state.responseVersion,
|
|
9707
|
+
responses: state.responses,
|
|
9708
|
+
submission: priorSubmission
|
|
9709
|
+
};
|
|
9710
|
+
}
|
|
9711
|
+
if (state.responseVersion !== input.expectedResponseVersion) {
|
|
9712
|
+
return {
|
|
9713
|
+
action: "reject",
|
|
9714
|
+
failure: assessmentResponseVersionConflict(input.expectedResponseVersion, state.responseVersion)
|
|
9715
|
+
};
|
|
9716
|
+
}
|
|
9717
|
+
const committed = new Set(state.itemSubmissions.map((submission) => submission.itemIdentifier));
|
|
9718
|
+
const itemIndex = state.assessment.items.findIndex((item) => !committed.has(item.identifier));
|
|
9719
|
+
const nextItem = state.assessment.items[itemIndex];
|
|
9720
|
+
if (nextItem?.identifier !== input.itemIdentifier) {
|
|
9721
|
+
return {
|
|
9722
|
+
action: "reject",
|
|
9723
|
+
failure: assessmentFlowViolation("Items must be submitted once and in presentation order.", {
|
|
9724
|
+
itemIdentifier: input.itemIdentifier,
|
|
9725
|
+
expectedItemIdentifier: nextItem?.identifier ?? null,
|
|
9726
|
+
flow
|
|
9727
|
+
})
|
|
9728
|
+
};
|
|
9729
|
+
}
|
|
9730
|
+
const update = { [input.itemIdentifier]: input.responses };
|
|
9731
|
+
const updateMessage = assessmentResponseUpdateValidationMessage(state.assessment, update);
|
|
9732
|
+
if (updateMessage) {
|
|
9733
|
+
return { action: "reject", failure: invalidResponse(updateMessage) };
|
|
9734
|
+
}
|
|
9735
|
+
const responses = applyAssessmentResponseUpdate(state.responses, update);
|
|
9736
|
+
const validationMessage = assessmentResponseValidationMessage(state.assessment, responses);
|
|
9737
|
+
if (validationMessage) {
|
|
9738
|
+
return { action: "reject", failure: invalidResponse(validationMessage) };
|
|
9739
|
+
}
|
|
9740
|
+
return {
|
|
9741
|
+
action: "commit",
|
|
9742
|
+
itemIndex,
|
|
9743
|
+
responseVersion: state.responseVersion + 1,
|
|
9744
|
+
responses,
|
|
9745
|
+
answered: Object.keys(responses[input.itemIdentifier] ?? {}).length > 0
|
|
9746
|
+
};
|
|
9747
|
+
}
|
|
9748
|
+
function completeAssessmentItemSubmission(state, input, prepared, scoring) {
|
|
9749
|
+
const submission = {
|
|
9750
|
+
submissionId: input.submissionId,
|
|
9751
|
+
itemIdentifier: input.itemIdentifier,
|
|
9752
|
+
responseVersion: prepared.responseVersion,
|
|
9753
|
+
answered: prepared.answered,
|
|
9754
|
+
score: scoring.score,
|
|
9755
|
+
isCorrect: scoring.isCorrect
|
|
9756
|
+
};
|
|
9757
|
+
return {
|
|
9758
|
+
responseVersion: prepared.responseVersion,
|
|
9759
|
+
responses: prepared.responses,
|
|
9760
|
+
itemSubmissions: [...state.itemSubmissions, submission],
|
|
9761
|
+
submission
|
|
9762
|
+
};
|
|
9763
|
+
}
|
|
9764
|
+
function assessmentResponseUpdateFlowFailure(purpose, itemSubmissions, update) {
|
|
9765
|
+
const committed = new Set(itemSubmissions.map((submission) => submission.itemIdentifier));
|
|
9766
|
+
const committedUpdate = Object.keys(update).find((identifier) => committed.has(identifier));
|
|
9767
|
+
return committedUpdate ? assessmentFlowViolation("A submitted item response cannot be edited.", {
|
|
9768
|
+
itemIdentifier: committedUpdate,
|
|
9769
|
+
flow: assessmentFlowForPurpose(purpose)
|
|
9770
|
+
}) : null;
|
|
9771
|
+
}
|
|
9772
|
+
function assessmentCompletionFlowFailure(purpose, itemSubmissions, itemCount) {
|
|
9773
|
+
const flow = assessmentFlowForPurpose(purpose);
|
|
9774
|
+
return flow === "item-submit" && itemSubmissions.length !== itemCount ? assessmentFlowViolation("Every item must be submitted before the attempt can be completed.", {
|
|
9775
|
+
flow,
|
|
9776
|
+
submittedItemCount: itemSubmissions.length,
|
|
9777
|
+
itemCount
|
|
9778
|
+
}) : null;
|
|
9779
|
+
}
|
|
9780
|
+
function selectAssessmentCourseCandidate(candidates, subjectSpecified) {
|
|
9781
|
+
if (candidates.length === 0) {
|
|
9782
|
+
return { outcome: "none" };
|
|
9783
|
+
}
|
|
9784
|
+
const serviceable = candidates.filter((candidate) => candidate.hasLiveTests || candidate.hasOpenAttempt);
|
|
9785
|
+
if (serviceable.length === 0) {
|
|
9786
|
+
return { outcome: "no_eligible_tests" };
|
|
9787
|
+
}
|
|
9788
|
+
const withOpenAttempt = serviceable.filter((candidate) => candidate.hasOpenAttempt);
|
|
9789
|
+
const eligible = withOpenAttempt.length > 0 ? withOpenAttempt : serviceable;
|
|
9790
|
+
const subjects = new Set(eligible.map((candidate) => candidate.subject)).size;
|
|
9791
|
+
if (!subjectSpecified && subjects > 1) {
|
|
9792
|
+
return { outcome: "ambiguous", subjects };
|
|
9793
|
+
}
|
|
9794
|
+
const selected = [...eligible].toSorted((left, right) => left.grade - right.grade || left.id.localeCompare(right.id))[0];
|
|
9795
|
+
return { outcome: "selected", selected };
|
|
9796
|
+
}
|
|
9797
|
+
function assessmentAttemptTimestamp(value) {
|
|
9798
|
+
const parsed = Date.parse(value);
|
|
9799
|
+
return Number.isNaN(parsed) ? 0 : parsed;
|
|
9800
|
+
}
|
|
9801
|
+
function newestAttempt(left, right, field) {
|
|
9802
|
+
const dateDifference = assessmentAttemptTimestamp(right[field]) - assessmentAttemptTimestamp(left[field]);
|
|
9803
|
+
return dateDifference || left.attemptId.localeCompare(right.attemptId);
|
|
9804
|
+
}
|
|
9805
|
+
function compareNewestCompletedAssessmentAttempt(left, right) {
|
|
9806
|
+
return newestAttempt(left, right, "scoreDate");
|
|
9807
|
+
}
|
|
9808
|
+
function compareAssessmentCatalogOrder(left, right) {
|
|
9809
|
+
if (left.sortOrder !== null || right.sortOrder !== null) {
|
|
9810
|
+
if (left.sortOrder === null) {
|
|
9811
|
+
return 1;
|
|
9812
|
+
}
|
|
9813
|
+
if (right.sortOrder === null) {
|
|
9814
|
+
return -1;
|
|
9815
|
+
}
|
|
9816
|
+
const orderDifference = left.sortOrder - right.sortOrder;
|
|
9817
|
+
if (orderDifference) {
|
|
9818
|
+
return orderDifference;
|
|
9819
|
+
}
|
|
9820
|
+
}
|
|
9821
|
+
const leftUpdatedAt = Date.parse(left.updatedAt);
|
|
9822
|
+
const rightUpdatedAt = Date.parse(right.updatedAt);
|
|
9823
|
+
const leftHasValidDate = Number.isFinite(leftUpdatedAt);
|
|
9824
|
+
const rightHasValidDate = Number.isFinite(rightUpdatedAt);
|
|
9825
|
+
if (leftHasValidDate && rightHasValidDate) {
|
|
9826
|
+
const updatedDifference = leftUpdatedAt - rightUpdatedAt;
|
|
9827
|
+
if (updatedDifference) {
|
|
9828
|
+
return updatedDifference;
|
|
9829
|
+
}
|
|
9830
|
+
} else if (leftHasValidDate !== rightHasValidDate) {
|
|
9831
|
+
return leftHasValidDate ? -1 : 1;
|
|
9832
|
+
}
|
|
9833
|
+
return left.qtiTestIdentifier.localeCompare(right.qtiTestIdentifier);
|
|
9834
|
+
}
|
|
9835
|
+
function orderRuntimeAssessmentTests(tests) {
|
|
9836
|
+
return [...tests].toSorted(compareAssessmentCatalogOrder);
|
|
9837
|
+
}
|
|
9838
|
+
function selectRuntimeAssessment(liveTests, attempts) {
|
|
9839
|
+
const resumable = attempts.filter((attempt) => attempt.resumable !== false && isAssessmentAttemptOpen(attempt)).toSorted((left, right) => newestAttempt(left, right, "updatedAt"));
|
|
9840
|
+
if (resumable.length > 0) {
|
|
9841
|
+
return {
|
|
9842
|
+
kind: "resume",
|
|
9843
|
+
attempt: resumable[0],
|
|
9844
|
+
additionalResumableAttemptIds: resumable.slice(1).map((candidate) => candidate.attemptId)
|
|
9845
|
+
};
|
|
9846
|
+
}
|
|
9847
|
+
const orderedTests = orderRuntimeAssessmentTests(liveTests);
|
|
9848
|
+
if (orderedTests.length === 0) {
|
|
9849
|
+
return null;
|
|
9850
|
+
}
|
|
9851
|
+
const completed = attempts.filter(isAssessmentAttemptCompleted);
|
|
9852
|
+
const exposedTestIds = new Set(attempts.filter((attempt) => isAssessmentAttemptOpen(attempt) || isAssessmentAttemptCompleted(attempt)).map((attempt) => attempt.selectedTestIdentifier));
|
|
9853
|
+
const unattempted = orderedTests.find((test) => !exposedTestIds.has(test.qtiTestIdentifier));
|
|
9854
|
+
if (unattempted) {
|
|
9855
|
+
return { kind: "start", test: unattempted, reason: "unattempted" };
|
|
9856
|
+
}
|
|
9857
|
+
const latestCompleted = completed.toSorted(compareNewestCompletedAssessmentAttempt)[0];
|
|
9858
|
+
const latestTestIndex = latestCompleted ? orderedTests.findIndex((test) => test.qtiTestIdentifier === latestCompleted.selectedTestIdentifier) : -1;
|
|
9859
|
+
if (latestTestIndex === -1) {
|
|
9860
|
+
return { kind: "start", test: orderedTests[0], reason: "first_live" };
|
|
9861
|
+
}
|
|
9862
|
+
return {
|
|
9863
|
+
kind: "start",
|
|
9864
|
+
test: orderedTests[(latestTestIndex + 1) % orderedTests.length],
|
|
9865
|
+
reason: "round_robin"
|
|
9866
|
+
};
|
|
9867
|
+
}
|
|
9868
|
+
function presentationHash(value) {
|
|
9869
|
+
let hash = 2166136261;
|
|
9870
|
+
for (let index = 0;index < value.length; index++) {
|
|
9871
|
+
hash ^= value.charCodeAt(index);
|
|
9872
|
+
hash = Math.imul(hash, 16777619);
|
|
9873
|
+
}
|
|
9874
|
+
return hash >>> 0;
|
|
9875
|
+
}
|
|
9876
|
+
function comparePresentationText(left, right) {
|
|
9877
|
+
if (left < right) {
|
|
9878
|
+
return -1;
|
|
9879
|
+
}
|
|
9880
|
+
if (left > right) {
|
|
9881
|
+
return 1;
|
|
9882
|
+
}
|
|
9883
|
+
return 0;
|
|
9884
|
+
}
|
|
9885
|
+
function presentationChoices(choices, attemptId, itemIdentifier, domain) {
|
|
9886
|
+
const seed = `${attemptId}\x00${itemIdentifier}\x00${domain}\x00`;
|
|
9887
|
+
return [...choices].toSorted((left, right) => {
|
|
9888
|
+
const hashDifference = presentationHash(`${seed}${left.identifier}`) - presentationHash(`${seed}${right.identifier}`);
|
|
9889
|
+
return hashDifference || comparePresentationText(left.identifier, right.identifier);
|
|
9890
|
+
});
|
|
9891
|
+
}
|
|
9892
|
+
function assessmentPresentationForAttempt(assessment, attemptId) {
|
|
9893
|
+
const items = assessment.items.map((item) => {
|
|
9894
|
+
const interactions = item.interactions.map((interaction) => {
|
|
9895
|
+
if (interaction.type === "order" && interaction.shuffle) {
|
|
9896
|
+
return {
|
|
9897
|
+
...interaction,
|
|
9898
|
+
choices: presentationChoices(interaction.choices, attemptId, item.identifier, `order\x00${interaction.responseIdentifier}`)
|
|
9899
|
+
};
|
|
9900
|
+
}
|
|
9901
|
+
if (interaction.type === "match" && interaction.shuffle) {
|
|
9902
|
+
return {
|
|
9903
|
+
...interaction,
|
|
9904
|
+
sourceChoices: presentationChoices(interaction.sourceChoices, attemptId, item.identifier, `match-source\x00${interaction.responseIdentifier}`),
|
|
9905
|
+
targetChoices: presentationChoices(interaction.targetChoices, attemptId, item.identifier, `match-target\x00${interaction.responseIdentifier}`)
|
|
9906
|
+
};
|
|
9907
|
+
}
|
|
9908
|
+
return interaction;
|
|
9909
|
+
});
|
|
9910
|
+
return { ...item, interactions };
|
|
9911
|
+
});
|
|
9912
|
+
return { ...assessment, items };
|
|
9913
|
+
}
|
|
9784
9914
|
function compareCodeUnits(left, right) {
|
|
9785
9915
|
if (left < right) {
|
|
9786
9916
|
return -1;
|
|
@@ -10172,6 +10302,9 @@ function isReviewFulfillment(value) {
|
|
|
10172
10302
|
function isReviewItemOutcome(value) {
|
|
10173
10303
|
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");
|
|
10174
10304
|
}
|
|
10305
|
+
function isAssessmentItemSubmission(value) {
|
|
10306
|
+
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");
|
|
10307
|
+
}
|
|
10175
10308
|
function isReviewAttemptMetadata(value) {
|
|
10176
10309
|
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);
|
|
10177
10310
|
}
|
|
@@ -10186,7 +10319,7 @@ function isPlaycademyAssessmentResultMetadataV1(value) {
|
|
|
10186
10319
|
const selectedTest = metadata2.selectedTest;
|
|
10187
10320
|
const mastery = metadata2.mastery;
|
|
10188
10321
|
const review = metadata2.review;
|
|
10189
|
-
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)));
|
|
10322
|
+
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)));
|
|
10190
10323
|
}
|
|
10191
10324
|
function isPlaycademyAssessmentItemResultMetadataV1(value) {
|
|
10192
10325
|
if (!isRecord(value) || !isRecord(value.responses)) {
|
|
@@ -10203,6 +10336,9 @@ function playcademyAssessmentResultMetadata(value) {
|
|
|
10203
10336
|
return null;
|
|
10204
10337
|
}
|
|
10205
10338
|
let normalized = metadata2.responses === undefined ? { ...metadata2, responses: {} } : metadata2;
|
|
10339
|
+
if (normalized.itemSubmissions === undefined) {
|
|
10340
|
+
normalized = { ...normalized, itemSubmissions: [] };
|
|
10341
|
+
}
|
|
10206
10342
|
const review = normalized.review;
|
|
10207
10343
|
if (isRecord(review) && isRecord(review.fulfillment) && review.fulfillment.status === "complete" && review.fulfillment.shortages === undefined) {
|
|
10208
10344
|
normalized = {
|
|
@@ -10229,7 +10365,7 @@ function playcademyAssessmentItemResultMetadata(value) {
|
|
|
10229
10365
|
const normalized = metadata2.responses === undefined ? { ...metadata2, responses: {} } : metadata2;
|
|
10230
10366
|
return isPlaycademyAssessmentItemResultMetadataV1(normalized) ? normalized : null;
|
|
10231
10367
|
}
|
|
10232
|
-
var SCRIPT_SCHEMES, DATA_RASTER_IMAGE_PATTERN, QTI_MATHML_ALLOWED_TAGS, metadataSymbol, parser, SUPPORTED_QTI_INTERACTION_TYPES, URL_ATTRIBUTES, IMAGE_URL_ATTRIBUTES, INTERACTION_TYPES, EMPHASIS_TAGS, STRONG_TAGS, CONTENT_SKIPPED_TAGS, BLOCK_CONTENT_KINDS, GAP_MATCH_TOKEN_TAGS, STAGE_GRAPHIC_EXCLUDED_CONTAINERS, BLANK_SENTINEL = "", MARKED_BLANK_PATTERN, MARKED_BLANK_GLOBAL_PATTERN, MATCH_CORRECT_TEMPLATE = "http://www.imsglobal.org/question/qti_v3p0/rptemplates/match_correct", MAP_RESPONSE_TEMPLATE = "https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/map_response", MAP_RESPONSE_TEMPLATES, RESPONSE_PROCESSING_TEMPLATES, VOID_BODY_ELEMENTS, URL_ATTRIBUTES2, IMAGE_URL_ATTRIBUTES2, VOID_CHOICE_ELEMENTS, NUMERIC_COMPARISON_ATTRIBUTES, RESPONSE_CARDINALITIES, RESPONSE_BASE_TYPES, POINT_INTERACTION_TYPES, INDEPENDENT_PROCESSING_TAGS, UUID_PATTERN, COMMON_CORE_MATH_FRAMEWORK_ALIASES, COMMON_CORE_ELA_FRAMEWORK_ALIASES, COMMON_CORE_FRAMEWORK_ALIASES, ASSESSMENT_ATTEMPT_OPEN, ASSESSMENT_ATTEMPT_COMPLETED, ASSESSMENT_RUNTIME_ERROR_STATUS, GRADE_VALUES, POINT_RESPONSE_PATTERN, REVIEW_SELECTION_POLICY_VERSION = "unseen-first-least-recently-delivered-v1", DEFAULT_REVIEW_SELECTION_POLICY, RuntimeSubjectSchema, RuntimeGradeSchema, OptionalQueryGradeSchema, AssessmentResponseValueSchema, AssessmentRuntimeIdentitySchema, ResponseKeySchema, StartAssessmentBaseSchema, AssessmentStandardRefSchema, LatestAssessmentFilterBaseSchema, LatestAssessmentFiltersSchema, LatestRuntimeAssessmentQuerySchema, StartAssessmentBodySchema, SaveAssessmentBodySchema, SubmitAssessmentBodySchema, StartRuntimeAssessmentRequestSchema, SaveRuntimeAssessmentRequestSchema, SubmitRuntimeAssessmentRequestSchema;
|
|
10368
|
+
var SCRIPT_SCHEMES, DATA_RASTER_IMAGE_PATTERN, QTI_MATHML_ALLOWED_TAGS, metadataSymbol, parser, SUPPORTED_QTI_INTERACTION_TYPES, URL_ATTRIBUTES, IMAGE_URL_ATTRIBUTES, INTERACTION_TYPES, EMPHASIS_TAGS, STRONG_TAGS, CONTENT_SKIPPED_TAGS, BLOCK_CONTENT_KINDS, GAP_MATCH_TOKEN_TAGS, STAGE_GRAPHIC_EXCLUDED_CONTAINERS, BLANK_SENTINEL = "", MARKED_BLANK_PATTERN, MARKED_BLANK_GLOBAL_PATTERN, MATCH_CORRECT_TEMPLATE = "http://www.imsglobal.org/question/qti_v3p0/rptemplates/match_correct", MAP_RESPONSE_TEMPLATE = "https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/map_response", MAP_RESPONSE_TEMPLATES, RESPONSE_PROCESSING_TEMPLATES, VOID_BODY_ELEMENTS, URL_ATTRIBUTES2, IMAGE_URL_ATTRIBUTES2, VOID_CHOICE_ELEMENTS, NUMERIC_COMPARISON_ATTRIBUTES, RESPONSE_CARDINALITIES, RESPONSE_BASE_TYPES, POINT_INTERACTION_TYPES, INDEPENDENT_PROCESSING_TAGS, UUID_PATTERN, COMMON_CORE_MATH_FRAMEWORK_ALIASES, COMMON_CORE_ELA_FRAMEWORK_ALIASES, COMMON_CORE_FRAMEWORK_ALIASES, ASSESSMENT_ATTEMPT_OPEN, ASSESSMENT_ATTEMPT_COMPLETED, ASSESSMENT_RUNTIME_ERROR_STATUS, GRADE_VALUES, POINT_RESPONSE_PATTERN, REVIEW_SELECTION_POLICY_VERSION = "unseen-first-least-recently-delivered-v1", DEFAULT_REVIEW_SELECTION_POLICY, RuntimeSubjectSchema, RuntimeGradeSchema, OptionalQueryGradeSchema, AssessmentResponseValueSchema, AssessmentRuntimeIdentitySchema, ResponseKeySchema, StartAssessmentBaseSchema, AssessmentStandardRefSchema, LatestAssessmentFilterBaseSchema, LatestAssessmentFiltersSchema, LatestRuntimeAssessmentQuerySchema, StartAssessmentBodySchema, SaveAssessmentBodySchema, SubmitAssessmentItemBodySchema, SubmitAssessmentBodySchema, StartRuntimeAssessmentRequestSchema, SaveRuntimeAssessmentRequestSchema, SubmitAssessmentItemRuntimeRequestSchema, SubmitRuntimeAssessmentRequestSchema;
|
|
10233
10369
|
var init_assessment_runtime = __esm(() => {
|
|
10234
10370
|
init_timeback3();
|
|
10235
10371
|
init_timeback3();
|
|
@@ -10417,7 +10553,8 @@ var init_assessment_runtime = __esm(() => {
|
|
|
10417
10553
|
RESPONSE_VERSION_CONFLICT: 409,
|
|
10418
10554
|
INVALID_RESPONSE: 422,
|
|
10419
10555
|
UNAUTHORIZED_ATTEMPT: 403,
|
|
10420
|
-
ATTEMPT_ALREADY_SUBMITTED: 409
|
|
10556
|
+
ATTEMPT_ALREADY_SUBMITTED: 409,
|
|
10557
|
+
ASSESSMENT_FLOW_VIOLATION: 409
|
|
10421
10558
|
};
|
|
10422
10559
|
GRADE_VALUES = TIMEBACK_GRADES;
|
|
10423
10560
|
POINT_RESPONSE_PATTERN = /^-?\d+(?:\.\d+)? -?\d+(?:\.\d+)?$/;
|
|
@@ -10488,12 +10625,19 @@ var init_assessment_runtime = __esm(() => {
|
|
|
10488
10625
|
expectedResponseVersion: exports_external.number().int().nonnegative(),
|
|
10489
10626
|
responses: exports_external.record(ResponseKeySchema, exports_external.record(ResponseKeySchema, AssessmentResponseValueSchema.nullable()))
|
|
10490
10627
|
});
|
|
10628
|
+
SubmitAssessmentItemBodySchema = exports_external.object({
|
|
10629
|
+
expectedResponseVersion: exports_external.number().int().nonnegative(),
|
|
10630
|
+
submissionId: exports_external.string().trim().min(1),
|
|
10631
|
+
itemIdentifier: ResponseKeySchema,
|
|
10632
|
+
responses: exports_external.record(ResponseKeySchema, AssessmentResponseValueSchema.nullable())
|
|
10633
|
+
});
|
|
10491
10634
|
SubmitAssessmentBodySchema = exports_external.object({
|
|
10492
10635
|
expectedResponseVersion: exports_external.number().int().nonnegative(),
|
|
10493
10636
|
submissionId: exports_external.string().trim().min(1)
|
|
10494
10637
|
});
|
|
10495
10638
|
StartRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.and(StartAssessmentBodySchema);
|
|
10496
10639
|
SaveRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.extend(SaveAssessmentBodySchema.shape);
|
|
10640
|
+
SubmitAssessmentItemRuntimeRequestSchema = AssessmentRuntimeIdentitySchema.extend(SubmitAssessmentItemBodySchema.shape);
|
|
10497
10641
|
SubmitRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.extend(SubmitAssessmentBodySchema.shape).extend({
|
|
10498
10642
|
appName: exports_external.string().trim().min(1),
|
|
10499
10643
|
sensorUrl: exports_external.string().url()
|
|
@@ -91168,6 +91312,9 @@ class TimebackAssessmentRuntimeService {
|
|
|
91168
91312
|
static ASSESSMENT_CACHE_LIMIT = 32;
|
|
91169
91313
|
static ASSESSMENT_CACHE_TTL_MS = 60000;
|
|
91170
91314
|
static EXPORT_CONCURRENCY = 4;
|
|
91315
|
+
static ITEM_SUBMISSION_MAX_PREPARATION_ATTEMPTS = 3;
|
|
91316
|
+
static PROJECTION_BARRIER_MAX_ATTEMPTS = 5;
|
|
91317
|
+
static PROJECTION_BARRIER_RETRY_DELAY_MS = 25;
|
|
91171
91318
|
static SCORING_CONCURRENCY = 4;
|
|
91172
91319
|
deps;
|
|
91173
91320
|
assessmentCache = new TimebackCache({
|
|
@@ -91260,6 +91407,7 @@ class TimebackAssessmentRuntimeService {
|
|
|
91260
91407
|
attemptNumber,
|
|
91261
91408
|
responseVersion: 0,
|
|
91262
91409
|
responses: {},
|
|
91410
|
+
itemSubmissions: [],
|
|
91263
91411
|
startedAt: timestamp6,
|
|
91264
91412
|
updatedAt: timestamp6
|
|
91265
91413
|
};
|
|
@@ -91385,6 +91533,7 @@ class TimebackAssessmentRuntimeService {
|
|
|
91385
91533
|
attemptNumber,
|
|
91386
91534
|
responseVersion: 0,
|
|
91387
91535
|
responses: {},
|
|
91536
|
+
itemSubmissions: [],
|
|
91388
91537
|
startedAt: timestamp6,
|
|
91389
91538
|
updatedAt: timestamp6,
|
|
91390
91539
|
review: {
|
|
@@ -91509,6 +91658,10 @@ class TimebackAssessmentRuntimeService {
|
|
|
91509
91658
|
this.assertInProgress(attempt.result);
|
|
91510
91659
|
this.assertResponseVersion(attempt.metadata, input.expectedResponseVersion);
|
|
91511
91660
|
const assessment = await this.loadAttemptAssessment(attempt.metadata);
|
|
91661
|
+
const flowFailure = assessmentResponseUpdateFlowFailure(attempt.metadata.purpose, attempt.metadata.itemSubmissions, input.responses);
|
|
91662
|
+
if (flowFailure) {
|
|
91663
|
+
throw AssessmentRuntimeError.from(flowFailure);
|
|
91664
|
+
}
|
|
91512
91665
|
validateAssessmentResponseUpdate(assessment, input.responses);
|
|
91513
91666
|
const responses = applyAssessmentResponseUpdate(attempt.metadata.responses, input.responses);
|
|
91514
91667
|
validateAssessmentResponses(assessment, responses);
|
|
@@ -91530,11 +91683,105 @@ class TimebackAssessmentRuntimeService {
|
|
|
91530
91683
|
};
|
|
91531
91684
|
});
|
|
91532
91685
|
}
|
|
91533
|
-
async
|
|
91686
|
+
async submitItem({
|
|
91687
|
+
input,
|
|
91688
|
+
...params
|
|
91689
|
+
}) {
|
|
91690
|
+
await this.deps.validateDeveloperAccess(params.user, params.gameId);
|
|
91691
|
+
let preview = await this.prepareItemSubmission(params, input);
|
|
91692
|
+
let preparationAttempts = 1;
|
|
91693
|
+
while (true) {
|
|
91694
|
+
const committed = await this.deps.assessmentRuntimeLock(this.deps.db, assessmentRuntimeAttemptLockKey(params.attemptId), async (db2) => {
|
|
91695
|
+
const attempt = await this.authorizeAttempt(params, {
|
|
91696
|
+
db: db2,
|
|
91697
|
+
developerAccessValidated: true
|
|
91698
|
+
});
|
|
91699
|
+
this.assertInProgress(attempt.result);
|
|
91700
|
+
if (!preview || preview.responseVersion !== attempt.metadata.responseVersion || preview.submissionId !== input.submissionId || preview.itemIdentifier !== input.itemIdentifier) {
|
|
91701
|
+
if (preparationAttempts >= TimebackAssessmentRuntimeService.ITEM_SUBMISSION_MAX_PREPARATION_ATTEMPTS) {
|
|
91702
|
+
if (input.expectedResponseVersion === attempt.metadata.responseVersion) {
|
|
91703
|
+
throw new ServiceUnavailableError("The assessment response state changed repeatedly while this item was being prepared. Try again shortly.", {
|
|
91704
|
+
retryable: true,
|
|
91705
|
+
reason: "PREPARATION_CONTENTION",
|
|
91706
|
+
expectedResponseVersion: input.expectedResponseVersion,
|
|
91707
|
+
responseVersion: attempt.metadata.responseVersion
|
|
91708
|
+
});
|
|
91709
|
+
}
|
|
91710
|
+
throw AssessmentRuntimeError.from(assessmentResponseVersionConflict(input.expectedResponseVersion, attempt.metadata.responseVersion));
|
|
91711
|
+
}
|
|
91712
|
+
return { action: "prepare" };
|
|
91713
|
+
}
|
|
91714
|
+
const transition = prepareAssessmentItemSubmission({
|
|
91715
|
+
purpose: attempt.metadata.purpose,
|
|
91716
|
+
responseVersion: attempt.metadata.responseVersion,
|
|
91717
|
+
responses: attempt.metadata.responses,
|
|
91718
|
+
itemSubmissions: attempt.metadata.itemSubmissions,
|
|
91719
|
+
assessment: preview.assessment
|
|
91720
|
+
}, input);
|
|
91721
|
+
if (transition.action === "reject") {
|
|
91722
|
+
throw AssessmentRuntimeError.from(transition.failure);
|
|
91723
|
+
}
|
|
91724
|
+
if (transition.action === "replay") {
|
|
91725
|
+
return {
|
|
91726
|
+
action: "complete",
|
|
91727
|
+
response: {
|
|
91728
|
+
attemptId: attempt.result.sourcedId,
|
|
91729
|
+
responseVersion: transition.responseVersion,
|
|
91730
|
+
status: "in_progress",
|
|
91731
|
+
responses: transition.responses,
|
|
91732
|
+
itemSubmissions: attempt.metadata.itemSubmissions,
|
|
91733
|
+
submission: transition.submission
|
|
91734
|
+
},
|
|
91735
|
+
projection: attempt.metadata.purpose === "review" ? {
|
|
91736
|
+
result: attempt.result,
|
|
91737
|
+
metadata: attempt.metadata,
|
|
91738
|
+
itemIdentifier: input.itemIdentifier
|
|
91739
|
+
} : null
|
|
91740
|
+
};
|
|
91741
|
+
}
|
|
91742
|
+
if (!preview.scoring) {
|
|
91743
|
+
throw new Error("Prepared item submission is missing its score");
|
|
91744
|
+
}
|
|
91745
|
+
const completed = completeAssessmentItemSubmission(attempt.metadata, input, transition, preview.scoring);
|
|
91746
|
+
const metadata2 = {
|
|
91747
|
+
...attempt.metadata,
|
|
91748
|
+
responses: completed.responses,
|
|
91749
|
+
responseVersion: completed.responseVersion,
|
|
91750
|
+
itemSubmissions: completed.itemSubmissions,
|
|
91751
|
+
updatedAt: new Date().toISOString()
|
|
91752
|
+
};
|
|
91753
|
+
await this.putResultMetadata(attempt.result, metadata2);
|
|
91754
|
+
return {
|
|
91755
|
+
action: "complete",
|
|
91756
|
+
response: {
|
|
91757
|
+
attemptId: attempt.result.sourcedId,
|
|
91758
|
+
responseVersion: completed.responseVersion,
|
|
91759
|
+
status: "in_progress",
|
|
91760
|
+
responses: completed.responses,
|
|
91761
|
+
itemSubmissions: completed.itemSubmissions,
|
|
91762
|
+
submission: completed.submission
|
|
91763
|
+
},
|
|
91764
|
+
projection: metadata2.purpose === "review" ? {
|
|
91765
|
+
result: attempt.result,
|
|
91766
|
+
metadata: metadata2,
|
|
91767
|
+
itemIdentifier: input.itemIdentifier
|
|
91768
|
+
} : null
|
|
91769
|
+
};
|
|
91770
|
+
});
|
|
91771
|
+
if (committed.action === "prepare") {
|
|
91772
|
+
preparationAttempts += 1;
|
|
91773
|
+
preview = await this.scoreItemSubmission(params, input);
|
|
91774
|
+
} else {
|
|
91775
|
+
if (committed.projection) {
|
|
91776
|
+
await this.projectReviewItemResponse(params, committed.projection);
|
|
91777
|
+
}
|
|
91778
|
+
return committed.response;
|
|
91779
|
+
}
|
|
91780
|
+
}
|
|
91781
|
+
}
|
|
91782
|
+
async scoreItems(assessment, responses, itemIdentifiers) {
|
|
91534
91783
|
const client2 = this.requireClient();
|
|
91535
|
-
const
|
|
91536
|
-
validateAssessmentResponses(assessment, attempt.metadata.responses);
|
|
91537
|
-
const scoringItems = assessmentScoringRequests(assessment, attempt.metadata.responses);
|
|
91784
|
+
const scoringItems = assessmentScoringRequests(assessment, responses).filter((item) => !itemIdentifiers || itemIdentifiers.has(item.itemIdentifier));
|
|
91538
91785
|
const scoringTasks = scoringItems.flatMap((item, itemIndex) => item.requests.map((request) => ({
|
|
91539
91786
|
itemIndex,
|
|
91540
91787
|
itemIdentifier: item.itemIdentifier,
|
|
@@ -91548,7 +91795,7 @@ class TimebackAssessmentRuntimeService {
|
|
|
91548
91795
|
scoresByItem[task.itemIndex].push(Number.isFinite(processed.score) ? processed.score : 0);
|
|
91549
91796
|
verdictsByItem[task.itemIndex].push(processed.isCorrect);
|
|
91550
91797
|
});
|
|
91551
|
-
|
|
91798
|
+
return scoringItems.map((item, itemIndex) => {
|
|
91552
91799
|
const earned = assessmentItemEarnedScore(scoresByItem[itemIndex], item.maxScore);
|
|
91553
91800
|
const possible = item.maxScore;
|
|
91554
91801
|
return {
|
|
@@ -91561,6 +91808,52 @@ class TimebackAssessmentRuntimeService {
|
|
|
91561
91808
|
isCorrect: assessmentItemCorrectness(verdictsByItem[itemIndex])
|
|
91562
91809
|
};
|
|
91563
91810
|
});
|
|
91811
|
+
}
|
|
91812
|
+
async scoreItem(assessment, responses, itemIdentifier) {
|
|
91813
|
+
const [scoring] = await this.scoreItems(assessment, responses, new Set([itemIdentifier]));
|
|
91814
|
+
return scoring;
|
|
91815
|
+
}
|
|
91816
|
+
async prepareItemSubmission(params, input) {
|
|
91817
|
+
try {
|
|
91818
|
+
return await this.scoreItemSubmission(params, input);
|
|
91819
|
+
} catch (error88) {
|
|
91820
|
+
addEvent("assessment.item_submission_preview_failed", {
|
|
91821
|
+
"app.assessment.attempt_id": params.attemptId,
|
|
91822
|
+
"exception.type": errorType(error88),
|
|
91823
|
+
"app.error.message": errorMessage(error88)
|
|
91824
|
+
});
|
|
91825
|
+
return null;
|
|
91826
|
+
}
|
|
91827
|
+
}
|
|
91828
|
+
async scoreItemSubmission(params, input) {
|
|
91829
|
+
const attempt = await this.peekAttempt(params);
|
|
91830
|
+
if (!isAssessmentAttemptOpen({
|
|
91831
|
+
inProgress: attempt.result.inProgress ?? "",
|
|
91832
|
+
scoreStatus: attempt.result.scoreStatus
|
|
91833
|
+
})) {
|
|
91834
|
+
return null;
|
|
91835
|
+
}
|
|
91836
|
+
const assessment = await this.loadAttemptAssessment(attempt.metadata);
|
|
91837
|
+
const transition = prepareAssessmentItemSubmission({
|
|
91838
|
+
purpose: attempt.metadata.purpose,
|
|
91839
|
+
responseVersion: attempt.metadata.responseVersion,
|
|
91840
|
+
responses: attempt.metadata.responses,
|
|
91841
|
+
itemSubmissions: attempt.metadata.itemSubmissions,
|
|
91842
|
+
assessment
|
|
91843
|
+
}, input);
|
|
91844
|
+
const scoring = transition.action === "commit" ? await this.scoreItem(assessment, transition.responses, input.itemIdentifier) : null;
|
|
91845
|
+
return {
|
|
91846
|
+
responseVersion: attempt.metadata.responseVersion,
|
|
91847
|
+
submissionId: input.submissionId,
|
|
91848
|
+
itemIdentifier: input.itemIdentifier,
|
|
91849
|
+
assessment,
|
|
91850
|
+
scoring
|
|
91851
|
+
};
|
|
91852
|
+
}
|
|
91853
|
+
async scoreSubmission(attempt) {
|
|
91854
|
+
const assessment = await this.loadAttemptAssessment(attempt.metadata);
|
|
91855
|
+
validateAssessmentResponses(assessment, attempt.metadata.responses);
|
|
91856
|
+
const itemResults = await this.scoreItems(assessment, attempt.metadata.responses);
|
|
91564
91857
|
return {
|
|
91565
91858
|
responseVersion: attempt.metadata.responseVersion,
|
|
91566
91859
|
assessment,
|
|
@@ -91630,6 +91923,13 @@ class TimebackAssessmentRuntimeService {
|
|
|
91630
91923
|
throw AssessmentRuntimeError.from(assessmentAlreadySubmitted(attempt.result.sourcedId));
|
|
91631
91924
|
}
|
|
91632
91925
|
this.assertResponseVersion(attempt.metadata, input.expectedResponseVersion);
|
|
91926
|
+
if (assessmentFlowForPurpose(attempt.metadata.purpose) === "item-submit") {
|
|
91927
|
+
const assessment2 = await this.loadAttemptAssessment(attempt.metadata);
|
|
91928
|
+
const flowFailure = assessmentCompletionFlowFailure(attempt.metadata.purpose, attempt.metadata.itemSubmissions, assessment2.items.length);
|
|
91929
|
+
if (flowFailure) {
|
|
91930
|
+
throw AssessmentRuntimeError.from(flowFailure);
|
|
91931
|
+
}
|
|
91932
|
+
}
|
|
91633
91933
|
const scored = preview && preview.responseVersion === attempt.metadata.responseVersion ? preview : await this.scoreSubmission(attempt);
|
|
91634
91934
|
const { assessment, itemResults, score, correctQuestions } = scored;
|
|
91635
91935
|
const client2 = this.requireClient();
|
|
@@ -92272,18 +92572,105 @@ class TimebackAssessmentRuntimeService {
|
|
|
92272
92572
|
}
|
|
92273
92573
|
async putReviewChildResponses(result, metadata2, changedItemIdentifiers) {
|
|
92274
92574
|
await runWithConcurrency(metadata2.review.selections.filter((selection) => changedItemIdentifiers.has(selection.itemIdentifier)), TimebackAssessmentRuntimeService.SCORING_CONCURRENCY, async (selection) => {
|
|
92275
|
-
|
|
92276
|
-
|
|
92277
|
-
|
|
92278
|
-
|
|
92279
|
-
|
|
92280
|
-
|
|
92575
|
+
try {
|
|
92576
|
+
const childLineItemId = await reviewQuestionLineItemId(result.assessmentLineItem.sourcedId, selection.itemIdentifier);
|
|
92577
|
+
const childResultId = await reviewQuestionResultId(result.sourcedId, childLineItemId);
|
|
92578
|
+
const childMetadata = buildReviewItemResultMetadata({
|
|
92579
|
+
metadata: metadata2,
|
|
92580
|
+
selection,
|
|
92581
|
+
parentAttemptId: result.sourcedId
|
|
92582
|
+
});
|
|
92583
|
+
await this.requireClient().api.oneroster.assessmentResults.upsert(childResultId, buildOpenReviewChildResult({
|
|
92584
|
+
childLineItemId,
|
|
92585
|
+
student: result.student,
|
|
92586
|
+
metadata: childMetadata
|
|
92587
|
+
}));
|
|
92588
|
+
} catch (error88) {
|
|
92589
|
+
addEvent("assessment.review_child_response_projection_failed", {
|
|
92590
|
+
"app.assessment.attempt_id": result.sourcedId,
|
|
92591
|
+
"app.assessment.qti_item_identifier": selection.itemIdentifier,
|
|
92592
|
+
"exception.type": errorType(error88),
|
|
92593
|
+
"app.error.message": errorMessage(error88)
|
|
92594
|
+
});
|
|
92595
|
+
}
|
|
92596
|
+
});
|
|
92597
|
+
}
|
|
92598
|
+
async projectReviewItemResponse(params, projection) {
|
|
92599
|
+
await this.putReviewChildResponses(projection.result, projection.metadata, new Set([projection.itemIdentifier]));
|
|
92600
|
+
try {
|
|
92601
|
+
await this.crossAttemptLockBarrier(params.attemptId);
|
|
92602
|
+
const latest = await this.peekAttempt(params);
|
|
92603
|
+
if (isAssessmentAttemptCompleted({
|
|
92604
|
+
inProgress: latest.result.inProgress ?? "",
|
|
92605
|
+
scoreStatus: latest.result.scoreStatus
|
|
92606
|
+
}) && latest.metadata.purpose === "review") {
|
|
92607
|
+
await this.restoreCompletedReviewChildResponses(latest.result, latest.metadata, new Set([projection.itemIdentifier]));
|
|
92608
|
+
}
|
|
92609
|
+
} catch (error88) {
|
|
92610
|
+
addEvent("assessment.review_child_projection_barrier_failed", {
|
|
92611
|
+
"app.assessment.attempt_id": params.attemptId,
|
|
92612
|
+
"app.assessment.qti_item_identifier": projection.itemIdentifier,
|
|
92613
|
+
"exception.type": errorType(error88),
|
|
92614
|
+
"app.error.message": errorMessage(error88)
|
|
92281
92615
|
});
|
|
92282
|
-
|
|
92283
|
-
|
|
92284
|
-
|
|
92285
|
-
|
|
92286
|
-
|
|
92616
|
+
}
|
|
92617
|
+
}
|
|
92618
|
+
async crossAttemptLockBarrier(attemptId) {
|
|
92619
|
+
for (let attempt = 1;attempt <= TimebackAssessmentRuntimeService.PROJECTION_BARRIER_MAX_ATTEMPTS; attempt += 1) {
|
|
92620
|
+
try {
|
|
92621
|
+
await this.deps.assessmentRuntimeLock(this.deps.db, assessmentRuntimeAttemptLockKey(attemptId), async () => {
|
|
92622
|
+
return;
|
|
92623
|
+
});
|
|
92624
|
+
return;
|
|
92625
|
+
} catch (error88) {
|
|
92626
|
+
if (!(error88 instanceof ServiceUnavailableError) || attempt === TimebackAssessmentRuntimeService.PROJECTION_BARRIER_MAX_ATTEMPTS) {
|
|
92627
|
+
throw error88;
|
|
92628
|
+
}
|
|
92629
|
+
await sleep(TimebackAssessmentRuntimeService.PROJECTION_BARRIER_RETRY_DELAY_MS);
|
|
92630
|
+
}
|
|
92631
|
+
}
|
|
92632
|
+
}
|
|
92633
|
+
async restoreCompletedReviewChildResponses(result, metadata2, changedItemIdentifiers) {
|
|
92634
|
+
const outcomes = metadata2.completion?.itemOutcomes;
|
|
92635
|
+
const submissionId = metadata2.submissionId;
|
|
92636
|
+
if (!submissionId || !outcomes) {
|
|
92637
|
+
addEvent("assessment.review_child_terminal_restore_unavailable", {
|
|
92638
|
+
"app.assessment.attempt_id": result.sourcedId
|
|
92639
|
+
});
|
|
92640
|
+
return;
|
|
92641
|
+
}
|
|
92642
|
+
const outcomeByItem = new Map(outcomes.map((outcome) => [outcome.itemIdentifier, outcome]));
|
|
92643
|
+
await runWithConcurrency(metadata2.review.selections.filter((selection) => changedItemIdentifiers.has(selection.itemIdentifier)), TimebackAssessmentRuntimeService.SCORING_CONCURRENCY, async (selection) => {
|
|
92644
|
+
try {
|
|
92645
|
+
const outcome = outcomeByItem.get(selection.itemIdentifier);
|
|
92646
|
+
if (!outcome) {
|
|
92647
|
+
throw new Error(`Completed review omitted item ${selection.itemIdentifier}`);
|
|
92648
|
+
}
|
|
92649
|
+
const childLineItemId = await reviewQuestionLineItemId(result.assessmentLineItem.sourcedId, selection.itemIdentifier);
|
|
92650
|
+
const childResultId = await reviewQuestionResultId(result.sourcedId, childLineItemId);
|
|
92651
|
+
const finalized = buildFinalizedReviewChildResult({
|
|
92652
|
+
childLineItemId,
|
|
92653
|
+
student: result.student,
|
|
92654
|
+
parentAttemptId: result.sourcedId,
|
|
92655
|
+
metadata: metadata2,
|
|
92656
|
+
selection,
|
|
92657
|
+
itemResult: {
|
|
92658
|
+
itemIdentifier: outcome.itemIdentifier,
|
|
92659
|
+
score: outcome.score,
|
|
92660
|
+
isCorrect: outcome.isCorrect
|
|
92661
|
+
},
|
|
92662
|
+
submissionId,
|
|
92663
|
+
timestamp: result.scoreDate
|
|
92664
|
+
});
|
|
92665
|
+
await this.requireClient().api.oneroster.assessmentResults.upsert(childResultId, finalized.resultUpdate);
|
|
92666
|
+
} catch (error88) {
|
|
92667
|
+
addEvent("assessment.review_child_terminal_restore_failed", {
|
|
92668
|
+
"app.assessment.attempt_id": result.sourcedId,
|
|
92669
|
+
"app.assessment.qti_item_identifier": selection.itemIdentifier,
|
|
92670
|
+
"exception.type": errorType(error88),
|
|
92671
|
+
"app.error.message": errorMessage(error88)
|
|
92672
|
+
});
|
|
92673
|
+
}
|
|
92287
92674
|
});
|
|
92288
92675
|
}
|
|
92289
92676
|
async finalizeReviewChildResults(input) {
|
|
@@ -92364,8 +92751,10 @@ class TimebackAssessmentRuntimeService {
|
|
|
92364
92751
|
attemptId: result.sourcedId,
|
|
92365
92752
|
responseVersion: metadata2.responseVersion,
|
|
92366
92753
|
status: completed ? "completed" : "in_progress",
|
|
92754
|
+
flow: assessmentFlowForPurpose(metadata2.purpose),
|
|
92367
92755
|
assessment: assessmentPresentationForAttempt(assessment, result.sourcedId),
|
|
92368
92756
|
responses: metadata2.responses,
|
|
92757
|
+
itemSubmissions: metadata2.itemSubmissions,
|
|
92369
92758
|
score: completed ? this.scoreFromResult(result, assessmentPossibleScore(assessment), metadata2) : null,
|
|
92370
92759
|
selection
|
|
92371
92760
|
};
|
|
@@ -154074,7 +154463,7 @@ function parseQtiLibraryParams(searchParams) {
|
|
|
154074
154463
|
limit
|
|
154075
154464
|
};
|
|
154076
154465
|
}
|
|
154077
|
-
var populateStudent, getUser, getUserEnrollments, getUserById, setupIntegration, getIntegrations, getRemovedIntegrations, createIntegration, updateIntegration, deactivateCourse, reactivateCourse, getIntegrationConfig, verifyIntegration, getConfig, deleteIntegrations, endActivity, heartbeat, advanceCourse, unenrollCourse, startRuntimeAssessment, getRuntimeAssessment, getLatestRuntimeAssessment, saveRuntimeAssessment, submitRuntimeAssessment, exportRuntimeAssessmentFixture, getStudentXp, getStudentMastery, getStudentHighestGradeMastered, getRoster, getStudentOverview, getGameMetrics, getStudentActivity, getGradeLevelTestResults, getGradeLevelTestReview, getActivityDetail, listMetricDiscrepancies, verifyMetricDiscrepancy, grantXp, adjustTime, adjustMastery, reconcileMasteryForConfigChange, searchStudents, enrollStudent, unenrollStudent, reactivateEnrollment, listAssessments, createAssessment, updateAssessment, reorderAssessments, reorderQuestions, removeAssessment, listQuestions, listQuestionLibrary, listTestLibrary, copyAssessment, createQuestion, updateQuestion, removeQuestion, timeback2;
|
|
154466
|
+
var populateStudent, getUser, getUserEnrollments, getUserById, setupIntegration, getIntegrations, getRemovedIntegrations, createIntegration, updateIntegration, deactivateCourse, reactivateCourse, getIntegrationConfig, verifyIntegration, getConfig, deleteIntegrations, endActivity, heartbeat, advanceCourse, unenrollCourse, startRuntimeAssessment, getRuntimeAssessment, getLatestRuntimeAssessment, saveRuntimeAssessment, submitRuntimeAssessmentItem, submitRuntimeAssessment, exportRuntimeAssessmentFixture, getStudentXp, getStudentMastery, getStudentHighestGradeMastered, getRoster, getStudentOverview, getGameMetrics, getStudentActivity, getGradeLevelTestResults, getGradeLevelTestReview, getActivityDetail, listMetricDiscrepancies, verifyMetricDiscrepancy, grantXp, adjustTime, adjustMastery, reconcileMasteryForConfigChange, searchStudents, enrollStudent, unenrollStudent, reactivateEnrollment, listAssessments, createAssessment, updateAssessment, reorderAssessments, reorderQuestions, removeAssessment, listQuestions, listQuestionLibrary, listTestLibrary, copyAssessment, createQuestion, updateQuestion, removeQuestion, timeback2;
|
|
154078
154467
|
var init_timeback_controller = __esm(() => {
|
|
154079
154468
|
init_esm();
|
|
154080
154469
|
init_src();
|
|
@@ -154382,6 +154771,25 @@ var init_timeback_controller = __esm(() => {
|
|
|
154382
154771
|
user: ctx.user
|
|
154383
154772
|
});
|
|
154384
154773
|
});
|
|
154774
|
+
submitRuntimeAssessmentItem = requireDeveloper(async (ctx) => {
|
|
154775
|
+
const attemptId = ctx.params.attemptId;
|
|
154776
|
+
const body2 = await parseRequestBody(ctx.request, SubmitAssessmentItemRuntimeRequestSchema);
|
|
154777
|
+
if (!attemptId) {
|
|
154778
|
+
throw ApiError.badRequest("Missing attemptId");
|
|
154779
|
+
}
|
|
154780
|
+
return ctx.services.timebackAssessmentRuntime.submitItem({
|
|
154781
|
+
gameId: body2.gameId,
|
|
154782
|
+
studentId: body2.studentId,
|
|
154783
|
+
attemptId,
|
|
154784
|
+
input: {
|
|
154785
|
+
expectedResponseVersion: body2.expectedResponseVersion,
|
|
154786
|
+
submissionId: body2.submissionId,
|
|
154787
|
+
itemIdentifier: body2.itemIdentifier,
|
|
154788
|
+
responses: body2.responses
|
|
154789
|
+
},
|
|
154790
|
+
user: ctx.user
|
|
154791
|
+
});
|
|
154792
|
+
});
|
|
154385
154793
|
submitRuntimeAssessment = requireDeveloper(async (ctx) => {
|
|
154386
154794
|
const attemptId = ctx.params.attemptId;
|
|
154387
154795
|
const body2 = await parseRequestBody(ctx.request, SubmitRuntimeAssessmentRequestSchema);
|
|
@@ -154833,6 +155241,7 @@ var init_timeback_controller = __esm(() => {
|
|
|
154833
155241
|
getLatestRuntimeAssessment,
|
|
154834
155242
|
getRuntimeAssessment,
|
|
154835
155243
|
saveRuntimeAssessment,
|
|
155244
|
+
submitRuntimeAssessmentItem,
|
|
154836
155245
|
submitRuntimeAssessment,
|
|
154837
155246
|
exportRuntimeAssessmentFixture,
|
|
154838
155247
|
getStudentXp,
|
|
@@ -155821,6 +156230,7 @@ var init_timeback8 = __esm(async () => {
|
|
|
155821
156230
|
timebackRouter.get("/assessments/latest", handle2(timeback2.getLatestRuntimeAssessment));
|
|
155822
156231
|
timebackRouter.get("/assessments/:attemptId", handle2(timeback2.getRuntimeAssessment));
|
|
155823
156232
|
timebackRouter.post("/assessments/:attemptId/save", handle2(timeback2.saveRuntimeAssessment));
|
|
156233
|
+
timebackRouter.post("/assessments/:attemptId/submit-item", handle2(timeback2.submitRuntimeAssessmentItem));
|
|
155824
156234
|
timebackRouter.post("/assessments/:attemptId/submit", handle2(timeback2.submitRuntimeAssessment));
|
|
155825
156235
|
timebackRouter.get("/user", async (c2) => {
|
|
155826
156236
|
const user = c2.get("user");
|