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