@playcademy/sandbox 0.7.1-beta.16 → 0.7.1-beta.17
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 +103 -67
- package/dist/constants.js +1 -1
- package/dist/server.js +103 -67
- 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.17",
|
|
1127
1127
|
description: "Local development server for Playcademy game development",
|
|
1128
1128
|
type: "module",
|
|
1129
1129
|
exports: {
|
|
@@ -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,20 @@ function prepareAssessmentItemSubmission(state, input) {
|
|
|
9742
9748
|
failure: assessmentResponseVersionConflict(input.expectedResponseVersion, state.responseVersion)
|
|
9743
9749
|
};
|
|
9744
9750
|
}
|
|
9745
|
-
const
|
|
9746
|
-
const
|
|
9747
|
-
const
|
|
9748
|
-
|
|
9751
|
+
const itemIndex = state.assessment.items.findIndex((item) => item.identifier === input.itemIdentifier);
|
|
9752
|
+
const committed = administeredItemIdentifiers(state.itemSubmissions);
|
|
9753
|
+
const expectedItemIndex = state.assessment.items.findIndex((item) => !committed.has(item.identifier));
|
|
9754
|
+
const previousItemIdentifier = state.itemSubmissions.at(-1)?.itemIdentifier;
|
|
9755
|
+
const previousItemIndex = state.assessment.items.findIndex((item) => item.identifier === previousItemIdentifier);
|
|
9756
|
+
const validOrder = state.purpose === "review" ? itemIndex > previousItemIndex : itemIndex === expectedItemIndex;
|
|
9757
|
+
if (itemIndex === -1 || !validOrder) {
|
|
9749
9758
|
return {
|
|
9750
9759
|
action: "reject",
|
|
9751
|
-
failure: assessmentFlowViolation("Items must be submitted once and in presentation order.", {
|
|
9760
|
+
failure: assessmentFlowViolation(state.purpose === "review" ? "Items must be submitted once and in candidate order." : "Items must be submitted once and in presentation order.", {
|
|
9752
9761
|
itemIdentifier: input.itemIdentifier,
|
|
9753
|
-
|
|
9762
|
+
...state.purpose === "review" ? { previousItemIdentifier: previousItemIdentifier ?? null } : {
|
|
9763
|
+
expectedItemIdentifier: state.assessment.items[expectedItemIndex]?.identifier ?? null
|
|
9764
|
+
},
|
|
9754
9765
|
flow
|
|
9755
9766
|
})
|
|
9756
9767
|
};
|
|
@@ -9773,10 +9784,11 @@ function prepareAssessmentItemSubmission(state, input) {
|
|
|
9773
9784
|
answered: Object.keys(responses[input.itemIdentifier] ?? {}).length > 0
|
|
9774
9785
|
};
|
|
9775
9786
|
}
|
|
9776
|
-
function completeAssessmentItemSubmission(state, input, prepared, scoring) {
|
|
9787
|
+
function completeAssessmentItemSubmission(state, input, prepared, scoring, submittedAt) {
|
|
9777
9788
|
const submission = {
|
|
9778
9789
|
submissionId: input.submissionId,
|
|
9779
9790
|
itemIdentifier: input.itemIdentifier,
|
|
9791
|
+
submittedAt,
|
|
9780
9792
|
responseVersion: prepared.responseVersion,
|
|
9781
9793
|
answered: prepared.answered,
|
|
9782
9794
|
score: scoring.score,
|
|
@@ -9790,8 +9802,8 @@ function completeAssessmentItemSubmission(state, input, prepared, scoring) {
|
|
|
9790
9802
|
};
|
|
9791
9803
|
}
|
|
9792
9804
|
function assessmentResponseUpdateFlowFailure(purpose, itemSubmissions, update) {
|
|
9793
|
-
const
|
|
9794
|
-
const committedUpdate = Object.keys(update).find((identifier) =>
|
|
9805
|
+
const administered = administeredItemIdentifiers(itemSubmissions);
|
|
9806
|
+
const committedUpdate = Object.keys(update).find((identifier) => administered.has(identifier));
|
|
9795
9807
|
return committedUpdate ? assessmentFlowViolation("A submitted item response cannot be edited.", {
|
|
9796
9808
|
itemIdentifier: committedUpdate,
|
|
9797
9809
|
flow: assessmentFlowForPurpose(purpose)
|
|
@@ -9799,11 +9811,18 @@ function assessmentResponseUpdateFlowFailure(purpose, itemSubmissions, update) {
|
|
|
9799
9811
|
}
|
|
9800
9812
|
function assessmentCompletionFlowFailure(purpose, itemSubmissions, itemCount) {
|
|
9801
9813
|
const flow = assessmentFlowForPurpose(purpose);
|
|
9802
|
-
|
|
9814
|
+
if (flow !== "item-submit") {
|
|
9815
|
+
return null;
|
|
9816
|
+
}
|
|
9817
|
+
const complete = purpose === "review" ? itemSubmissions.length > 0 && itemSubmissions.length <= itemCount : itemSubmissions.length === itemCount;
|
|
9818
|
+
if (complete) {
|
|
9819
|
+
return null;
|
|
9820
|
+
}
|
|
9821
|
+
return assessmentFlowViolation(purpose === "review" ? "At least one candidate must be administered before the review can be completed." : "Every item must be submitted before the attempt can be completed.", {
|
|
9803
9822
|
flow,
|
|
9804
9823
|
submittedItemCount: itemSubmissions.length,
|
|
9805
9824
|
itemCount
|
|
9806
|
-
})
|
|
9825
|
+
});
|
|
9807
9826
|
}
|
|
9808
9827
|
function selectAssessmentCourseCandidate(candidates, subjectSpecified) {
|
|
9809
9828
|
if (candidates.length === 0) {
|
|
@@ -9979,15 +9998,15 @@ function canonicalRequestStandards(standards) {
|
|
|
9979
9998
|
return [...canonical.values()].toSorted((left, right) => compareCodeUnits(assessmentStandardRefKey(left), assessmentStandardRefKey(right)));
|
|
9980
9999
|
}
|
|
9981
10000
|
function normalizeReviewRequest(request) {
|
|
9982
|
-
const
|
|
9983
|
-
if (!Number.isInteger(
|
|
9984
|
-
throw new RangeError(`
|
|
10001
|
+
const candidateItemsPerStandard = request.candidateItemsPerStandard ?? 1;
|
|
10002
|
+
if (!Number.isInteger(candidateItemsPerStandard) || candidateItemsPerStandard <= 0 || candidateItemsPerStandard > TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.candidateItemsPerStandard) {
|
|
10003
|
+
throw new RangeError(`candidateItemsPerStandard must be a positive integer at most ${TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.candidateItemsPerStandard}`);
|
|
9985
10004
|
}
|
|
9986
10005
|
const standards = canonicalRequestStandards(request.standards);
|
|
9987
10006
|
if (standards.length === 0) {
|
|
9988
10007
|
throw new Error("At least one review standard is required");
|
|
9989
10008
|
}
|
|
9990
|
-
return { standards,
|
|
10009
|
+
return { standards, candidateItemsPerStandard };
|
|
9991
10010
|
}
|
|
9992
10011
|
function reviewRequestFingerprint(request, policy) {
|
|
9993
10012
|
const normalized = normalizeReviewRequest(request);
|
|
@@ -9996,7 +10015,7 @@ function reviewRequestFingerprint(request, policy) {
|
|
|
9996
10015
|
policy: {
|
|
9997
10016
|
version: policy.version
|
|
9998
10017
|
},
|
|
9999
|
-
|
|
10018
|
+
candidateItemsPerStandard: normalized.candidateItemsPerStandard,
|
|
10000
10019
|
standardKeys: normalized.standards.map(assessmentStandardRefKey)
|
|
10001
10020
|
});
|
|
10002
10021
|
}
|
|
@@ -10096,7 +10115,7 @@ function rankedCandidatesByStandard(bank, exposures) {
|
|
|
10096
10115
|
return byStandard;
|
|
10097
10116
|
}
|
|
10098
10117
|
function reviewSlots(request) {
|
|
10099
|
-
return request.standards.flatMap((standard) => Array.from({ length: request.
|
|
10118
|
+
return request.standards.flatMap((standard) => Array.from({ length: request.candidateItemsPerStandard }, () => ({
|
|
10100
10119
|
standard,
|
|
10101
10120
|
standardKey: assessmentStandardRefKey(standard)
|
|
10102
10121
|
})));
|
|
@@ -10200,7 +10219,7 @@ function preferredDistinctMatching(slots, candidatesByStandard) {
|
|
|
10200
10219
|
return new Map(slotItemEdges.filter((candidate) => candidate.edge.capacity === 0).map((candidate) => [candidate.slotIndex, candidate.itemIdentifier]));
|
|
10201
10220
|
}
|
|
10202
10221
|
function fulfillment(request, candidatesByStandard, selections) {
|
|
10203
|
-
const requestedItemCount = request.standards.length * request.
|
|
10222
|
+
const requestedItemCount = request.standards.length * request.candidateItemsPerStandard;
|
|
10204
10223
|
const selectedByStandard = new Map;
|
|
10205
10224
|
for (const selection of selections) {
|
|
10206
10225
|
const key = assessmentStandardRefKey(selection.standard);
|
|
@@ -10209,10 +10228,10 @@ function fulfillment(request, candidatesByStandard, selections) {
|
|
|
10209
10228
|
const shortages = request.standards.flatMap((standard) => {
|
|
10210
10229
|
const key = assessmentStandardRefKey(standard);
|
|
10211
10230
|
const selected = selectedByStandard.get(key) ?? 0;
|
|
10212
|
-
return selected < request.
|
|
10231
|
+
return selected < request.candidateItemsPerStandard ? [
|
|
10213
10232
|
{
|
|
10214
10233
|
standard,
|
|
10215
|
-
requested: request.
|
|
10234
|
+
requested: request.candidateItemsPerStandard,
|
|
10216
10235
|
selected,
|
|
10217
10236
|
eligible: candidatesByStandard.get(key)?.length ?? 0
|
|
10218
10237
|
}
|
|
@@ -10254,9 +10273,9 @@ function projectReviewAssessment(assessment, bank, selections) {
|
|
|
10254
10273
|
throw new Error(`Review bank ${bank.bankRevision} was built from content revision ${bank.sourceContentRevision}, not ${assessment.contentRevision}`);
|
|
10255
10274
|
}
|
|
10256
10275
|
const selectedIdentifiers = new Set(selections.map((selection) => selection.itemIdentifier));
|
|
10257
|
-
const
|
|
10276
|
+
const itemByIdentifier = new Map(assessment.items.map((item) => [item.identifier, item]));
|
|
10258
10277
|
const indexedItems = new Map(bank.items.map((item) => [item.itemIdentifier, item]));
|
|
10259
|
-
const missingIdentifiers = [...selectedIdentifiers].filter((identifier) => !
|
|
10278
|
+
const missingIdentifiers = [...selectedIdentifiers].filter((identifier) => !itemByIdentifier.has(identifier) || !indexedItems.has(identifier));
|
|
10260
10279
|
if (missingIdentifiers.length > 0) {
|
|
10261
10280
|
throw new Error(`Selected review items are absent from the pinned bank: ${missingIdentifiers.join(", ")}`);
|
|
10262
10281
|
}
|
|
@@ -10272,7 +10291,7 @@ function projectReviewAssessment(assessment, bank, selections) {
|
|
|
10272
10291
|
return structuredClone({
|
|
10273
10292
|
...assessment,
|
|
10274
10293
|
contentRevision: bank.bankRevision,
|
|
10275
|
-
items:
|
|
10294
|
+
items: selections.map((selection) => itemByIdentifier.get(selection.itemIdentifier))
|
|
10276
10295
|
});
|
|
10277
10296
|
}
|
|
10278
10297
|
function assessmentItemCorrectness(verdicts) {
|
|
@@ -10331,10 +10350,10 @@ function isReviewItemOutcome(value) {
|
|
|
10331
10350
|
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
10351
|
}
|
|
10333
10352
|
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");
|
|
10353
|
+
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
10354
|
}
|
|
10336
10355
|
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.
|
|
10356
|
+
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
10357
|
}
|
|
10339
10358
|
function isMasteryAttemptMetadata(value) {
|
|
10340
10359
|
return isRecord(value) && typeof value.requestFingerprint === "string" && isAssessmentStandardRef(value.standard);
|
|
@@ -10644,7 +10663,7 @@ var init_assessment_runtime = __esm(() => {
|
|
|
10644
10663
|
StartAssessmentBaseSchema.extend({
|
|
10645
10664
|
purpose: exports_external.literal("review"),
|
|
10646
10665
|
standards: exports_external.array(AssessmentStandardRefSchema).min(1).max(TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.standards),
|
|
10647
|
-
|
|
10666
|
+
candidateItemsPerStandard: exports_external.number().int().positive().max(TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.candidateItemsPerStandard).optional()
|
|
10648
10667
|
}),
|
|
10649
10668
|
StartAssessmentBaseSchema.extend({
|
|
10650
10669
|
purpose: exports_external.literal("mastery"),
|
|
@@ -90938,6 +90957,10 @@ var init_timeback4 = __esm(() => {
|
|
|
90938
90957
|
|
|
90939
90958
|
// ../api-core/src/utils/timeback-assessment-runtime.util.ts
|
|
90940
90959
|
function buildReviewItemResultMetadata(input) {
|
|
90960
|
+
const administration = itemAdministration(input.metadata.itemSubmissions, input.selection.itemIdentifier);
|
|
90961
|
+
if (!administration) {
|
|
90962
|
+
throw new Error(`Cannot create a review result for unadministered candidate ${input.selection.itemIdentifier}`);
|
|
90963
|
+
}
|
|
90941
90964
|
return {
|
|
90942
90965
|
version: PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_VERSION,
|
|
90943
90966
|
parentAttemptId: input.parentAttemptId,
|
|
@@ -90947,7 +90970,7 @@ function buildReviewItemResultMetadata(input) {
|
|
|
90947
90970
|
bankRevision: input.metadata.review.bankRevision,
|
|
90948
90971
|
itemIdentifier: input.selection.itemIdentifier,
|
|
90949
90972
|
standard: input.selection.standard,
|
|
90950
|
-
deliveredAt:
|
|
90973
|
+
deliveredAt: administration.submittedAt,
|
|
90951
90974
|
responseVersion: input.responseVersion ?? input.metadata.responseVersion,
|
|
90952
90975
|
responses: input.metadata.responses[input.selection.itemIdentifier] ?? {},
|
|
90953
90976
|
...input.submissionId ? { submissionId: input.submissionId } : {},
|
|
@@ -91002,7 +91025,8 @@ function resolveReviewChildResult(input) {
|
|
|
91002
91025
|
}
|
|
91003
91026
|
const childMetadata = playcademyAssessmentItemResultMetadata(input.childResult.metadata);
|
|
91004
91027
|
const expectedResponses = input.parentMetadata.responses[input.selection.itemIdentifier] ?? {};
|
|
91005
|
-
|
|
91028
|
+
const administration = itemAdministration(input.parentMetadata.itemSubmissions, input.selection.itemIdentifier);
|
|
91029
|
+
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
91030
|
return { action: "restore", reason: "incompatible" };
|
|
91007
91031
|
}
|
|
91008
91032
|
return input.childResult.status === "active" ? { action: "reuse", metadata: childMetadata } : { action: "restore", reason: "inactive" };
|
|
@@ -91693,7 +91717,7 @@ class TimebackAssessmentRuntimeService {
|
|
|
91693
91717
|
requestFingerprint,
|
|
91694
91718
|
bankRevision: source.bank.bankRevision,
|
|
91695
91719
|
standards: [...normalizedRequest.standards],
|
|
91696
|
-
|
|
91720
|
+
candidateItemsPerStandard: normalizedRequest.candidateItemsPerStandard,
|
|
91697
91721
|
selections: [...selected.selections],
|
|
91698
91722
|
fulfillment: selected.fulfillment
|
|
91699
91723
|
}
|
|
@@ -91825,9 +91849,6 @@ class TimebackAssessmentRuntimeService {
|
|
|
91825
91849
|
updatedAt: new Date().toISOString()
|
|
91826
91850
|
};
|
|
91827
91851
|
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
91852
|
return {
|
|
91832
91853
|
attemptId: attempt.result.sourcedId,
|
|
91833
91854
|
responseVersion: metadata2.responseVersion,
|
|
@@ -91895,13 +91916,14 @@ class TimebackAssessmentRuntimeService {
|
|
|
91895
91916
|
if (!preview.scoring) {
|
|
91896
91917
|
throw new Error("Prepared item submission is missing its score");
|
|
91897
91918
|
}
|
|
91898
|
-
const
|
|
91919
|
+
const submittedAt = new Date().toISOString();
|
|
91920
|
+
const completed = completeAssessmentItemSubmission(attempt.metadata, input, transition, preview.scoring, submittedAt);
|
|
91899
91921
|
const metadata2 = {
|
|
91900
91922
|
...attempt.metadata,
|
|
91901
91923
|
responses: completed.responses,
|
|
91902
91924
|
responseVersion: completed.responseVersion,
|
|
91903
91925
|
itemSubmissions: completed.itemSubmissions,
|
|
91904
|
-
updatedAt:
|
|
91926
|
+
updatedAt: submittedAt
|
|
91905
91927
|
};
|
|
91906
91928
|
await this.putResultMetadata(attempt.result, metadata2);
|
|
91907
91929
|
return {
|
|
@@ -92006,12 +92028,13 @@ class TimebackAssessmentRuntimeService {
|
|
|
92006
92028
|
async scoreSubmission(attempt) {
|
|
92007
92029
|
const assessment = await this.loadAttemptAssessment(attempt.metadata);
|
|
92008
92030
|
validateAssessmentResponses(assessment, attempt.metadata.responses);
|
|
92009
|
-
const
|
|
92031
|
+
const administeredItems = attempt.metadata.purpose === "review" ? administeredItemIdentifiers(attempt.metadata.itemSubmissions) : undefined;
|
|
92032
|
+
const itemResults = await this.scoreItems(assessment, attempt.metadata.responses, administeredItems);
|
|
92010
92033
|
return {
|
|
92011
92034
|
responseVersion: attempt.metadata.responseVersion,
|
|
92012
92035
|
assessment,
|
|
92013
92036
|
itemResults,
|
|
92014
|
-
score: aggregateAssessmentScore(itemResults.map((result) => result.score.earned),
|
|
92037
|
+
score: aggregateAssessmentScore(itemResults.map((result) => result.score.earned), itemResults.reduce((total, result) => total + result.score.possible, 0)),
|
|
92015
92038
|
correctQuestions: countCorrectQuestions(itemResults.map((result) => result.isCorrect))
|
|
92016
92039
|
};
|
|
92017
92040
|
}
|
|
@@ -92104,7 +92127,7 @@ class TimebackAssessmentRuntimeService {
|
|
|
92104
92127
|
submissionId: input.submissionId,
|
|
92105
92128
|
timestamp: timestamp6,
|
|
92106
92129
|
testName: assessment.title,
|
|
92107
|
-
totalQuestions:
|
|
92130
|
+
totalQuestions: itemResults.length,
|
|
92108
92131
|
correctQuestions,
|
|
92109
92132
|
...input.session ? { session: input.session } : {},
|
|
92110
92133
|
...reviewOutcomes ? { itemOutcomes: reviewOutcomes } : {}
|
|
@@ -92135,7 +92158,7 @@ class TimebackAssessmentRuntimeService {
|
|
|
92135
92158
|
testIdentifier: assessment.identifier,
|
|
92136
92159
|
testName: assessment.title,
|
|
92137
92160
|
score,
|
|
92138
|
-
totalQuestions:
|
|
92161
|
+
totalQuestions: itemResults.length,
|
|
92139
92162
|
correctQuestions,
|
|
92140
92163
|
submissionId: input.submissionId,
|
|
92141
92164
|
gameId: params.gameId,
|
|
@@ -92679,11 +92702,21 @@ class TimebackAssessmentRuntimeService {
|
|
|
92679
92702
|
if (physicalItems.size !== input.metadata.review.selections.length) {
|
|
92680
92703
|
throw new Error("MVP review selection must use each physical item at most once");
|
|
92681
92704
|
}
|
|
92705
|
+
const administeredItems = administeredItemIdentifiers(input.metadata.itemSubmissions);
|
|
92682
92706
|
await runWithConcurrency(input.metadata.review.selections, TimebackAssessmentRuntimeService.SCORING_CONCURRENCY, async (selection) => {
|
|
92683
92707
|
const item = itemByIdentifier.get(selection.itemIdentifier);
|
|
92684
92708
|
if (!item) {
|
|
92685
92709
|
throw new Error(`Review assessment is missing selected item ${selection.itemIdentifier}`);
|
|
92686
92710
|
}
|
|
92711
|
+
if (!administeredItems.has(selection.itemIdentifier)) {
|
|
92712
|
+
await this.ensureReviewQuestionLineItem({
|
|
92713
|
+
parentLineItemId: input.result.assessmentLineItem.sourcedId,
|
|
92714
|
+
integration: input.integration,
|
|
92715
|
+
bank: input.source.bank,
|
|
92716
|
+
item
|
|
92717
|
+
});
|
|
92718
|
+
return;
|
|
92719
|
+
}
|
|
92687
92720
|
const childLineItemId = await reviewQuestionLineItemId(input.result.assessmentLineItem.sourcedId, item.identifier);
|
|
92688
92721
|
const childResultId = await reviewQuestionResultId(input.result.sourcedId, childLineItemId);
|
|
92689
92722
|
const streamedChildResult = input.existingChildResults.get(childResultId);
|
|
@@ -92730,33 +92763,35 @@ class TimebackAssessmentRuntimeService {
|
|
|
92730
92763
|
});
|
|
92731
92764
|
});
|
|
92732
92765
|
}
|
|
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
|
-
|
|
92766
|
+
async putReviewChildResponse(result, metadata2, itemIdentifier) {
|
|
92767
|
+
const selection = metadata2.review.selections.find((candidate) => candidate.itemIdentifier === itemIdentifier);
|
|
92768
|
+
if (!selection) {
|
|
92769
|
+
return;
|
|
92770
|
+
}
|
|
92771
|
+
try {
|
|
92772
|
+
const childLineItemId = await reviewQuestionLineItemId(result.assessmentLineItem.sourcedId, selection.itemIdentifier);
|
|
92773
|
+
const childResultId = await reviewQuestionResultId(result.sourcedId, childLineItemId);
|
|
92774
|
+
const childMetadata = buildReviewItemResultMetadata({
|
|
92775
|
+
metadata: metadata2,
|
|
92776
|
+
selection,
|
|
92777
|
+
parentAttemptId: result.sourcedId
|
|
92778
|
+
});
|
|
92779
|
+
await this.requireClient().api.oneroster.assessmentResults.upsert(childResultId, buildOpenReviewChildResult({
|
|
92780
|
+
childLineItemId,
|
|
92781
|
+
student: result.student,
|
|
92782
|
+
metadata: childMetadata
|
|
92783
|
+
}));
|
|
92784
|
+
} catch (error88) {
|
|
92785
|
+
addEvent("assessment.review_child_response_projection_failed", {
|
|
92786
|
+
"app.assessment.attempt_id": result.sourcedId,
|
|
92787
|
+
"app.assessment.qti_item_identifier": selection.itemIdentifier,
|
|
92788
|
+
"exception.type": errorType(error88),
|
|
92789
|
+
"app.error.message": errorMessage(error88)
|
|
92790
|
+
});
|
|
92791
|
+
}
|
|
92757
92792
|
}
|
|
92758
92793
|
async projectReviewItemResponse(params, projection) {
|
|
92759
|
-
await this.
|
|
92794
|
+
await this.putReviewChildResponse(projection.result, projection.metadata, projection.itemIdentifier);
|
|
92760
92795
|
try {
|
|
92761
92796
|
await this.crossAttemptLockBarrier(params.attemptId);
|
|
92762
92797
|
const latest = await this.peekAttempt(params);
|
|
@@ -92835,7 +92870,8 @@ class TimebackAssessmentRuntimeService {
|
|
|
92835
92870
|
}
|
|
92836
92871
|
async finalizeReviewChildResults(input) {
|
|
92837
92872
|
const resultByItem = new Map(input.itemResults.map((itemResult) => [itemResult.itemIdentifier, itemResult]));
|
|
92838
|
-
|
|
92873
|
+
const administeredItems = administeredItemIdentifiers(input.metadata.itemSubmissions);
|
|
92874
|
+
return runWithConcurrency(input.metadata.review.selections.filter((selection) => administeredItems.has(selection.itemIdentifier)), TimebackAssessmentRuntimeService.SCORING_CONCURRENCY, async (selection) => {
|
|
92839
92875
|
const itemResult = resultByItem.get(selection.itemIdentifier);
|
|
92840
92876
|
if (!itemResult) {
|
|
92841
92877
|
throw new Error(`Scoring omitted selected review item ${selection.itemIdentifier}`);
|
|
@@ -92951,7 +92987,7 @@ class TimebackAssessmentRuntimeService {
|
|
|
92951
92987
|
kind: "standards-review",
|
|
92952
92988
|
purpose: metadata2.purpose,
|
|
92953
92989
|
standards: [...metadata2.review.standards],
|
|
92954
|
-
|
|
92990
|
+
candidateItemsPerStandard: metadata2.review.candidateItemsPerStandard,
|
|
92955
92991
|
selections: [...metadata2.review.selections],
|
|
92956
92992
|
fulfillment: metadata2.review.fulfillment
|
|
92957
92993
|
};
|
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.17",
|
|
1126
1126
|
description: "Local development server for Playcademy game development",
|
|
1127
1127
|
type: "module",
|
|
1128
1128
|
exports: {
|
|
@@ -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,20 @@ function prepareAssessmentItemSubmission(state, input) {
|
|
|
9741
9747
|
failure: assessmentResponseVersionConflict(input.expectedResponseVersion, state.responseVersion)
|
|
9742
9748
|
};
|
|
9743
9749
|
}
|
|
9744
|
-
const
|
|
9745
|
-
const
|
|
9746
|
-
const
|
|
9747
|
-
|
|
9750
|
+
const itemIndex = state.assessment.items.findIndex((item) => item.identifier === input.itemIdentifier);
|
|
9751
|
+
const committed = administeredItemIdentifiers(state.itemSubmissions);
|
|
9752
|
+
const expectedItemIndex = state.assessment.items.findIndex((item) => !committed.has(item.identifier));
|
|
9753
|
+
const previousItemIdentifier = state.itemSubmissions.at(-1)?.itemIdentifier;
|
|
9754
|
+
const previousItemIndex = state.assessment.items.findIndex((item) => item.identifier === previousItemIdentifier);
|
|
9755
|
+
const validOrder = state.purpose === "review" ? itemIndex > previousItemIndex : itemIndex === expectedItemIndex;
|
|
9756
|
+
if (itemIndex === -1 || !validOrder) {
|
|
9748
9757
|
return {
|
|
9749
9758
|
action: "reject",
|
|
9750
|
-
failure: assessmentFlowViolation("Items must be submitted once and in presentation order.", {
|
|
9759
|
+
failure: assessmentFlowViolation(state.purpose === "review" ? "Items must be submitted once and in candidate order." : "Items must be submitted once and in presentation order.", {
|
|
9751
9760
|
itemIdentifier: input.itemIdentifier,
|
|
9752
|
-
|
|
9761
|
+
...state.purpose === "review" ? { previousItemIdentifier: previousItemIdentifier ?? null } : {
|
|
9762
|
+
expectedItemIdentifier: state.assessment.items[expectedItemIndex]?.identifier ?? null
|
|
9763
|
+
},
|
|
9753
9764
|
flow
|
|
9754
9765
|
})
|
|
9755
9766
|
};
|
|
@@ -9772,10 +9783,11 @@ function prepareAssessmentItemSubmission(state, input) {
|
|
|
9772
9783
|
answered: Object.keys(responses[input.itemIdentifier] ?? {}).length > 0
|
|
9773
9784
|
};
|
|
9774
9785
|
}
|
|
9775
|
-
function completeAssessmentItemSubmission(state, input, prepared, scoring) {
|
|
9786
|
+
function completeAssessmentItemSubmission(state, input, prepared, scoring, submittedAt) {
|
|
9776
9787
|
const submission = {
|
|
9777
9788
|
submissionId: input.submissionId,
|
|
9778
9789
|
itemIdentifier: input.itemIdentifier,
|
|
9790
|
+
submittedAt,
|
|
9779
9791
|
responseVersion: prepared.responseVersion,
|
|
9780
9792
|
answered: prepared.answered,
|
|
9781
9793
|
score: scoring.score,
|
|
@@ -9789,8 +9801,8 @@ function completeAssessmentItemSubmission(state, input, prepared, scoring) {
|
|
|
9789
9801
|
};
|
|
9790
9802
|
}
|
|
9791
9803
|
function assessmentResponseUpdateFlowFailure(purpose, itemSubmissions, update) {
|
|
9792
|
-
const
|
|
9793
|
-
const committedUpdate = Object.keys(update).find((identifier) =>
|
|
9804
|
+
const administered = administeredItemIdentifiers(itemSubmissions);
|
|
9805
|
+
const committedUpdate = Object.keys(update).find((identifier) => administered.has(identifier));
|
|
9794
9806
|
return committedUpdate ? assessmentFlowViolation("A submitted item response cannot be edited.", {
|
|
9795
9807
|
itemIdentifier: committedUpdate,
|
|
9796
9808
|
flow: assessmentFlowForPurpose(purpose)
|
|
@@ -9798,11 +9810,18 @@ function assessmentResponseUpdateFlowFailure(purpose, itemSubmissions, update) {
|
|
|
9798
9810
|
}
|
|
9799
9811
|
function assessmentCompletionFlowFailure(purpose, itemSubmissions, itemCount) {
|
|
9800
9812
|
const flow = assessmentFlowForPurpose(purpose);
|
|
9801
|
-
|
|
9813
|
+
if (flow !== "item-submit") {
|
|
9814
|
+
return null;
|
|
9815
|
+
}
|
|
9816
|
+
const complete = purpose === "review" ? itemSubmissions.length > 0 && itemSubmissions.length <= itemCount : itemSubmissions.length === itemCount;
|
|
9817
|
+
if (complete) {
|
|
9818
|
+
return null;
|
|
9819
|
+
}
|
|
9820
|
+
return assessmentFlowViolation(purpose === "review" ? "At least one candidate must be administered before the review can be completed." : "Every item must be submitted before the attempt can be completed.", {
|
|
9802
9821
|
flow,
|
|
9803
9822
|
submittedItemCount: itemSubmissions.length,
|
|
9804
9823
|
itemCount
|
|
9805
|
-
})
|
|
9824
|
+
});
|
|
9806
9825
|
}
|
|
9807
9826
|
function selectAssessmentCourseCandidate(candidates, subjectSpecified) {
|
|
9808
9827
|
if (candidates.length === 0) {
|
|
@@ -9978,15 +9997,15 @@ function canonicalRequestStandards(standards) {
|
|
|
9978
9997
|
return [...canonical.values()].toSorted((left, right) => compareCodeUnits(assessmentStandardRefKey(left), assessmentStandardRefKey(right)));
|
|
9979
9998
|
}
|
|
9980
9999
|
function normalizeReviewRequest(request) {
|
|
9981
|
-
const
|
|
9982
|
-
if (!Number.isInteger(
|
|
9983
|
-
throw new RangeError(`
|
|
10000
|
+
const candidateItemsPerStandard = request.candidateItemsPerStandard ?? 1;
|
|
10001
|
+
if (!Number.isInteger(candidateItemsPerStandard) || candidateItemsPerStandard <= 0 || candidateItemsPerStandard > TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.candidateItemsPerStandard) {
|
|
10002
|
+
throw new RangeError(`candidateItemsPerStandard must be a positive integer at most ${TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.candidateItemsPerStandard}`);
|
|
9984
10003
|
}
|
|
9985
10004
|
const standards = canonicalRequestStandards(request.standards);
|
|
9986
10005
|
if (standards.length === 0) {
|
|
9987
10006
|
throw new Error("At least one review standard is required");
|
|
9988
10007
|
}
|
|
9989
|
-
return { standards,
|
|
10008
|
+
return { standards, candidateItemsPerStandard };
|
|
9990
10009
|
}
|
|
9991
10010
|
function reviewRequestFingerprint(request, policy) {
|
|
9992
10011
|
const normalized = normalizeReviewRequest(request);
|
|
@@ -9995,7 +10014,7 @@ function reviewRequestFingerprint(request, policy) {
|
|
|
9995
10014
|
policy: {
|
|
9996
10015
|
version: policy.version
|
|
9997
10016
|
},
|
|
9998
|
-
|
|
10017
|
+
candidateItemsPerStandard: normalized.candidateItemsPerStandard,
|
|
9999
10018
|
standardKeys: normalized.standards.map(assessmentStandardRefKey)
|
|
10000
10019
|
});
|
|
10001
10020
|
}
|
|
@@ -10095,7 +10114,7 @@ function rankedCandidatesByStandard(bank, exposures) {
|
|
|
10095
10114
|
return byStandard;
|
|
10096
10115
|
}
|
|
10097
10116
|
function reviewSlots(request) {
|
|
10098
|
-
return request.standards.flatMap((standard) => Array.from({ length: request.
|
|
10117
|
+
return request.standards.flatMap((standard) => Array.from({ length: request.candidateItemsPerStandard }, () => ({
|
|
10099
10118
|
standard,
|
|
10100
10119
|
standardKey: assessmentStandardRefKey(standard)
|
|
10101
10120
|
})));
|
|
@@ -10199,7 +10218,7 @@ function preferredDistinctMatching(slots, candidatesByStandard) {
|
|
|
10199
10218
|
return new Map(slotItemEdges.filter((candidate) => candidate.edge.capacity === 0).map((candidate) => [candidate.slotIndex, candidate.itemIdentifier]));
|
|
10200
10219
|
}
|
|
10201
10220
|
function fulfillment(request, candidatesByStandard, selections) {
|
|
10202
|
-
const requestedItemCount = request.standards.length * request.
|
|
10221
|
+
const requestedItemCount = request.standards.length * request.candidateItemsPerStandard;
|
|
10203
10222
|
const selectedByStandard = new Map;
|
|
10204
10223
|
for (const selection of selections) {
|
|
10205
10224
|
const key = assessmentStandardRefKey(selection.standard);
|
|
@@ -10208,10 +10227,10 @@ function fulfillment(request, candidatesByStandard, selections) {
|
|
|
10208
10227
|
const shortages = request.standards.flatMap((standard) => {
|
|
10209
10228
|
const key = assessmentStandardRefKey(standard);
|
|
10210
10229
|
const selected = selectedByStandard.get(key) ?? 0;
|
|
10211
|
-
return selected < request.
|
|
10230
|
+
return selected < request.candidateItemsPerStandard ? [
|
|
10212
10231
|
{
|
|
10213
10232
|
standard,
|
|
10214
|
-
requested: request.
|
|
10233
|
+
requested: request.candidateItemsPerStandard,
|
|
10215
10234
|
selected,
|
|
10216
10235
|
eligible: candidatesByStandard.get(key)?.length ?? 0
|
|
10217
10236
|
}
|
|
@@ -10253,9 +10272,9 @@ function projectReviewAssessment(assessment, bank, selections) {
|
|
|
10253
10272
|
throw new Error(`Review bank ${bank.bankRevision} was built from content revision ${bank.sourceContentRevision}, not ${assessment.contentRevision}`);
|
|
10254
10273
|
}
|
|
10255
10274
|
const selectedIdentifiers = new Set(selections.map((selection) => selection.itemIdentifier));
|
|
10256
|
-
const
|
|
10275
|
+
const itemByIdentifier = new Map(assessment.items.map((item) => [item.identifier, item]));
|
|
10257
10276
|
const indexedItems = new Map(bank.items.map((item) => [item.itemIdentifier, item]));
|
|
10258
|
-
const missingIdentifiers = [...selectedIdentifiers].filter((identifier) => !
|
|
10277
|
+
const missingIdentifiers = [...selectedIdentifiers].filter((identifier) => !itemByIdentifier.has(identifier) || !indexedItems.has(identifier));
|
|
10259
10278
|
if (missingIdentifiers.length > 0) {
|
|
10260
10279
|
throw new Error(`Selected review items are absent from the pinned bank: ${missingIdentifiers.join(", ")}`);
|
|
10261
10280
|
}
|
|
@@ -10271,7 +10290,7 @@ function projectReviewAssessment(assessment, bank, selections) {
|
|
|
10271
10290
|
return structuredClone({
|
|
10272
10291
|
...assessment,
|
|
10273
10292
|
contentRevision: bank.bankRevision,
|
|
10274
|
-
items:
|
|
10293
|
+
items: selections.map((selection) => itemByIdentifier.get(selection.itemIdentifier))
|
|
10275
10294
|
});
|
|
10276
10295
|
}
|
|
10277
10296
|
function assessmentItemCorrectness(verdicts) {
|
|
@@ -10330,10 +10349,10 @@ function isReviewItemOutcome(value) {
|
|
|
10330
10349
|
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
10350
|
}
|
|
10332
10351
|
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");
|
|
10352
|
+
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
10353
|
}
|
|
10335
10354
|
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.
|
|
10355
|
+
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
10356
|
}
|
|
10338
10357
|
function isMasteryAttemptMetadata(value) {
|
|
10339
10358
|
return isRecord(value) && typeof value.requestFingerprint === "string" && isAssessmentStandardRef(value.standard);
|
|
@@ -10643,7 +10662,7 @@ var init_assessment_runtime = __esm(() => {
|
|
|
10643
10662
|
StartAssessmentBaseSchema.extend({
|
|
10644
10663
|
purpose: exports_external.literal("review"),
|
|
10645
10664
|
standards: exports_external.array(AssessmentStandardRefSchema).min(1).max(TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.standards),
|
|
10646
|
-
|
|
10665
|
+
candidateItemsPerStandard: exports_external.number().int().positive().max(TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.candidateItemsPerStandard).optional()
|
|
10647
10666
|
}),
|
|
10648
10667
|
StartAssessmentBaseSchema.extend({
|
|
10649
10668
|
purpose: exports_external.literal("mastery"),
|
|
@@ -90937,6 +90956,10 @@ var init_timeback4 = __esm(() => {
|
|
|
90937
90956
|
|
|
90938
90957
|
// ../api-core/src/utils/timeback-assessment-runtime.util.ts
|
|
90939
90958
|
function buildReviewItemResultMetadata(input) {
|
|
90959
|
+
const administration = itemAdministration(input.metadata.itemSubmissions, input.selection.itemIdentifier);
|
|
90960
|
+
if (!administration) {
|
|
90961
|
+
throw new Error(`Cannot create a review result for unadministered candidate ${input.selection.itemIdentifier}`);
|
|
90962
|
+
}
|
|
90940
90963
|
return {
|
|
90941
90964
|
version: PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_VERSION,
|
|
90942
90965
|
parentAttemptId: input.parentAttemptId,
|
|
@@ -90946,7 +90969,7 @@ function buildReviewItemResultMetadata(input) {
|
|
|
90946
90969
|
bankRevision: input.metadata.review.bankRevision,
|
|
90947
90970
|
itemIdentifier: input.selection.itemIdentifier,
|
|
90948
90971
|
standard: input.selection.standard,
|
|
90949
|
-
deliveredAt:
|
|
90972
|
+
deliveredAt: administration.submittedAt,
|
|
90950
90973
|
responseVersion: input.responseVersion ?? input.metadata.responseVersion,
|
|
90951
90974
|
responses: input.metadata.responses[input.selection.itemIdentifier] ?? {},
|
|
90952
90975
|
...input.submissionId ? { submissionId: input.submissionId } : {},
|
|
@@ -91001,7 +91024,8 @@ function resolveReviewChildResult(input) {
|
|
|
91001
91024
|
}
|
|
91002
91025
|
const childMetadata = playcademyAssessmentItemResultMetadata(input.childResult.metadata);
|
|
91003
91026
|
const expectedResponses = input.parentMetadata.responses[input.selection.itemIdentifier] ?? {};
|
|
91004
|
-
|
|
91027
|
+
const administration = itemAdministration(input.parentMetadata.itemSubmissions, input.selection.itemIdentifier);
|
|
91028
|
+
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
91029
|
return { action: "restore", reason: "incompatible" };
|
|
91006
91030
|
}
|
|
91007
91031
|
return input.childResult.status === "active" ? { action: "reuse", metadata: childMetadata } : { action: "restore", reason: "inactive" };
|
|
@@ -91692,7 +91716,7 @@ class TimebackAssessmentRuntimeService {
|
|
|
91692
91716
|
requestFingerprint,
|
|
91693
91717
|
bankRevision: source.bank.bankRevision,
|
|
91694
91718
|
standards: [...normalizedRequest.standards],
|
|
91695
|
-
|
|
91719
|
+
candidateItemsPerStandard: normalizedRequest.candidateItemsPerStandard,
|
|
91696
91720
|
selections: [...selected.selections],
|
|
91697
91721
|
fulfillment: selected.fulfillment
|
|
91698
91722
|
}
|
|
@@ -91824,9 +91848,6 @@ class TimebackAssessmentRuntimeService {
|
|
|
91824
91848
|
updatedAt: new Date().toISOString()
|
|
91825
91849
|
};
|
|
91826
91850
|
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
91851
|
return {
|
|
91831
91852
|
attemptId: attempt.result.sourcedId,
|
|
91832
91853
|
responseVersion: metadata2.responseVersion,
|
|
@@ -91894,13 +91915,14 @@ class TimebackAssessmentRuntimeService {
|
|
|
91894
91915
|
if (!preview.scoring) {
|
|
91895
91916
|
throw new Error("Prepared item submission is missing its score");
|
|
91896
91917
|
}
|
|
91897
|
-
const
|
|
91918
|
+
const submittedAt = new Date().toISOString();
|
|
91919
|
+
const completed = completeAssessmentItemSubmission(attempt.metadata, input, transition, preview.scoring, submittedAt);
|
|
91898
91920
|
const metadata2 = {
|
|
91899
91921
|
...attempt.metadata,
|
|
91900
91922
|
responses: completed.responses,
|
|
91901
91923
|
responseVersion: completed.responseVersion,
|
|
91902
91924
|
itemSubmissions: completed.itemSubmissions,
|
|
91903
|
-
updatedAt:
|
|
91925
|
+
updatedAt: submittedAt
|
|
91904
91926
|
};
|
|
91905
91927
|
await this.putResultMetadata(attempt.result, metadata2);
|
|
91906
91928
|
return {
|
|
@@ -92005,12 +92027,13 @@ class TimebackAssessmentRuntimeService {
|
|
|
92005
92027
|
async scoreSubmission(attempt) {
|
|
92006
92028
|
const assessment = await this.loadAttemptAssessment(attempt.metadata);
|
|
92007
92029
|
validateAssessmentResponses(assessment, attempt.metadata.responses);
|
|
92008
|
-
const
|
|
92030
|
+
const administeredItems = attempt.metadata.purpose === "review" ? administeredItemIdentifiers(attempt.metadata.itemSubmissions) : undefined;
|
|
92031
|
+
const itemResults = await this.scoreItems(assessment, attempt.metadata.responses, administeredItems);
|
|
92009
92032
|
return {
|
|
92010
92033
|
responseVersion: attempt.metadata.responseVersion,
|
|
92011
92034
|
assessment,
|
|
92012
92035
|
itemResults,
|
|
92013
|
-
score: aggregateAssessmentScore(itemResults.map((result) => result.score.earned),
|
|
92036
|
+
score: aggregateAssessmentScore(itemResults.map((result) => result.score.earned), itemResults.reduce((total, result) => total + result.score.possible, 0)),
|
|
92014
92037
|
correctQuestions: countCorrectQuestions(itemResults.map((result) => result.isCorrect))
|
|
92015
92038
|
};
|
|
92016
92039
|
}
|
|
@@ -92103,7 +92126,7 @@ class TimebackAssessmentRuntimeService {
|
|
|
92103
92126
|
submissionId: input.submissionId,
|
|
92104
92127
|
timestamp: timestamp6,
|
|
92105
92128
|
testName: assessment.title,
|
|
92106
|
-
totalQuestions:
|
|
92129
|
+
totalQuestions: itemResults.length,
|
|
92107
92130
|
correctQuestions,
|
|
92108
92131
|
...input.session ? { session: input.session } : {},
|
|
92109
92132
|
...reviewOutcomes ? { itemOutcomes: reviewOutcomes } : {}
|
|
@@ -92134,7 +92157,7 @@ class TimebackAssessmentRuntimeService {
|
|
|
92134
92157
|
testIdentifier: assessment.identifier,
|
|
92135
92158
|
testName: assessment.title,
|
|
92136
92159
|
score,
|
|
92137
|
-
totalQuestions:
|
|
92160
|
+
totalQuestions: itemResults.length,
|
|
92138
92161
|
correctQuestions,
|
|
92139
92162
|
submissionId: input.submissionId,
|
|
92140
92163
|
gameId: params.gameId,
|
|
@@ -92678,11 +92701,21 @@ class TimebackAssessmentRuntimeService {
|
|
|
92678
92701
|
if (physicalItems.size !== input.metadata.review.selections.length) {
|
|
92679
92702
|
throw new Error("MVP review selection must use each physical item at most once");
|
|
92680
92703
|
}
|
|
92704
|
+
const administeredItems = administeredItemIdentifiers(input.metadata.itemSubmissions);
|
|
92681
92705
|
await runWithConcurrency(input.metadata.review.selections, TimebackAssessmentRuntimeService.SCORING_CONCURRENCY, async (selection) => {
|
|
92682
92706
|
const item = itemByIdentifier.get(selection.itemIdentifier);
|
|
92683
92707
|
if (!item) {
|
|
92684
92708
|
throw new Error(`Review assessment is missing selected item ${selection.itemIdentifier}`);
|
|
92685
92709
|
}
|
|
92710
|
+
if (!administeredItems.has(selection.itemIdentifier)) {
|
|
92711
|
+
await this.ensureReviewQuestionLineItem({
|
|
92712
|
+
parentLineItemId: input.result.assessmentLineItem.sourcedId,
|
|
92713
|
+
integration: input.integration,
|
|
92714
|
+
bank: input.source.bank,
|
|
92715
|
+
item
|
|
92716
|
+
});
|
|
92717
|
+
return;
|
|
92718
|
+
}
|
|
92686
92719
|
const childLineItemId = await reviewQuestionLineItemId(input.result.assessmentLineItem.sourcedId, item.identifier);
|
|
92687
92720
|
const childResultId = await reviewQuestionResultId(input.result.sourcedId, childLineItemId);
|
|
92688
92721
|
const streamedChildResult = input.existingChildResults.get(childResultId);
|
|
@@ -92729,33 +92762,35 @@ class TimebackAssessmentRuntimeService {
|
|
|
92729
92762
|
});
|
|
92730
92763
|
});
|
|
92731
92764
|
}
|
|
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
|
-
|
|
92765
|
+
async putReviewChildResponse(result, metadata2, itemIdentifier) {
|
|
92766
|
+
const selection = metadata2.review.selections.find((candidate) => candidate.itemIdentifier === itemIdentifier);
|
|
92767
|
+
if (!selection) {
|
|
92768
|
+
return;
|
|
92769
|
+
}
|
|
92770
|
+
try {
|
|
92771
|
+
const childLineItemId = await reviewQuestionLineItemId(result.assessmentLineItem.sourcedId, selection.itemIdentifier);
|
|
92772
|
+
const childResultId = await reviewQuestionResultId(result.sourcedId, childLineItemId);
|
|
92773
|
+
const childMetadata = buildReviewItemResultMetadata({
|
|
92774
|
+
metadata: metadata2,
|
|
92775
|
+
selection,
|
|
92776
|
+
parentAttemptId: result.sourcedId
|
|
92777
|
+
});
|
|
92778
|
+
await this.requireClient().api.oneroster.assessmentResults.upsert(childResultId, buildOpenReviewChildResult({
|
|
92779
|
+
childLineItemId,
|
|
92780
|
+
student: result.student,
|
|
92781
|
+
metadata: childMetadata
|
|
92782
|
+
}));
|
|
92783
|
+
} catch (error88) {
|
|
92784
|
+
addEvent("assessment.review_child_response_projection_failed", {
|
|
92785
|
+
"app.assessment.attempt_id": result.sourcedId,
|
|
92786
|
+
"app.assessment.qti_item_identifier": selection.itemIdentifier,
|
|
92787
|
+
"exception.type": errorType(error88),
|
|
92788
|
+
"app.error.message": errorMessage(error88)
|
|
92789
|
+
});
|
|
92790
|
+
}
|
|
92756
92791
|
}
|
|
92757
92792
|
async projectReviewItemResponse(params, projection) {
|
|
92758
|
-
await this.
|
|
92793
|
+
await this.putReviewChildResponse(projection.result, projection.metadata, projection.itemIdentifier);
|
|
92759
92794
|
try {
|
|
92760
92795
|
await this.crossAttemptLockBarrier(params.attemptId);
|
|
92761
92796
|
const latest = await this.peekAttempt(params);
|
|
@@ -92834,7 +92869,8 @@ class TimebackAssessmentRuntimeService {
|
|
|
92834
92869
|
}
|
|
92835
92870
|
async finalizeReviewChildResults(input) {
|
|
92836
92871
|
const resultByItem = new Map(input.itemResults.map((itemResult) => [itemResult.itemIdentifier, itemResult]));
|
|
92837
|
-
|
|
92872
|
+
const administeredItems = administeredItemIdentifiers(input.metadata.itemSubmissions);
|
|
92873
|
+
return runWithConcurrency(input.metadata.review.selections.filter((selection) => administeredItems.has(selection.itemIdentifier)), TimebackAssessmentRuntimeService.SCORING_CONCURRENCY, async (selection) => {
|
|
92838
92874
|
const itemResult = resultByItem.get(selection.itemIdentifier);
|
|
92839
92875
|
if (!itemResult) {
|
|
92840
92876
|
throw new Error(`Scoring omitted selected review item ${selection.itemIdentifier}`);
|
|
@@ -92950,7 +92986,7 @@ class TimebackAssessmentRuntimeService {
|
|
|
92950
92986
|
kind: "standards-review",
|
|
92951
92987
|
purpose: metadata2.purpose,
|
|
92952
92988
|
standards: [...metadata2.review.standards],
|
|
92953
|
-
|
|
92989
|
+
candidateItemsPerStandard: metadata2.review.candidateItemsPerStandard,
|
|
92954
92990
|
selections: [...metadata2.review.selections],
|
|
92955
92991
|
fulfillment: metadata2.review.fulfillment
|
|
92956
92992
|
};
|