@playcademy/sandbox 0.7.1-beta.16 → 0.7.1-beta.18
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 +98 -68
- package/dist/constants.js +1 -1
- package/dist/server.js +98 -68
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -297,7 +297,7 @@ var init_timeback2 = __esm(() => {
|
|
|
297
297
|
ASSESSMENT_PURPOSES = ["end_of_course", "diagnostic", "review", "mastery"];
|
|
298
298
|
TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS = {
|
|
299
299
|
standards: 20,
|
|
300
|
-
|
|
300
|
+
candidateItemsPerStandard: 2,
|
|
301
301
|
standardFieldLength: 128
|
|
302
302
|
};
|
|
303
303
|
TIMEBACK_COURSE_DEFAULTS = {
|
|
@@ -1123,7 +1123,7 @@ var package_default;
|
|
|
1123
1123
|
var init_package = __esm(() => {
|
|
1124
1124
|
package_default = {
|
|
1125
1125
|
name: "@playcademy/sandbox",
|
|
1126
|
-
version: "0.7.1-beta.
|
|
1126
|
+
version: "0.7.1-beta.18",
|
|
1127
1127
|
description: "Local development server for Playcademy game development",
|
|
1128
1128
|
type: "module",
|
|
1129
1129
|
exports: {
|
|
@@ -9490,7 +9490,7 @@ function assessmentReviewBankShortage(fulfillment) {
|
|
|
9490
9490
|
};
|
|
9491
9491
|
}
|
|
9492
9492
|
function assessmentFlowForPurpose(purpose) {
|
|
9493
|
-
return purpose === "review"
|
|
9493
|
+
return purpose === "review" ? "item-submit" : "attempt-submit";
|
|
9494
9494
|
}
|
|
9495
9495
|
function isObject(value) {
|
|
9496
9496
|
return typeof value === "object" && value !== null;
|
|
@@ -9692,6 +9692,12 @@ function interactionResponseValidationMessage(itemIdentifier, interaction, respo
|
|
|
9692
9692
|
function invalidResponse(message) {
|
|
9693
9693
|
return { code: "INVALID_RESPONSE", message };
|
|
9694
9694
|
}
|
|
9695
|
+
function administeredItemIdentifiers(itemSubmissions) {
|
|
9696
|
+
return new Set(itemSubmissions.map((submission) => submission.itemIdentifier));
|
|
9697
|
+
}
|
|
9698
|
+
function itemAdministration(itemSubmissions, itemIdentifier) {
|
|
9699
|
+
return itemSubmissions.find((submission) => submission.itemIdentifier === itemIdentifier);
|
|
9700
|
+
}
|
|
9695
9701
|
function responseValuesEqual(left, right) {
|
|
9696
9702
|
if (left === undefined || right === undefined) {
|
|
9697
9703
|
return left === right;
|
|
@@ -9742,15 +9748,15 @@ function prepareAssessmentItemSubmission(state, input) {
|
|
|
9742
9748
|
failure: assessmentResponseVersionConflict(input.expectedResponseVersion, state.responseVersion)
|
|
9743
9749
|
};
|
|
9744
9750
|
}
|
|
9745
|
-
const
|
|
9746
|
-
const
|
|
9747
|
-
const
|
|
9748
|
-
if (
|
|
9751
|
+
const itemIndex = state.assessment.items.findIndex((item) => item.identifier === input.itemIdentifier);
|
|
9752
|
+
const previousItemIdentifier = state.itemSubmissions.at(-1)?.itemIdentifier;
|
|
9753
|
+
const previousItemIndex = state.assessment.items.findIndex((item) => item.identifier === previousItemIdentifier);
|
|
9754
|
+
if (itemIndex === -1 || itemIndex <= previousItemIndex) {
|
|
9749
9755
|
return {
|
|
9750
9756
|
action: "reject",
|
|
9751
|
-
failure: assessmentFlowViolation("Items must be submitted once and in
|
|
9757
|
+
failure: assessmentFlowViolation("Items must be submitted once and in candidate order.", {
|
|
9752
9758
|
itemIdentifier: input.itemIdentifier,
|
|
9753
|
-
|
|
9759
|
+
previousItemIdentifier: previousItemIdentifier ?? null,
|
|
9754
9760
|
flow
|
|
9755
9761
|
})
|
|
9756
9762
|
};
|
|
@@ -9773,10 +9779,11 @@ function prepareAssessmentItemSubmission(state, input) {
|
|
|
9773
9779
|
answered: Object.keys(responses[input.itemIdentifier] ?? {}).length > 0
|
|
9774
9780
|
};
|
|
9775
9781
|
}
|
|
9776
|
-
function completeAssessmentItemSubmission(state, input, prepared, scoring) {
|
|
9782
|
+
function completeAssessmentItemSubmission(state, input, prepared, scoring, submittedAt) {
|
|
9777
9783
|
const submission = {
|
|
9778
9784
|
submissionId: input.submissionId,
|
|
9779
9785
|
itemIdentifier: input.itemIdentifier,
|
|
9786
|
+
submittedAt,
|
|
9780
9787
|
responseVersion: prepared.responseVersion,
|
|
9781
9788
|
answered: prepared.answered,
|
|
9782
9789
|
score: scoring.score,
|
|
@@ -9790,8 +9797,8 @@ function completeAssessmentItemSubmission(state, input, prepared, scoring) {
|
|
|
9790
9797
|
};
|
|
9791
9798
|
}
|
|
9792
9799
|
function assessmentResponseUpdateFlowFailure(purpose, itemSubmissions, update) {
|
|
9793
|
-
const
|
|
9794
|
-
const committedUpdate = Object.keys(update).find((identifier) =>
|
|
9800
|
+
const administered = administeredItemIdentifiers(itemSubmissions);
|
|
9801
|
+
const committedUpdate = Object.keys(update).find((identifier) => administered.has(identifier));
|
|
9795
9802
|
return committedUpdate ? assessmentFlowViolation("A submitted item response cannot be edited.", {
|
|
9796
9803
|
itemIdentifier: committedUpdate,
|
|
9797
9804
|
flow: assessmentFlowForPurpose(purpose)
|
|
@@ -9799,11 +9806,17 @@ function assessmentResponseUpdateFlowFailure(purpose, itemSubmissions, update) {
|
|
|
9799
9806
|
}
|
|
9800
9807
|
function assessmentCompletionFlowFailure(purpose, itemSubmissions, itemCount) {
|
|
9801
9808
|
const flow = assessmentFlowForPurpose(purpose);
|
|
9802
|
-
|
|
9809
|
+
if (flow !== "item-submit") {
|
|
9810
|
+
return null;
|
|
9811
|
+
}
|
|
9812
|
+
if (itemSubmissions.length > 0 && itemSubmissions.length <= itemCount) {
|
|
9813
|
+
return null;
|
|
9814
|
+
}
|
|
9815
|
+
return assessmentFlowViolation("At least one candidate must be administered before the review can be completed.", {
|
|
9803
9816
|
flow,
|
|
9804
9817
|
submittedItemCount: itemSubmissions.length,
|
|
9805
9818
|
itemCount
|
|
9806
|
-
})
|
|
9819
|
+
});
|
|
9807
9820
|
}
|
|
9808
9821
|
function selectAssessmentCourseCandidate(candidates, subjectSpecified) {
|
|
9809
9822
|
if (candidates.length === 0) {
|
|
@@ -9979,15 +9992,15 @@ function canonicalRequestStandards(standards) {
|
|
|
9979
9992
|
return [...canonical.values()].toSorted((left, right) => compareCodeUnits(assessmentStandardRefKey(left), assessmentStandardRefKey(right)));
|
|
9980
9993
|
}
|
|
9981
9994
|
function normalizeReviewRequest(request) {
|
|
9982
|
-
const
|
|
9983
|
-
if (!Number.isInteger(
|
|
9984
|
-
throw new RangeError(`
|
|
9995
|
+
const candidateItemsPerStandard = request.candidateItemsPerStandard ?? 1;
|
|
9996
|
+
if (!Number.isInteger(candidateItemsPerStandard) || candidateItemsPerStandard <= 0 || candidateItemsPerStandard > TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.candidateItemsPerStandard) {
|
|
9997
|
+
throw new RangeError(`candidateItemsPerStandard must be a positive integer at most ${TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.candidateItemsPerStandard}`);
|
|
9985
9998
|
}
|
|
9986
9999
|
const standards = canonicalRequestStandards(request.standards);
|
|
9987
10000
|
if (standards.length === 0) {
|
|
9988
10001
|
throw new Error("At least one review standard is required");
|
|
9989
10002
|
}
|
|
9990
|
-
return { standards,
|
|
10003
|
+
return { standards, candidateItemsPerStandard };
|
|
9991
10004
|
}
|
|
9992
10005
|
function reviewRequestFingerprint(request, policy) {
|
|
9993
10006
|
const normalized = normalizeReviewRequest(request);
|
|
@@ -9996,7 +10009,7 @@ function reviewRequestFingerprint(request, policy) {
|
|
|
9996
10009
|
policy: {
|
|
9997
10010
|
version: policy.version
|
|
9998
10011
|
},
|
|
9999
|
-
|
|
10012
|
+
candidateItemsPerStandard: normalized.candidateItemsPerStandard,
|
|
10000
10013
|
standardKeys: normalized.standards.map(assessmentStandardRefKey)
|
|
10001
10014
|
});
|
|
10002
10015
|
}
|
|
@@ -10096,7 +10109,7 @@ function rankedCandidatesByStandard(bank, exposures) {
|
|
|
10096
10109
|
return byStandard;
|
|
10097
10110
|
}
|
|
10098
10111
|
function reviewSlots(request) {
|
|
10099
|
-
return request.standards.flatMap((standard) => Array.from({ length: request.
|
|
10112
|
+
return request.standards.flatMap((standard) => Array.from({ length: request.candidateItemsPerStandard }, () => ({
|
|
10100
10113
|
standard,
|
|
10101
10114
|
standardKey: assessmentStandardRefKey(standard)
|
|
10102
10115
|
})));
|
|
@@ -10200,7 +10213,7 @@ function preferredDistinctMatching(slots, candidatesByStandard) {
|
|
|
10200
10213
|
return new Map(slotItemEdges.filter((candidate) => candidate.edge.capacity === 0).map((candidate) => [candidate.slotIndex, candidate.itemIdentifier]));
|
|
10201
10214
|
}
|
|
10202
10215
|
function fulfillment(request, candidatesByStandard, selections) {
|
|
10203
|
-
const requestedItemCount = request.standards.length * request.
|
|
10216
|
+
const requestedItemCount = request.standards.length * request.candidateItemsPerStandard;
|
|
10204
10217
|
const selectedByStandard = new Map;
|
|
10205
10218
|
for (const selection of selections) {
|
|
10206
10219
|
const key = assessmentStandardRefKey(selection.standard);
|
|
@@ -10209,10 +10222,10 @@ function fulfillment(request, candidatesByStandard, selections) {
|
|
|
10209
10222
|
const shortages = request.standards.flatMap((standard) => {
|
|
10210
10223
|
const key = assessmentStandardRefKey(standard);
|
|
10211
10224
|
const selected = selectedByStandard.get(key) ?? 0;
|
|
10212
|
-
return selected < request.
|
|
10225
|
+
return selected < request.candidateItemsPerStandard ? [
|
|
10213
10226
|
{
|
|
10214
10227
|
standard,
|
|
10215
|
-
requested: request.
|
|
10228
|
+
requested: request.candidateItemsPerStandard,
|
|
10216
10229
|
selected,
|
|
10217
10230
|
eligible: candidatesByStandard.get(key)?.length ?? 0
|
|
10218
10231
|
}
|
|
@@ -10254,9 +10267,9 @@ function projectReviewAssessment(assessment, bank, selections) {
|
|
|
10254
10267
|
throw new Error(`Review bank ${bank.bankRevision} was built from content revision ${bank.sourceContentRevision}, not ${assessment.contentRevision}`);
|
|
10255
10268
|
}
|
|
10256
10269
|
const selectedIdentifiers = new Set(selections.map((selection) => selection.itemIdentifier));
|
|
10257
|
-
const
|
|
10270
|
+
const itemByIdentifier = new Map(assessment.items.map((item) => [item.identifier, item]));
|
|
10258
10271
|
const indexedItems = new Map(bank.items.map((item) => [item.itemIdentifier, item]));
|
|
10259
|
-
const missingIdentifiers = [...selectedIdentifiers].filter((identifier) => !
|
|
10272
|
+
const missingIdentifiers = [...selectedIdentifiers].filter((identifier) => !itemByIdentifier.has(identifier) || !indexedItems.has(identifier));
|
|
10260
10273
|
if (missingIdentifiers.length > 0) {
|
|
10261
10274
|
throw new Error(`Selected review items are absent from the pinned bank: ${missingIdentifiers.join(", ")}`);
|
|
10262
10275
|
}
|
|
@@ -10272,7 +10285,7 @@ function projectReviewAssessment(assessment, bank, selections) {
|
|
|
10272
10285
|
return structuredClone({
|
|
10273
10286
|
...assessment,
|
|
10274
10287
|
contentRevision: bank.bankRevision,
|
|
10275
|
-
items:
|
|
10288
|
+
items: selections.map((selection) => itemByIdentifier.get(selection.itemIdentifier))
|
|
10276
10289
|
});
|
|
10277
10290
|
}
|
|
10278
10291
|
function assessmentItemCorrectness(verdicts) {
|
|
@@ -10331,10 +10344,10 @@ function isReviewItemOutcome(value) {
|
|
|
10331
10344
|
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");
|
|
10332
10345
|
}
|
|
10333
10346
|
function isAssessmentItemSubmission(value) {
|
|
10334
|
-
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");
|
|
10347
|
+
return isRecord(value) && typeof value.submissionId === "string" && value.submissionId.length > 0 && typeof value.itemIdentifier === "string" && value.itemIdentifier.length > 0 && typeof value.submittedAt === "string" && Number.isInteger(value.responseVersion) && typeof value.answered === "boolean" && isAssessmentScore(value.score) && (value.isCorrect === null || typeof value.isCorrect === "boolean");
|
|
10335
10348
|
}
|
|
10336
10349
|
function isReviewAttemptMetadata(value) {
|
|
10337
|
-
return isRecord(value) && typeof value.requestFingerprint === "string" && typeof value.bankRevision === "string" && Array.isArray(value.standards) && value.standards.every(isAssessmentStandardRef) && Number.isInteger(value.
|
|
10350
|
+
return isRecord(value) && typeof value.requestFingerprint === "string" && typeof value.bankRevision === "string" && Array.isArray(value.standards) && value.standards.every(isAssessmentStandardRef) && Number.isInteger(value.candidateItemsPerStandard) && Array.isArray(value.selections) && value.selections.every((selection) => isRecord(selection) && isAssessmentStandardRef(selection.standard) && typeof selection.itemIdentifier === "string") && isReviewFulfillment(value.fulfillment);
|
|
10338
10351
|
}
|
|
10339
10352
|
function isMasteryAttemptMetadata(value) {
|
|
10340
10353
|
return isRecord(value) && typeof value.requestFingerprint === "string" && isAssessmentStandardRef(value.standard);
|
|
@@ -10644,7 +10657,7 @@ var init_assessment_runtime = __esm(() => {
|
|
|
10644
10657
|
StartAssessmentBaseSchema.extend({
|
|
10645
10658
|
purpose: exports_external.literal("review"),
|
|
10646
10659
|
standards: exports_external.array(AssessmentStandardRefSchema).min(1).max(TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.standards),
|
|
10647
|
-
|
|
10660
|
+
candidateItemsPerStandard: exports_external.number().int().positive().max(TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.candidateItemsPerStandard).optional()
|
|
10648
10661
|
}),
|
|
10649
10662
|
StartAssessmentBaseSchema.extend({
|
|
10650
10663
|
purpose: exports_external.literal("mastery"),
|
|
@@ -90938,6 +90951,10 @@ var init_timeback4 = __esm(() => {
|
|
|
90938
90951
|
|
|
90939
90952
|
// ../api-core/src/utils/timeback-assessment-runtime.util.ts
|
|
90940
90953
|
function buildReviewItemResultMetadata(input) {
|
|
90954
|
+
const administration = itemAdministration(input.metadata.itemSubmissions, input.selection.itemIdentifier);
|
|
90955
|
+
if (!administration) {
|
|
90956
|
+
throw new Error(`Cannot create a review result for unadministered candidate ${input.selection.itemIdentifier}`);
|
|
90957
|
+
}
|
|
90941
90958
|
return {
|
|
90942
90959
|
version: PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_VERSION,
|
|
90943
90960
|
parentAttemptId: input.parentAttemptId,
|
|
@@ -90947,7 +90964,7 @@ function buildReviewItemResultMetadata(input) {
|
|
|
90947
90964
|
bankRevision: input.metadata.review.bankRevision,
|
|
90948
90965
|
itemIdentifier: input.selection.itemIdentifier,
|
|
90949
90966
|
standard: input.selection.standard,
|
|
90950
|
-
deliveredAt:
|
|
90967
|
+
deliveredAt: administration.submittedAt,
|
|
90951
90968
|
responseVersion: input.responseVersion ?? input.metadata.responseVersion,
|
|
90952
90969
|
responses: input.metadata.responses[input.selection.itemIdentifier] ?? {},
|
|
90953
90970
|
...input.submissionId ? { submissionId: input.submissionId } : {},
|
|
@@ -91002,7 +91019,8 @@ function resolveReviewChildResult(input) {
|
|
|
91002
91019
|
}
|
|
91003
91020
|
const childMetadata = playcademyAssessmentItemResultMetadata(input.childResult.metadata);
|
|
91004
91021
|
const expectedResponses = input.parentMetadata.responses[input.selection.itemIdentifier] ?? {};
|
|
91005
|
-
|
|
91022
|
+
const administration = itemAdministration(input.parentMetadata.itemSubmissions, input.selection.itemIdentifier);
|
|
91023
|
+
if (input.childResult.assessmentLineItem.sourcedId !== input.childLineItemId || input.childResult.student.sourcedId !== input.parentResult.student.sourcedId || !childMetadata || childMetadata.parentAttemptId !== input.parentResult.sourcedId || childMetadata.activityId !== input.parentMetadata.activityId || childMetadata.integrationId !== input.parentMetadata.integrationId || childMetadata.enrollmentId !== input.parentMetadata.enrollmentId || childMetadata.bankRevision !== input.parentMetadata.review.bankRevision || childMetadata.itemIdentifier !== input.selection.itemIdentifier || childMetadata.standard.framework !== input.selection.standard.framework || childMetadata.standard.identifier !== input.selection.standard.identifier || childMetadata.deliveredAt !== administration?.submittedAt || childMetadata.responseVersion > input.parentMetadata.responseVersion || canonicalJson(childMetadata.responses) !== canonicalJson(expectedResponses)) {
|
|
91006
91024
|
return { action: "restore", reason: "incompatible" };
|
|
91007
91025
|
}
|
|
91008
91026
|
return input.childResult.status === "active" ? { action: "reuse", metadata: childMetadata } : { action: "restore", reason: "inactive" };
|
|
@@ -91693,7 +91711,7 @@ class TimebackAssessmentRuntimeService {
|
|
|
91693
91711
|
requestFingerprint,
|
|
91694
91712
|
bankRevision: source.bank.bankRevision,
|
|
91695
91713
|
standards: [...normalizedRequest.standards],
|
|
91696
|
-
|
|
91714
|
+
candidateItemsPerStandard: normalizedRequest.candidateItemsPerStandard,
|
|
91697
91715
|
selections: [...selected.selections],
|
|
91698
91716
|
fulfillment: selected.fulfillment
|
|
91699
91717
|
}
|
|
@@ -91825,9 +91843,6 @@ class TimebackAssessmentRuntimeService {
|
|
|
91825
91843
|
updatedAt: new Date().toISOString()
|
|
91826
91844
|
};
|
|
91827
91845
|
await this.putResultMetadata(attempt.result, metadata2);
|
|
91828
|
-
if (metadata2.purpose === "review") {
|
|
91829
|
-
await this.putReviewChildResponses(attempt.result, metadata2, new Set(Object.keys(input.responses)));
|
|
91830
|
-
}
|
|
91831
91846
|
return {
|
|
91832
91847
|
attemptId: attempt.result.sourcedId,
|
|
91833
91848
|
responseVersion: metadata2.responseVersion,
|
|
@@ -91895,13 +91910,14 @@ class TimebackAssessmentRuntimeService {
|
|
|
91895
91910
|
if (!preview.scoring) {
|
|
91896
91911
|
throw new Error("Prepared item submission is missing its score");
|
|
91897
91912
|
}
|
|
91898
|
-
const
|
|
91913
|
+
const submittedAt = new Date().toISOString();
|
|
91914
|
+
const completed = completeAssessmentItemSubmission(attempt.metadata, input, transition, preview.scoring, submittedAt);
|
|
91899
91915
|
const metadata2 = {
|
|
91900
91916
|
...attempt.metadata,
|
|
91901
91917
|
responses: completed.responses,
|
|
91902
91918
|
responseVersion: completed.responseVersion,
|
|
91903
91919
|
itemSubmissions: completed.itemSubmissions,
|
|
91904
|
-
updatedAt:
|
|
91920
|
+
updatedAt: submittedAt
|
|
91905
91921
|
};
|
|
91906
91922
|
await this.putResultMetadata(attempt.result, metadata2);
|
|
91907
91923
|
return {
|
|
@@ -92006,12 +92022,13 @@ class TimebackAssessmentRuntimeService {
|
|
|
92006
92022
|
async scoreSubmission(attempt) {
|
|
92007
92023
|
const assessment = await this.loadAttemptAssessment(attempt.metadata);
|
|
92008
92024
|
validateAssessmentResponses(assessment, attempt.metadata.responses);
|
|
92009
|
-
const
|
|
92025
|
+
const administeredItems = attempt.metadata.purpose === "review" ? administeredItemIdentifiers(attempt.metadata.itemSubmissions) : undefined;
|
|
92026
|
+
const itemResults = await this.scoreItems(assessment, attempt.metadata.responses, administeredItems);
|
|
92010
92027
|
return {
|
|
92011
92028
|
responseVersion: attempt.metadata.responseVersion,
|
|
92012
92029
|
assessment,
|
|
92013
92030
|
itemResults,
|
|
92014
|
-
score: aggregateAssessmentScore(itemResults.map((result) => result.score.earned),
|
|
92031
|
+
score: aggregateAssessmentScore(itemResults.map((result) => result.score.earned), itemResults.reduce((total, result) => total + result.score.possible, 0)),
|
|
92015
92032
|
correctQuestions: countCorrectQuestions(itemResults.map((result) => result.isCorrect))
|
|
92016
92033
|
};
|
|
92017
92034
|
}
|
|
@@ -92104,7 +92121,7 @@ class TimebackAssessmentRuntimeService {
|
|
|
92104
92121
|
submissionId: input.submissionId,
|
|
92105
92122
|
timestamp: timestamp6,
|
|
92106
92123
|
testName: assessment.title,
|
|
92107
|
-
totalQuestions:
|
|
92124
|
+
totalQuestions: itemResults.length,
|
|
92108
92125
|
correctQuestions,
|
|
92109
92126
|
...input.session ? { session: input.session } : {},
|
|
92110
92127
|
...reviewOutcomes ? { itemOutcomes: reviewOutcomes } : {}
|
|
@@ -92135,7 +92152,7 @@ class TimebackAssessmentRuntimeService {
|
|
|
92135
92152
|
testIdentifier: assessment.identifier,
|
|
92136
92153
|
testName: assessment.title,
|
|
92137
92154
|
score,
|
|
92138
|
-
totalQuestions:
|
|
92155
|
+
totalQuestions: itemResults.length,
|
|
92139
92156
|
correctQuestions,
|
|
92140
92157
|
submissionId: input.submissionId,
|
|
92141
92158
|
gameId: params.gameId,
|
|
@@ -92679,11 +92696,21 @@ class TimebackAssessmentRuntimeService {
|
|
|
92679
92696
|
if (physicalItems.size !== input.metadata.review.selections.length) {
|
|
92680
92697
|
throw new Error("MVP review selection must use each physical item at most once");
|
|
92681
92698
|
}
|
|
92699
|
+
const administeredItems = administeredItemIdentifiers(input.metadata.itemSubmissions);
|
|
92682
92700
|
await runWithConcurrency(input.metadata.review.selections, TimebackAssessmentRuntimeService.SCORING_CONCURRENCY, async (selection) => {
|
|
92683
92701
|
const item = itemByIdentifier.get(selection.itemIdentifier);
|
|
92684
92702
|
if (!item) {
|
|
92685
92703
|
throw new Error(`Review assessment is missing selected item ${selection.itemIdentifier}`);
|
|
92686
92704
|
}
|
|
92705
|
+
if (!administeredItems.has(selection.itemIdentifier)) {
|
|
92706
|
+
await this.ensureReviewQuestionLineItem({
|
|
92707
|
+
parentLineItemId: input.result.assessmentLineItem.sourcedId,
|
|
92708
|
+
integration: input.integration,
|
|
92709
|
+
bank: input.source.bank,
|
|
92710
|
+
item
|
|
92711
|
+
});
|
|
92712
|
+
return;
|
|
92713
|
+
}
|
|
92687
92714
|
const childLineItemId = await reviewQuestionLineItemId(input.result.assessmentLineItem.sourcedId, item.identifier);
|
|
92688
92715
|
const childResultId = await reviewQuestionResultId(input.result.sourcedId, childLineItemId);
|
|
92689
92716
|
const streamedChildResult = input.existingChildResults.get(childResultId);
|
|
@@ -92730,33 +92757,35 @@ class TimebackAssessmentRuntimeService {
|
|
|
92730
92757
|
});
|
|
92731
92758
|
});
|
|
92732
92759
|
}
|
|
92733
|
-
async
|
|
92734
|
-
|
|
92735
|
-
|
|
92736
|
-
|
|
92737
|
-
|
|
92738
|
-
|
|
92739
|
-
|
|
92740
|
-
|
|
92741
|
-
|
|
92742
|
-
|
|
92743
|
-
|
|
92744
|
-
|
|
92745
|
-
|
|
92746
|
-
|
|
92747
|
-
|
|
92748
|
-
|
|
92749
|
-
|
|
92750
|
-
|
|
92751
|
-
|
|
92752
|
-
|
|
92753
|
-
|
|
92754
|
-
|
|
92755
|
-
|
|
92756
|
-
|
|
92760
|
+
async putReviewChildResponse(result, metadata2, itemIdentifier) {
|
|
92761
|
+
const selection = metadata2.review.selections.find((candidate) => candidate.itemIdentifier === itemIdentifier);
|
|
92762
|
+
if (!selection) {
|
|
92763
|
+
return;
|
|
92764
|
+
}
|
|
92765
|
+
try {
|
|
92766
|
+
const childLineItemId = await reviewQuestionLineItemId(result.assessmentLineItem.sourcedId, selection.itemIdentifier);
|
|
92767
|
+
const childResultId = await reviewQuestionResultId(result.sourcedId, childLineItemId);
|
|
92768
|
+
const childMetadata = buildReviewItemResultMetadata({
|
|
92769
|
+
metadata: metadata2,
|
|
92770
|
+
selection,
|
|
92771
|
+
parentAttemptId: result.sourcedId
|
|
92772
|
+
});
|
|
92773
|
+
await this.requireClient().api.oneroster.assessmentResults.upsert(childResultId, buildOpenReviewChildResult({
|
|
92774
|
+
childLineItemId,
|
|
92775
|
+
student: result.student,
|
|
92776
|
+
metadata: childMetadata
|
|
92777
|
+
}));
|
|
92778
|
+
} catch (error88) {
|
|
92779
|
+
addEvent("assessment.review_child_response_projection_failed", {
|
|
92780
|
+
"app.assessment.attempt_id": result.sourcedId,
|
|
92781
|
+
"app.assessment.qti_item_identifier": selection.itemIdentifier,
|
|
92782
|
+
"exception.type": errorType(error88),
|
|
92783
|
+
"app.error.message": errorMessage(error88)
|
|
92784
|
+
});
|
|
92785
|
+
}
|
|
92757
92786
|
}
|
|
92758
92787
|
async projectReviewItemResponse(params, projection) {
|
|
92759
|
-
await this.
|
|
92788
|
+
await this.putReviewChildResponse(projection.result, projection.metadata, projection.itemIdentifier);
|
|
92760
92789
|
try {
|
|
92761
92790
|
await this.crossAttemptLockBarrier(params.attemptId);
|
|
92762
92791
|
const latest = await this.peekAttempt(params);
|
|
@@ -92835,7 +92864,8 @@ class TimebackAssessmentRuntimeService {
|
|
|
92835
92864
|
}
|
|
92836
92865
|
async finalizeReviewChildResults(input) {
|
|
92837
92866
|
const resultByItem = new Map(input.itemResults.map((itemResult) => [itemResult.itemIdentifier, itemResult]));
|
|
92838
|
-
|
|
92867
|
+
const administeredItems = administeredItemIdentifiers(input.metadata.itemSubmissions);
|
|
92868
|
+
return runWithConcurrency(input.metadata.review.selections.filter((selection) => administeredItems.has(selection.itemIdentifier)), TimebackAssessmentRuntimeService.SCORING_CONCURRENCY, async (selection) => {
|
|
92839
92869
|
const itemResult = resultByItem.get(selection.itemIdentifier);
|
|
92840
92870
|
if (!itemResult) {
|
|
92841
92871
|
throw new Error(`Scoring omitted selected review item ${selection.itemIdentifier}`);
|
|
@@ -92951,7 +92981,7 @@ class TimebackAssessmentRuntimeService {
|
|
|
92951
92981
|
kind: "standards-review",
|
|
92952
92982
|
purpose: metadata2.purpose,
|
|
92953
92983
|
standards: [...metadata2.review.standards],
|
|
92954
|
-
|
|
92984
|
+
candidateItemsPerStandard: metadata2.review.candidateItemsPerStandard,
|
|
92955
92985
|
selections: [...metadata2.review.selections],
|
|
92956
92986
|
fulfillment: metadata2.review.fulfillment
|
|
92957
92987
|
};
|
package/dist/constants.js
CHANGED
|
@@ -112,7 +112,7 @@ var init_timeback = __esm(() => {
|
|
|
112
112
|
ASSESSMENT_PURPOSES = ["end_of_course", "diagnostic", "review", "mastery"];
|
|
113
113
|
TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS = {
|
|
114
114
|
standards: 20,
|
|
115
|
-
|
|
115
|
+
candidateItemsPerStandard: 2,
|
|
116
116
|
standardFieldLength: 128
|
|
117
117
|
};
|
|
118
118
|
TIMEBACK_COURSE_DEFAULTS = {
|
package/dist/server.js
CHANGED
|
@@ -296,7 +296,7 @@ var init_timeback2 = __esm(() => {
|
|
|
296
296
|
ASSESSMENT_PURPOSES = ["end_of_course", "diagnostic", "review", "mastery"];
|
|
297
297
|
TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS = {
|
|
298
298
|
standards: 20,
|
|
299
|
-
|
|
299
|
+
candidateItemsPerStandard: 2,
|
|
300
300
|
standardFieldLength: 128
|
|
301
301
|
};
|
|
302
302
|
TIMEBACK_COURSE_DEFAULTS = {
|
|
@@ -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.18",
|
|
1126
1126
|
description: "Local development server for Playcademy game development",
|
|
1127
1127
|
type: "module",
|
|
1128
1128
|
exports: {
|
|
@@ -9489,7 +9489,7 @@ function assessmentReviewBankShortage(fulfillment) {
|
|
|
9489
9489
|
};
|
|
9490
9490
|
}
|
|
9491
9491
|
function assessmentFlowForPurpose(purpose) {
|
|
9492
|
-
return purpose === "review"
|
|
9492
|
+
return purpose === "review" ? "item-submit" : "attempt-submit";
|
|
9493
9493
|
}
|
|
9494
9494
|
function isObject(value) {
|
|
9495
9495
|
return typeof value === "object" && value !== null;
|
|
@@ -9691,6 +9691,12 @@ function interactionResponseValidationMessage(itemIdentifier, interaction, respo
|
|
|
9691
9691
|
function invalidResponse(message) {
|
|
9692
9692
|
return { code: "INVALID_RESPONSE", message };
|
|
9693
9693
|
}
|
|
9694
|
+
function administeredItemIdentifiers(itemSubmissions) {
|
|
9695
|
+
return new Set(itemSubmissions.map((submission) => submission.itemIdentifier));
|
|
9696
|
+
}
|
|
9697
|
+
function itemAdministration(itemSubmissions, itemIdentifier) {
|
|
9698
|
+
return itemSubmissions.find((submission) => submission.itemIdentifier === itemIdentifier);
|
|
9699
|
+
}
|
|
9694
9700
|
function responseValuesEqual(left, right) {
|
|
9695
9701
|
if (left === undefined || right === undefined) {
|
|
9696
9702
|
return left === right;
|
|
@@ -9741,15 +9747,15 @@ function prepareAssessmentItemSubmission(state, input) {
|
|
|
9741
9747
|
failure: assessmentResponseVersionConflict(input.expectedResponseVersion, state.responseVersion)
|
|
9742
9748
|
};
|
|
9743
9749
|
}
|
|
9744
|
-
const
|
|
9745
|
-
const
|
|
9746
|
-
const
|
|
9747
|
-
if (
|
|
9750
|
+
const itemIndex = state.assessment.items.findIndex((item) => item.identifier === input.itemIdentifier);
|
|
9751
|
+
const previousItemIdentifier = state.itemSubmissions.at(-1)?.itemIdentifier;
|
|
9752
|
+
const previousItemIndex = state.assessment.items.findIndex((item) => item.identifier === previousItemIdentifier);
|
|
9753
|
+
if (itemIndex === -1 || itemIndex <= previousItemIndex) {
|
|
9748
9754
|
return {
|
|
9749
9755
|
action: "reject",
|
|
9750
|
-
failure: assessmentFlowViolation("Items must be submitted once and in
|
|
9756
|
+
failure: assessmentFlowViolation("Items must be submitted once and in candidate order.", {
|
|
9751
9757
|
itemIdentifier: input.itemIdentifier,
|
|
9752
|
-
|
|
9758
|
+
previousItemIdentifier: previousItemIdentifier ?? null,
|
|
9753
9759
|
flow
|
|
9754
9760
|
})
|
|
9755
9761
|
};
|
|
@@ -9772,10 +9778,11 @@ function prepareAssessmentItemSubmission(state, input) {
|
|
|
9772
9778
|
answered: Object.keys(responses[input.itemIdentifier] ?? {}).length > 0
|
|
9773
9779
|
};
|
|
9774
9780
|
}
|
|
9775
|
-
function completeAssessmentItemSubmission(state, input, prepared, scoring) {
|
|
9781
|
+
function completeAssessmentItemSubmission(state, input, prepared, scoring, submittedAt) {
|
|
9776
9782
|
const submission = {
|
|
9777
9783
|
submissionId: input.submissionId,
|
|
9778
9784
|
itemIdentifier: input.itemIdentifier,
|
|
9785
|
+
submittedAt,
|
|
9779
9786
|
responseVersion: prepared.responseVersion,
|
|
9780
9787
|
answered: prepared.answered,
|
|
9781
9788
|
score: scoring.score,
|
|
@@ -9789,8 +9796,8 @@ function completeAssessmentItemSubmission(state, input, prepared, scoring) {
|
|
|
9789
9796
|
};
|
|
9790
9797
|
}
|
|
9791
9798
|
function assessmentResponseUpdateFlowFailure(purpose, itemSubmissions, update) {
|
|
9792
|
-
const
|
|
9793
|
-
const committedUpdate = Object.keys(update).find((identifier) =>
|
|
9799
|
+
const administered = administeredItemIdentifiers(itemSubmissions);
|
|
9800
|
+
const committedUpdate = Object.keys(update).find((identifier) => administered.has(identifier));
|
|
9794
9801
|
return committedUpdate ? assessmentFlowViolation("A submitted item response cannot be edited.", {
|
|
9795
9802
|
itemIdentifier: committedUpdate,
|
|
9796
9803
|
flow: assessmentFlowForPurpose(purpose)
|
|
@@ -9798,11 +9805,17 @@ function assessmentResponseUpdateFlowFailure(purpose, itemSubmissions, update) {
|
|
|
9798
9805
|
}
|
|
9799
9806
|
function assessmentCompletionFlowFailure(purpose, itemSubmissions, itemCount) {
|
|
9800
9807
|
const flow = assessmentFlowForPurpose(purpose);
|
|
9801
|
-
|
|
9808
|
+
if (flow !== "item-submit") {
|
|
9809
|
+
return null;
|
|
9810
|
+
}
|
|
9811
|
+
if (itemSubmissions.length > 0 && itemSubmissions.length <= itemCount) {
|
|
9812
|
+
return null;
|
|
9813
|
+
}
|
|
9814
|
+
return assessmentFlowViolation("At least one candidate must be administered before the review can be completed.", {
|
|
9802
9815
|
flow,
|
|
9803
9816
|
submittedItemCount: itemSubmissions.length,
|
|
9804
9817
|
itemCount
|
|
9805
|
-
})
|
|
9818
|
+
});
|
|
9806
9819
|
}
|
|
9807
9820
|
function selectAssessmentCourseCandidate(candidates, subjectSpecified) {
|
|
9808
9821
|
if (candidates.length === 0) {
|
|
@@ -9978,15 +9991,15 @@ function canonicalRequestStandards(standards) {
|
|
|
9978
9991
|
return [...canonical.values()].toSorted((left, right) => compareCodeUnits(assessmentStandardRefKey(left), assessmentStandardRefKey(right)));
|
|
9979
9992
|
}
|
|
9980
9993
|
function normalizeReviewRequest(request) {
|
|
9981
|
-
const
|
|
9982
|
-
if (!Number.isInteger(
|
|
9983
|
-
throw new RangeError(`
|
|
9994
|
+
const candidateItemsPerStandard = request.candidateItemsPerStandard ?? 1;
|
|
9995
|
+
if (!Number.isInteger(candidateItemsPerStandard) || candidateItemsPerStandard <= 0 || candidateItemsPerStandard > TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.candidateItemsPerStandard) {
|
|
9996
|
+
throw new RangeError(`candidateItemsPerStandard must be a positive integer at most ${TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.candidateItemsPerStandard}`);
|
|
9984
9997
|
}
|
|
9985
9998
|
const standards = canonicalRequestStandards(request.standards);
|
|
9986
9999
|
if (standards.length === 0) {
|
|
9987
10000
|
throw new Error("At least one review standard is required");
|
|
9988
10001
|
}
|
|
9989
|
-
return { standards,
|
|
10002
|
+
return { standards, candidateItemsPerStandard };
|
|
9990
10003
|
}
|
|
9991
10004
|
function reviewRequestFingerprint(request, policy) {
|
|
9992
10005
|
const normalized = normalizeReviewRequest(request);
|
|
@@ -9995,7 +10008,7 @@ function reviewRequestFingerprint(request, policy) {
|
|
|
9995
10008
|
policy: {
|
|
9996
10009
|
version: policy.version
|
|
9997
10010
|
},
|
|
9998
|
-
|
|
10011
|
+
candidateItemsPerStandard: normalized.candidateItemsPerStandard,
|
|
9999
10012
|
standardKeys: normalized.standards.map(assessmentStandardRefKey)
|
|
10000
10013
|
});
|
|
10001
10014
|
}
|
|
@@ -10095,7 +10108,7 @@ function rankedCandidatesByStandard(bank, exposures) {
|
|
|
10095
10108
|
return byStandard;
|
|
10096
10109
|
}
|
|
10097
10110
|
function reviewSlots(request) {
|
|
10098
|
-
return request.standards.flatMap((standard) => Array.from({ length: request.
|
|
10111
|
+
return request.standards.flatMap((standard) => Array.from({ length: request.candidateItemsPerStandard }, () => ({
|
|
10099
10112
|
standard,
|
|
10100
10113
|
standardKey: assessmentStandardRefKey(standard)
|
|
10101
10114
|
})));
|
|
@@ -10199,7 +10212,7 @@ function preferredDistinctMatching(slots, candidatesByStandard) {
|
|
|
10199
10212
|
return new Map(slotItemEdges.filter((candidate) => candidate.edge.capacity === 0).map((candidate) => [candidate.slotIndex, candidate.itemIdentifier]));
|
|
10200
10213
|
}
|
|
10201
10214
|
function fulfillment(request, candidatesByStandard, selections) {
|
|
10202
|
-
const requestedItemCount = request.standards.length * request.
|
|
10215
|
+
const requestedItemCount = request.standards.length * request.candidateItemsPerStandard;
|
|
10203
10216
|
const selectedByStandard = new Map;
|
|
10204
10217
|
for (const selection of selections) {
|
|
10205
10218
|
const key = assessmentStandardRefKey(selection.standard);
|
|
@@ -10208,10 +10221,10 @@ function fulfillment(request, candidatesByStandard, selections) {
|
|
|
10208
10221
|
const shortages = request.standards.flatMap((standard) => {
|
|
10209
10222
|
const key = assessmentStandardRefKey(standard);
|
|
10210
10223
|
const selected = selectedByStandard.get(key) ?? 0;
|
|
10211
|
-
return selected < request.
|
|
10224
|
+
return selected < request.candidateItemsPerStandard ? [
|
|
10212
10225
|
{
|
|
10213
10226
|
standard,
|
|
10214
|
-
requested: request.
|
|
10227
|
+
requested: request.candidateItemsPerStandard,
|
|
10215
10228
|
selected,
|
|
10216
10229
|
eligible: candidatesByStandard.get(key)?.length ?? 0
|
|
10217
10230
|
}
|
|
@@ -10253,9 +10266,9 @@ function projectReviewAssessment(assessment, bank, selections) {
|
|
|
10253
10266
|
throw new Error(`Review bank ${bank.bankRevision} was built from content revision ${bank.sourceContentRevision}, not ${assessment.contentRevision}`);
|
|
10254
10267
|
}
|
|
10255
10268
|
const selectedIdentifiers = new Set(selections.map((selection) => selection.itemIdentifier));
|
|
10256
|
-
const
|
|
10269
|
+
const itemByIdentifier = new Map(assessment.items.map((item) => [item.identifier, item]));
|
|
10257
10270
|
const indexedItems = new Map(bank.items.map((item) => [item.itemIdentifier, item]));
|
|
10258
|
-
const missingIdentifiers = [...selectedIdentifiers].filter((identifier) => !
|
|
10271
|
+
const missingIdentifiers = [...selectedIdentifiers].filter((identifier) => !itemByIdentifier.has(identifier) || !indexedItems.has(identifier));
|
|
10259
10272
|
if (missingIdentifiers.length > 0) {
|
|
10260
10273
|
throw new Error(`Selected review items are absent from the pinned bank: ${missingIdentifiers.join(", ")}`);
|
|
10261
10274
|
}
|
|
@@ -10271,7 +10284,7 @@ function projectReviewAssessment(assessment, bank, selections) {
|
|
|
10271
10284
|
return structuredClone({
|
|
10272
10285
|
...assessment,
|
|
10273
10286
|
contentRevision: bank.bankRevision,
|
|
10274
|
-
items:
|
|
10287
|
+
items: selections.map((selection) => itemByIdentifier.get(selection.itemIdentifier))
|
|
10275
10288
|
});
|
|
10276
10289
|
}
|
|
10277
10290
|
function assessmentItemCorrectness(verdicts) {
|
|
@@ -10330,10 +10343,10 @@ function isReviewItemOutcome(value) {
|
|
|
10330
10343
|
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");
|
|
10331
10344
|
}
|
|
10332
10345
|
function isAssessmentItemSubmission(value) {
|
|
10333
|
-
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");
|
|
10346
|
+
return isRecord(value) && typeof value.submissionId === "string" && value.submissionId.length > 0 && typeof value.itemIdentifier === "string" && value.itemIdentifier.length > 0 && typeof value.submittedAt === "string" && Number.isInteger(value.responseVersion) && typeof value.answered === "boolean" && isAssessmentScore(value.score) && (value.isCorrect === null || typeof value.isCorrect === "boolean");
|
|
10334
10347
|
}
|
|
10335
10348
|
function isReviewAttemptMetadata(value) {
|
|
10336
|
-
return isRecord(value) && typeof value.requestFingerprint === "string" && typeof value.bankRevision === "string" && Array.isArray(value.standards) && value.standards.every(isAssessmentStandardRef) && Number.isInteger(value.
|
|
10349
|
+
return isRecord(value) && typeof value.requestFingerprint === "string" && typeof value.bankRevision === "string" && Array.isArray(value.standards) && value.standards.every(isAssessmentStandardRef) && Number.isInteger(value.candidateItemsPerStandard) && Array.isArray(value.selections) && value.selections.every((selection) => isRecord(selection) && isAssessmentStandardRef(selection.standard) && typeof selection.itemIdentifier === "string") && isReviewFulfillment(value.fulfillment);
|
|
10337
10350
|
}
|
|
10338
10351
|
function isMasteryAttemptMetadata(value) {
|
|
10339
10352
|
return isRecord(value) && typeof value.requestFingerprint === "string" && isAssessmentStandardRef(value.standard);
|
|
@@ -10643,7 +10656,7 @@ var init_assessment_runtime = __esm(() => {
|
|
|
10643
10656
|
StartAssessmentBaseSchema.extend({
|
|
10644
10657
|
purpose: exports_external.literal("review"),
|
|
10645
10658
|
standards: exports_external.array(AssessmentStandardRefSchema).min(1).max(TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.standards),
|
|
10646
|
-
|
|
10659
|
+
candidateItemsPerStandard: exports_external.number().int().positive().max(TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.candidateItemsPerStandard).optional()
|
|
10647
10660
|
}),
|
|
10648
10661
|
StartAssessmentBaseSchema.extend({
|
|
10649
10662
|
purpose: exports_external.literal("mastery"),
|
|
@@ -90937,6 +90950,10 @@ var init_timeback4 = __esm(() => {
|
|
|
90937
90950
|
|
|
90938
90951
|
// ../api-core/src/utils/timeback-assessment-runtime.util.ts
|
|
90939
90952
|
function buildReviewItemResultMetadata(input) {
|
|
90953
|
+
const administration = itemAdministration(input.metadata.itemSubmissions, input.selection.itemIdentifier);
|
|
90954
|
+
if (!administration) {
|
|
90955
|
+
throw new Error(`Cannot create a review result for unadministered candidate ${input.selection.itemIdentifier}`);
|
|
90956
|
+
}
|
|
90940
90957
|
return {
|
|
90941
90958
|
version: PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_VERSION,
|
|
90942
90959
|
parentAttemptId: input.parentAttemptId,
|
|
@@ -90946,7 +90963,7 @@ function buildReviewItemResultMetadata(input) {
|
|
|
90946
90963
|
bankRevision: input.metadata.review.bankRevision,
|
|
90947
90964
|
itemIdentifier: input.selection.itemIdentifier,
|
|
90948
90965
|
standard: input.selection.standard,
|
|
90949
|
-
deliveredAt:
|
|
90966
|
+
deliveredAt: administration.submittedAt,
|
|
90950
90967
|
responseVersion: input.responseVersion ?? input.metadata.responseVersion,
|
|
90951
90968
|
responses: input.metadata.responses[input.selection.itemIdentifier] ?? {},
|
|
90952
90969
|
...input.submissionId ? { submissionId: input.submissionId } : {},
|
|
@@ -91001,7 +91018,8 @@ function resolveReviewChildResult(input) {
|
|
|
91001
91018
|
}
|
|
91002
91019
|
const childMetadata = playcademyAssessmentItemResultMetadata(input.childResult.metadata);
|
|
91003
91020
|
const expectedResponses = input.parentMetadata.responses[input.selection.itemIdentifier] ?? {};
|
|
91004
|
-
|
|
91021
|
+
const administration = itemAdministration(input.parentMetadata.itemSubmissions, input.selection.itemIdentifier);
|
|
91022
|
+
if (input.childResult.assessmentLineItem.sourcedId !== input.childLineItemId || input.childResult.student.sourcedId !== input.parentResult.student.sourcedId || !childMetadata || childMetadata.parentAttemptId !== input.parentResult.sourcedId || childMetadata.activityId !== input.parentMetadata.activityId || childMetadata.integrationId !== input.parentMetadata.integrationId || childMetadata.enrollmentId !== input.parentMetadata.enrollmentId || childMetadata.bankRevision !== input.parentMetadata.review.bankRevision || childMetadata.itemIdentifier !== input.selection.itemIdentifier || childMetadata.standard.framework !== input.selection.standard.framework || childMetadata.standard.identifier !== input.selection.standard.identifier || childMetadata.deliveredAt !== administration?.submittedAt || childMetadata.responseVersion > input.parentMetadata.responseVersion || canonicalJson(childMetadata.responses) !== canonicalJson(expectedResponses)) {
|
|
91005
91023
|
return { action: "restore", reason: "incompatible" };
|
|
91006
91024
|
}
|
|
91007
91025
|
return input.childResult.status === "active" ? { action: "reuse", metadata: childMetadata } : { action: "restore", reason: "inactive" };
|
|
@@ -91692,7 +91710,7 @@ class TimebackAssessmentRuntimeService {
|
|
|
91692
91710
|
requestFingerprint,
|
|
91693
91711
|
bankRevision: source.bank.bankRevision,
|
|
91694
91712
|
standards: [...normalizedRequest.standards],
|
|
91695
|
-
|
|
91713
|
+
candidateItemsPerStandard: normalizedRequest.candidateItemsPerStandard,
|
|
91696
91714
|
selections: [...selected.selections],
|
|
91697
91715
|
fulfillment: selected.fulfillment
|
|
91698
91716
|
}
|
|
@@ -91824,9 +91842,6 @@ class TimebackAssessmentRuntimeService {
|
|
|
91824
91842
|
updatedAt: new Date().toISOString()
|
|
91825
91843
|
};
|
|
91826
91844
|
await this.putResultMetadata(attempt.result, metadata2);
|
|
91827
|
-
if (metadata2.purpose === "review") {
|
|
91828
|
-
await this.putReviewChildResponses(attempt.result, metadata2, new Set(Object.keys(input.responses)));
|
|
91829
|
-
}
|
|
91830
91845
|
return {
|
|
91831
91846
|
attemptId: attempt.result.sourcedId,
|
|
91832
91847
|
responseVersion: metadata2.responseVersion,
|
|
@@ -91894,13 +91909,14 @@ class TimebackAssessmentRuntimeService {
|
|
|
91894
91909
|
if (!preview.scoring) {
|
|
91895
91910
|
throw new Error("Prepared item submission is missing its score");
|
|
91896
91911
|
}
|
|
91897
|
-
const
|
|
91912
|
+
const submittedAt = new Date().toISOString();
|
|
91913
|
+
const completed = completeAssessmentItemSubmission(attempt.metadata, input, transition, preview.scoring, submittedAt);
|
|
91898
91914
|
const metadata2 = {
|
|
91899
91915
|
...attempt.metadata,
|
|
91900
91916
|
responses: completed.responses,
|
|
91901
91917
|
responseVersion: completed.responseVersion,
|
|
91902
91918
|
itemSubmissions: completed.itemSubmissions,
|
|
91903
|
-
updatedAt:
|
|
91919
|
+
updatedAt: submittedAt
|
|
91904
91920
|
};
|
|
91905
91921
|
await this.putResultMetadata(attempt.result, metadata2);
|
|
91906
91922
|
return {
|
|
@@ -92005,12 +92021,13 @@ class TimebackAssessmentRuntimeService {
|
|
|
92005
92021
|
async scoreSubmission(attempt) {
|
|
92006
92022
|
const assessment = await this.loadAttemptAssessment(attempt.metadata);
|
|
92007
92023
|
validateAssessmentResponses(assessment, attempt.metadata.responses);
|
|
92008
|
-
const
|
|
92024
|
+
const administeredItems = attempt.metadata.purpose === "review" ? administeredItemIdentifiers(attempt.metadata.itemSubmissions) : undefined;
|
|
92025
|
+
const itemResults = await this.scoreItems(assessment, attempt.metadata.responses, administeredItems);
|
|
92009
92026
|
return {
|
|
92010
92027
|
responseVersion: attempt.metadata.responseVersion,
|
|
92011
92028
|
assessment,
|
|
92012
92029
|
itemResults,
|
|
92013
|
-
score: aggregateAssessmentScore(itemResults.map((result) => result.score.earned),
|
|
92030
|
+
score: aggregateAssessmentScore(itemResults.map((result) => result.score.earned), itemResults.reduce((total, result) => total + result.score.possible, 0)),
|
|
92014
92031
|
correctQuestions: countCorrectQuestions(itemResults.map((result) => result.isCorrect))
|
|
92015
92032
|
};
|
|
92016
92033
|
}
|
|
@@ -92103,7 +92120,7 @@ class TimebackAssessmentRuntimeService {
|
|
|
92103
92120
|
submissionId: input.submissionId,
|
|
92104
92121
|
timestamp: timestamp6,
|
|
92105
92122
|
testName: assessment.title,
|
|
92106
|
-
totalQuestions:
|
|
92123
|
+
totalQuestions: itemResults.length,
|
|
92107
92124
|
correctQuestions,
|
|
92108
92125
|
...input.session ? { session: input.session } : {},
|
|
92109
92126
|
...reviewOutcomes ? { itemOutcomes: reviewOutcomes } : {}
|
|
@@ -92134,7 +92151,7 @@ class TimebackAssessmentRuntimeService {
|
|
|
92134
92151
|
testIdentifier: assessment.identifier,
|
|
92135
92152
|
testName: assessment.title,
|
|
92136
92153
|
score,
|
|
92137
|
-
totalQuestions:
|
|
92154
|
+
totalQuestions: itemResults.length,
|
|
92138
92155
|
correctQuestions,
|
|
92139
92156
|
submissionId: input.submissionId,
|
|
92140
92157
|
gameId: params.gameId,
|
|
@@ -92678,11 +92695,21 @@ class TimebackAssessmentRuntimeService {
|
|
|
92678
92695
|
if (physicalItems.size !== input.metadata.review.selections.length) {
|
|
92679
92696
|
throw new Error("MVP review selection must use each physical item at most once");
|
|
92680
92697
|
}
|
|
92698
|
+
const administeredItems = administeredItemIdentifiers(input.metadata.itemSubmissions);
|
|
92681
92699
|
await runWithConcurrency(input.metadata.review.selections, TimebackAssessmentRuntimeService.SCORING_CONCURRENCY, async (selection) => {
|
|
92682
92700
|
const item = itemByIdentifier.get(selection.itemIdentifier);
|
|
92683
92701
|
if (!item) {
|
|
92684
92702
|
throw new Error(`Review assessment is missing selected item ${selection.itemIdentifier}`);
|
|
92685
92703
|
}
|
|
92704
|
+
if (!administeredItems.has(selection.itemIdentifier)) {
|
|
92705
|
+
await this.ensureReviewQuestionLineItem({
|
|
92706
|
+
parentLineItemId: input.result.assessmentLineItem.sourcedId,
|
|
92707
|
+
integration: input.integration,
|
|
92708
|
+
bank: input.source.bank,
|
|
92709
|
+
item
|
|
92710
|
+
});
|
|
92711
|
+
return;
|
|
92712
|
+
}
|
|
92686
92713
|
const childLineItemId = await reviewQuestionLineItemId(input.result.assessmentLineItem.sourcedId, item.identifier);
|
|
92687
92714
|
const childResultId = await reviewQuestionResultId(input.result.sourcedId, childLineItemId);
|
|
92688
92715
|
const streamedChildResult = input.existingChildResults.get(childResultId);
|
|
@@ -92729,33 +92756,35 @@ class TimebackAssessmentRuntimeService {
|
|
|
92729
92756
|
});
|
|
92730
92757
|
});
|
|
92731
92758
|
}
|
|
92732
|
-
async
|
|
92733
|
-
|
|
92734
|
-
|
|
92735
|
-
|
|
92736
|
-
|
|
92737
|
-
|
|
92738
|
-
|
|
92739
|
-
|
|
92740
|
-
|
|
92741
|
-
|
|
92742
|
-
|
|
92743
|
-
|
|
92744
|
-
|
|
92745
|
-
|
|
92746
|
-
|
|
92747
|
-
|
|
92748
|
-
|
|
92749
|
-
|
|
92750
|
-
|
|
92751
|
-
|
|
92752
|
-
|
|
92753
|
-
|
|
92754
|
-
|
|
92755
|
-
|
|
92759
|
+
async putReviewChildResponse(result, metadata2, itemIdentifier) {
|
|
92760
|
+
const selection = metadata2.review.selections.find((candidate) => candidate.itemIdentifier === itemIdentifier);
|
|
92761
|
+
if (!selection) {
|
|
92762
|
+
return;
|
|
92763
|
+
}
|
|
92764
|
+
try {
|
|
92765
|
+
const childLineItemId = await reviewQuestionLineItemId(result.assessmentLineItem.sourcedId, selection.itemIdentifier);
|
|
92766
|
+
const childResultId = await reviewQuestionResultId(result.sourcedId, childLineItemId);
|
|
92767
|
+
const childMetadata = buildReviewItemResultMetadata({
|
|
92768
|
+
metadata: metadata2,
|
|
92769
|
+
selection,
|
|
92770
|
+
parentAttemptId: result.sourcedId
|
|
92771
|
+
});
|
|
92772
|
+
await this.requireClient().api.oneroster.assessmentResults.upsert(childResultId, buildOpenReviewChildResult({
|
|
92773
|
+
childLineItemId,
|
|
92774
|
+
student: result.student,
|
|
92775
|
+
metadata: childMetadata
|
|
92776
|
+
}));
|
|
92777
|
+
} catch (error88) {
|
|
92778
|
+
addEvent("assessment.review_child_response_projection_failed", {
|
|
92779
|
+
"app.assessment.attempt_id": result.sourcedId,
|
|
92780
|
+
"app.assessment.qti_item_identifier": selection.itemIdentifier,
|
|
92781
|
+
"exception.type": errorType(error88),
|
|
92782
|
+
"app.error.message": errorMessage(error88)
|
|
92783
|
+
});
|
|
92784
|
+
}
|
|
92756
92785
|
}
|
|
92757
92786
|
async projectReviewItemResponse(params, projection) {
|
|
92758
|
-
await this.
|
|
92787
|
+
await this.putReviewChildResponse(projection.result, projection.metadata, projection.itemIdentifier);
|
|
92759
92788
|
try {
|
|
92760
92789
|
await this.crossAttemptLockBarrier(params.attemptId);
|
|
92761
92790
|
const latest = await this.peekAttempt(params);
|
|
@@ -92834,7 +92863,8 @@ class TimebackAssessmentRuntimeService {
|
|
|
92834
92863
|
}
|
|
92835
92864
|
async finalizeReviewChildResults(input) {
|
|
92836
92865
|
const resultByItem = new Map(input.itemResults.map((itemResult) => [itemResult.itemIdentifier, itemResult]));
|
|
92837
|
-
|
|
92866
|
+
const administeredItems = administeredItemIdentifiers(input.metadata.itemSubmissions);
|
|
92867
|
+
return runWithConcurrency(input.metadata.review.selections.filter((selection) => administeredItems.has(selection.itemIdentifier)), TimebackAssessmentRuntimeService.SCORING_CONCURRENCY, async (selection) => {
|
|
92838
92868
|
const itemResult = resultByItem.get(selection.itemIdentifier);
|
|
92839
92869
|
if (!itemResult) {
|
|
92840
92870
|
throw new Error(`Scoring omitted selected review item ${selection.itemIdentifier}`);
|
|
@@ -92950,7 +92980,7 @@ class TimebackAssessmentRuntimeService {
|
|
|
92950
92980
|
kind: "standards-review",
|
|
92951
92981
|
purpose: metadata2.purpose,
|
|
92952
92982
|
standards: [...metadata2.review.standards],
|
|
92953
|
-
|
|
92983
|
+
candidateItemsPerStandard: metadata2.review.candidateItemsPerStandard,
|
|
92954
92984
|
selections: [...metadata2.review.selections],
|
|
92955
92985
|
fulfillment: metadata2.review.fulfillment
|
|
92956
92986
|
};
|