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