@playcademy/vite-plugin 1.3.1-beta.1 → 1.3.1-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +1136 -494
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -24317,7 +24317,7 @@ import path2 from "node:path";
|
|
|
24317
24317
|
// package.json
|
|
24318
24318
|
var package_default = {
|
|
24319
24319
|
name: "@playcademy/vite-plugin",
|
|
24320
|
-
version: "1.3.1-beta.
|
|
24320
|
+
version: "1.3.1-beta.3",
|
|
24321
24321
|
type: "module",
|
|
24322
24322
|
exports: {
|
|
24323
24323
|
".": {
|
|
@@ -25934,7 +25934,7 @@ var package_default2;
|
|
|
25934
25934
|
var init_package = __esm(() => {
|
|
25935
25935
|
package_default2 = {
|
|
25936
25936
|
name: "@playcademy/sandbox",
|
|
25937
|
-
version: "0.8.1-beta.
|
|
25937
|
+
version: "0.8.1-beta.3",
|
|
25938
25938
|
description: "Local development server for Playcademy game development",
|
|
25939
25939
|
type: "module",
|
|
25940
25940
|
exports: {
|
|
@@ -26324,6 +26324,9 @@ async function runWithConcurrency(items, concurrency, worker) {
|
|
|
26324
26324
|
}
|
|
26325
26325
|
return results;
|
|
26326
26326
|
}
|
|
26327
|
+
function reviewBankSinkIdentifier(integrationId) {
|
|
26328
|
+
return `playcademy-review-bank.${integrationId}`;
|
|
26329
|
+
}
|
|
26327
26330
|
function playcademySupportedQtiInteractionType(value) {
|
|
26328
26331
|
if (typeof value !== "string") {
|
|
26329
26332
|
return;
|
|
@@ -34411,9 +34414,12 @@ function classifyAssessmentSubmission(attempt, submissionId) {
|
|
|
34411
34414
|
}
|
|
34412
34415
|
return isAssessmentAttemptOpen(attempt) ? "submit" : "reject";
|
|
34413
34416
|
}
|
|
34417
|
+
function isSameAssessmentAward(recorded, input) {
|
|
34418
|
+
return recorded.xpAwarded === input.xpAwarded && recorded.masteredUnits === input.masteredUnits && recorded.masteredUnitsAbsolute === input.masteredUnitsAbsolute;
|
|
34419
|
+
}
|
|
34414
34420
|
function classifyAssessmentFinalization(attempt, input) {
|
|
34415
34421
|
if (isAssessmentAttemptCompleted(attempt)) {
|
|
34416
|
-
return attempt.submissionId === input.submissionId && attempt.
|
|
34422
|
+
return attempt.submissionId === input.submissionId && attempt.award !== undefined && isSameAssessmentAward(attempt.award, input) ? "replay" : "conflict";
|
|
34417
34423
|
}
|
|
34418
34424
|
if (!isAssessmentAttemptAwaitingAward(attempt) || attempt.submissionId !== input.submissionId) {
|
|
34419
34425
|
return "reject";
|
|
@@ -35691,39 +35697,21 @@ function compareNewestCompletedAssessmentAttempt(left, right) {
|
|
|
35691
35697
|
return newestAttempt(left, right, "scoreDate");
|
|
35692
35698
|
}
|
|
35693
35699
|
function compareAssessmentCatalogOrder(left, right) {
|
|
35694
|
-
if (left.sortOrder !== null || right.sortOrder !== null) {
|
|
35695
|
-
if (left.sortOrder === null) {
|
|
35696
|
-
return 1;
|
|
35697
|
-
}
|
|
35698
|
-
if (right.sortOrder === null) {
|
|
35699
|
-
return -1;
|
|
35700
|
-
}
|
|
35701
|
-
const orderDifference = left.sortOrder - right.sortOrder;
|
|
35702
|
-
if (orderDifference) {
|
|
35703
|
-
return orderDifference;
|
|
35704
|
-
}
|
|
35705
|
-
}
|
|
35706
|
-
const leftUpdatedAt = Date.parse(left.updatedAt);
|
|
35707
|
-
const rightUpdatedAt = Date.parse(right.updatedAt);
|
|
35708
|
-
const leftHasValidDate = Number.isFinite(leftUpdatedAt);
|
|
35709
|
-
const rightHasValidDate = Number.isFinite(rightUpdatedAt);
|
|
35710
|
-
if (leftHasValidDate && rightHasValidDate) {
|
|
35711
|
-
const updatedDifference = leftUpdatedAt - rightUpdatedAt;
|
|
35712
|
-
if (updatedDifference) {
|
|
35713
|
-
return updatedDifference;
|
|
35714
|
-
}
|
|
35715
|
-
} else if (leftHasValidDate !== rightHasValidDate) {
|
|
35716
|
-
return leftHasValidDate ? -1 : 1;
|
|
35717
|
-
}
|
|
35718
35700
|
if (left.assessmentKey !== null && right.assessmentKey !== null) {
|
|
35719
|
-
const keyDifference = left.assessmentKey
|
|
35701
|
+
const keyDifference = compareCodePoints(left.assessmentKey, right.assessmentKey);
|
|
35720
35702
|
if (keyDifference) {
|
|
35721
35703
|
return keyDifference;
|
|
35722
35704
|
}
|
|
35723
35705
|
} else if (left.assessmentKey !== null || right.assessmentKey !== null) {
|
|
35724
35706
|
return left.assessmentKey === null ? 1 : -1;
|
|
35725
35707
|
}
|
|
35726
|
-
return left.qtiTestIdentifier
|
|
35708
|
+
return compareCodePoints(left.qtiTestIdentifier, right.qtiTestIdentifier);
|
|
35709
|
+
}
|
|
35710
|
+
function compareCodePoints(left, right) {
|
|
35711
|
+
if (left === right) {
|
|
35712
|
+
return 0;
|
|
35713
|
+
}
|
|
35714
|
+
return left < right ? -1 : 1;
|
|
35727
35715
|
}
|
|
35728
35716
|
function orderRuntimeAssessmentTests(tests) {
|
|
35729
35717
|
return [...tests].toSorted(compareAssessmentCatalogOrder);
|
|
@@ -35894,7 +35882,7 @@ async function reviewBankRevision(input) {
|
|
|
35894
35882
|
})}`);
|
|
35895
35883
|
return `review-bank-v1:${bankRevisionUuid}`;
|
|
35896
35884
|
}
|
|
35897
|
-
async function
|
|
35885
|
+
async function buildReviewBankIndexFromReferences(input) {
|
|
35898
35886
|
return {
|
|
35899
35887
|
...input,
|
|
35900
35888
|
bankRevision: await reviewBankRevision(input)
|
|
@@ -35923,7 +35911,7 @@ async function buildReviewBankIndex(assessment, questions, options = {}) {
|
|
|
35923
35911
|
standards
|
|
35924
35912
|
};
|
|
35925
35913
|
});
|
|
35926
|
-
return
|
|
35914
|
+
return buildReviewBankIndexFromReferences({
|
|
35927
35915
|
bankIdentifier: assessment.identifier,
|
|
35928
35916
|
sourceContentRevision: assessment.contentRevision,
|
|
35929
35917
|
items
|
|
@@ -36028,7 +36016,7 @@ async function reviewBankIndexFromManifest(metadata2, input) {
|
|
|
36028
36016
|
}
|
|
36029
36017
|
}
|
|
36030
36018
|
}
|
|
36031
|
-
const index = await
|
|
36019
|
+
const index = await buildReviewBankIndexFromReferences({
|
|
36032
36020
|
bankIdentifier,
|
|
36033
36021
|
sourceContentRevision,
|
|
36034
36022
|
items: input.membershipItemIdentifiers.map((itemIdentifier) => ({
|
|
@@ -36383,6 +36371,18 @@ function isRoutedDiagnosticAttemptMetadata(value, itemSubmissions, responseVersi
|
|
|
36383
36371
|
function isReviewAttemptMetadata(value) {
|
|
36384
36372
|
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);
|
|
36385
36373
|
}
|
|
36374
|
+
function isOptionalInteger(value) {
|
|
36375
|
+
return value === undefined || Number.isInteger(value);
|
|
36376
|
+
}
|
|
36377
|
+
function isMasteryWriteWarning(value) {
|
|
36378
|
+
return isRecord(value) && value.code === "MASTERY_WRITE_CAPPED" && typeof value.message === "string" && Number.isFinite(value.currentMasteredUnits) && Number.isFinite(value.attemptedMasteredUnits) && Number.isFinite(value.appliedMasteredUnits) && Number.isFinite(value.storedMasteredUnits) && Number.isFinite(value.masterableUnits);
|
|
36379
|
+
}
|
|
36380
|
+
function isAssessmentAwardRecord(value) {
|
|
36381
|
+
if (value === undefined) {
|
|
36382
|
+
return true;
|
|
36383
|
+
}
|
|
36384
|
+
return isRecord(value) && typeof value.xpAwarded === "number" && Number.isFinite(value.xpAwarded) && value.xpAwarded >= 0 && (value.masteredUnits === undefined || Number.isFinite(value.masteredUnits)) && isOptionalInteger(value.masteredUnitsAbsolute) && Number.isFinite(value.masteredUnitsApplied) && isOptionalInteger(value.pctCompleteApp) && (value.warnings === undefined || Array.isArray(value.warnings) && value.warnings.every(isMasteryWriteWarning));
|
|
36385
|
+
}
|
|
36386
36386
|
function isMasteryAttemptMetadata(value) {
|
|
36387
36387
|
return isRecord(value) && typeof value.requestFingerprint === "string" && isAssessmentStandardRef(value.standard);
|
|
36388
36388
|
}
|
|
@@ -36407,7 +36407,7 @@ function isPlaycademyAssessmentResultMetadataV1(value) {
|
|
|
36407
36407
|
const review = metadata2.review;
|
|
36408
36408
|
const diagnostic = metadata2.diagnostic;
|
|
36409
36409
|
const routedDiagnostic = metadata2.purpose === "diagnostic" && diagnostic !== undefined && Array.isArray(metadata2.itemSubmissions) && Number.isInteger(metadata2.responseVersion) && isRoutedDiagnosticAttemptMetadata(diagnostic, metadata2.itemSubmissions, metadata2.responseVersion);
|
|
36410
|
-
return metadata2.version === 1 && typeof metadata2.activityId === "string" && isAssessmentPurpose(metadata2.purpose) && typeof metadata2.courseId === "string" && typeof metadata2.integrationId === "string" && typeof metadata2.enrollmentId === "string" && Boolean(selectedTest) && (selectedTest?.assessmentKey === undefined || isNonEmptyString(selectedTest.assessmentKey)) && typeof selectedTest?.identifier === "string" && typeof selectedTest.contentRevision === "string" && Number.isInteger(metadata2.attemptNumber) && Number.isInteger(metadata2.responseVersion) && Boolean(metadata2.responses) && typeof metadata2.responses === "object" && Array.isArray(metadata2.itemSubmissions) && metadata2.itemSubmissions.every(isAssessmentItemSubmission) && typeof metadata2.startedAt === "string" && typeof metadata2.updatedAt === "string" && (metadata2.finalizedAt === undefined || typeof metadata2.finalizedAt === "string") && (metadata2.purpose === "mastery" ? isMasteryAttemptMetadata(mastery) : mastery === undefined) && (metadata2.purpose === "review" ? isReviewAttemptMetadata(review) : review === undefined) && (metadata2.purpose === "diagnostic" ? diagnostic === undefined || routedDiagnostic : diagnostic === undefined) && (metadata2.submissionId === undefined || typeof metadata2.submissionId === "string") && (metadata2.score === undefined || isAssessmentScore(metadata2.score)) && isCompletionMetadata(metadata2.completion, metadata2.purpose, routedDiagnostic);
|
|
36410
|
+
return metadata2.version === 1 && typeof metadata2.activityId === "string" && isAssessmentPurpose(metadata2.purpose) && typeof metadata2.courseId === "string" && typeof metadata2.integrationId === "string" && typeof metadata2.enrollmentId === "string" && Boolean(selectedTest) && (selectedTest?.assessmentKey === undefined || isNonEmptyString(selectedTest.assessmentKey)) && typeof selectedTest?.identifier === "string" && typeof selectedTest.contentRevision === "string" && Number.isInteger(metadata2.attemptNumber) && Number.isInteger(metadata2.responseVersion) && Boolean(metadata2.responses) && typeof metadata2.responses === "object" && Array.isArray(metadata2.itemSubmissions) && metadata2.itemSubmissions.every(isAssessmentItemSubmission) && typeof metadata2.startedAt === "string" && typeof metadata2.updatedAt === "string" && (metadata2.finalizedAt === undefined || typeof metadata2.finalizedAt === "string") && isAssessmentAwardRecord(metadata2.award) && (metadata2.purpose === "mastery" ? isMasteryAttemptMetadata(mastery) : mastery === undefined) && (metadata2.purpose === "review" ? isReviewAttemptMetadata(review) : review === undefined) && (metadata2.purpose === "diagnostic" ? diagnostic === undefined || routedDiagnostic : diagnostic === undefined) && (metadata2.submissionId === undefined || typeof metadata2.submissionId === "string") && (metadata2.score === undefined || isAssessmentScore(metadata2.score)) && isCompletionMetadata(metadata2.completion, metadata2.purpose, routedDiagnostic);
|
|
36411
36411
|
}
|
|
36412
36412
|
function isPlaycademyReviewAssessmentItemResultMetadataV1(value) {
|
|
36413
36413
|
if (!isRecord(value) || !isRecord(value.responses)) {
|
|
@@ -36556,7 +36556,7 @@ var REVIEW_SELECTION_POLICY_VERSION = "unseen-first-least-recently-delivered-v1"
|
|
|
36556
36556
|
var PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY = "playcademyReviewBank";
|
|
36557
36557
|
var PLAYCADEMY_REVIEW_BANK_MANIFEST_VERSION = 1;
|
|
36558
36558
|
var DEFAULT_REVIEW_SELECTION_POLICY;
|
|
36559
|
-
var ASSESSMENT_RUNTIME_FIXTURE_BUNDLE_VERSION =
|
|
36559
|
+
var ASSESSMENT_RUNTIME_FIXTURE_BUNDLE_VERSION = 8;
|
|
36560
36560
|
var RuntimeSubjectSchema;
|
|
36561
36561
|
var RuntimeGradeSchema;
|
|
36562
36562
|
var OptionalQueryGradeSchema;
|
|
@@ -36927,7 +36927,12 @@ var init_assessment_runtime2 = __esm(() => {
|
|
|
36927
36927
|
});
|
|
36928
36928
|
FinalizeAssessmentBodySchema = exports_external.object({
|
|
36929
36929
|
submissionId: exports_external.string().trim().min(1),
|
|
36930
|
-
xpAwarded: exports_external.number().finite().nonnegative()
|
|
36930
|
+
xpAwarded: exports_external.number().finite().nonnegative(),
|
|
36931
|
+
masteredUnits: exports_external.number().finite().optional(),
|
|
36932
|
+
masteredUnitsAbsolute: exports_external.number().finite().int().optional()
|
|
36933
|
+
}).refine((body2) => body2.masteredUnits === undefined || body2.masteredUnitsAbsolute === undefined, {
|
|
36934
|
+
message: "Provide either masteredUnits or masteredUnitsAbsolute, not both",
|
|
36935
|
+
path: ["masteredUnits"]
|
|
36931
36936
|
});
|
|
36932
36937
|
StartRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.and(StartAssessmentBodySchema);
|
|
36933
36938
|
SaveRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.extend(SaveAssessmentBodySchema.shape);
|
|
@@ -36936,10 +36941,10 @@ var init_assessment_runtime2 = __esm(() => {
|
|
|
36936
36941
|
appName: exports_external.string().trim().min(1),
|
|
36937
36942
|
sensorUrl: exports_external.string().url()
|
|
36938
36943
|
});
|
|
36939
|
-
FinalizeRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.extend(
|
|
36944
|
+
FinalizeRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.extend({
|
|
36940
36945
|
appName: exports_external.string().trim().min(1),
|
|
36941
36946
|
sensorUrl: exports_external.string().url()
|
|
36942
|
-
});
|
|
36947
|
+
}).and(FinalizeAssessmentBodySchema);
|
|
36943
36948
|
});
|
|
36944
36949
|
var DOMAIN_CODE_BY_STATUS;
|
|
36945
36950
|
var AssessmentRuntimeError;
|
|
@@ -71642,6 +71647,15 @@ function requireMasteryStandard(input, context2) {
|
|
|
71642
71647
|
});
|
|
71643
71648
|
}
|
|
71644
71649
|
}
|
|
71650
|
+
function rejectSystemManagedReview(input, context2) {
|
|
71651
|
+
if (input.purpose === "review") {
|
|
71652
|
+
context2.addIssue({
|
|
71653
|
+
code: "custom",
|
|
71654
|
+
path: ["purpose"],
|
|
71655
|
+
message: "The review bank is system-managed and cannot be managed manually"
|
|
71656
|
+
});
|
|
71657
|
+
}
|
|
71658
|
+
}
|
|
71645
71659
|
var TimebackGradeSchema;
|
|
71646
71660
|
var TimebackSubjectSchema;
|
|
71647
71661
|
var CourseGoalsSchema;
|
|
@@ -71684,7 +71698,6 @@ var UpdateAssessmentRequestSchema;
|
|
|
71684
71698
|
var CopyAssessmentRequestSchema;
|
|
71685
71699
|
var AssessmentAssociationImportEntrySchema;
|
|
71686
71700
|
var AttachExistingAssessmentsRequestSchema;
|
|
71687
|
-
var ReorderAssessmentsRequestSchema;
|
|
71688
71701
|
var ReorderQuestionsRequestSchema;
|
|
71689
71702
|
var init_schemas4 = __esm(() => {
|
|
71690
71703
|
init_esm();
|
|
@@ -71946,7 +71959,10 @@ var init_schemas4 = __esm(() => {
|
|
|
71946
71959
|
title: exports_external.string().min(1, "Assessment title is required"),
|
|
71947
71960
|
purpose: AssessmentPurposeSchema,
|
|
71948
71961
|
standard: AssessmentStandardRefSchema2.optional()
|
|
71949
|
-
}).superRefine(
|
|
71962
|
+
}).superRefine((input, context2) => {
|
|
71963
|
+
requireMasteryStandard(input, context2);
|
|
71964
|
+
rejectSystemManagedReview(input, context2);
|
|
71965
|
+
});
|
|
71950
71966
|
UpdateAssessmentRequestSchema = exports_external.object({
|
|
71951
71967
|
title: exports_external.string().trim().min(1, "Assessment title is required").optional(),
|
|
71952
71968
|
purpose: AssessmentPurposeSchema.optional(),
|
|
@@ -71955,21 +71971,24 @@ var init_schemas4 = __esm(() => {
|
|
|
71955
71971
|
status: AssessmentStatusSchema.optional()
|
|
71956
71972
|
}).refine((input) => input.title !== undefined || input.purpose !== undefined || input.standard !== undefined || input.diagnostic !== undefined || input.status !== undefined, {
|
|
71957
71973
|
message: "Title, purpose, standard, diagnostic, or status is required"
|
|
71958
|
-
});
|
|
71974
|
+
}).superRefine(rejectSystemManagedReview);
|
|
71959
71975
|
CopyAssessmentRequestSchema = exports_external.object({
|
|
71960
71976
|
assessmentKey: AssessmentKeySchema,
|
|
71961
71977
|
testIdentifier: exports_external.string().trim().min(1, "Assessment identifier is required"),
|
|
71962
71978
|
purpose: AssessmentPurposeSchema,
|
|
71963
71979
|
standard: AssessmentStandardRefSchema2.optional()
|
|
71964
|
-
}).superRefine(
|
|
71980
|
+
}).superRefine((input, context2) => {
|
|
71981
|
+
requireMasteryStandard(input, context2);
|
|
71982
|
+
rejectSystemManagedReview(input, context2);
|
|
71983
|
+
});
|
|
71965
71984
|
AssessmentAssociationImportEntrySchema = exports_external.object({
|
|
71966
71985
|
assessmentKey: AssessmentKeySchema,
|
|
71967
71986
|
qtiTestIdentifier: exports_external.string().trim().min(1, "Assessment identifier is required"),
|
|
71968
71987
|
purpose: AssessmentPurposeSchema,
|
|
71969
|
-
standard: AssessmentStandardRefSchema2.optional()
|
|
71970
|
-
sortOrder: exports_external.number().int().nonnegative().nullable()
|
|
71988
|
+
standard: AssessmentStandardRefSchema2.optional()
|
|
71971
71989
|
}).superRefine((input, context2) => {
|
|
71972
71990
|
requireMasteryStandard(input, context2);
|
|
71991
|
+
rejectSystemManagedReview(input, context2);
|
|
71973
71992
|
if (input.purpose !== "mastery" && input.standard !== undefined) {
|
|
71974
71993
|
context2.addIssue({
|
|
71975
71994
|
code: "custom",
|
|
@@ -72016,10 +72035,6 @@ var init_schemas4 = __esm(() => {
|
|
|
72016
72035
|
seenKeys.add(assessment.assessmentKey);
|
|
72017
72036
|
});
|
|
72018
72037
|
});
|
|
72019
|
-
ReorderAssessmentsRequestSchema = exports_external.object({
|
|
72020
|
-
purpose: AssessmentPurposeSchema,
|
|
72021
|
-
testIdentifiers: exports_external.array(exports_external.string().trim().min(1, "Assessment identifier is required")).min(1, "At least one assessment is required")
|
|
72022
|
-
});
|
|
72023
72038
|
ReorderQuestionsRequestSchema = exports_external.object({
|
|
72024
72039
|
items: exports_external.array(exports_external.object({
|
|
72025
72040
|
identifier: exports_external.string().min(1),
|
|
@@ -116646,6 +116661,7 @@ function buildAssessmentCompletionEvent(input) {
|
|
|
116646
116661
|
totalQuestions: input.totalQuestions,
|
|
116647
116662
|
correctQuestions: input.correctQuestions,
|
|
116648
116663
|
xpEarned: input.xpAwarded,
|
|
116664
|
+
...input.masteredUnits ? { masteredUnits: input.masteredUnits } : {},
|
|
116649
116665
|
attemptNumber: input.attemptNumber,
|
|
116650
116666
|
process: false,
|
|
116651
116667
|
subject: input.subject,
|
|
@@ -116654,7 +116670,10 @@ function buildAssessmentCompletionEvent(input) {
|
|
|
116654
116670
|
eventId: input.eventId,
|
|
116655
116671
|
eventTime: input.submittedAt,
|
|
116656
116672
|
runId: input.attemptId,
|
|
116657
|
-
generatedExtensions:
|
|
116673
|
+
generatedExtensions: {
|
|
116674
|
+
...metadata2,
|
|
116675
|
+
...input.pctCompleteApp !== undefined ? { pctCompleteApp: input.pctCompleteApp } : {}
|
|
116676
|
+
},
|
|
116658
116677
|
eventExtensions: {
|
|
116659
116678
|
playcademy: {
|
|
116660
116679
|
assessmentPurpose: input.purpose,
|
|
@@ -116708,6 +116727,85 @@ function deriveSourcedIds(courseId) {
|
|
|
116708
116727
|
componentResource: `${courseId}-cr`
|
|
116709
116728
|
};
|
|
116710
116729
|
}
|
|
116730
|
+
function hasMasteryAwardRequest(request) {
|
|
116731
|
+
return request.masteredUnits !== undefined || request.masteredUnitsAbsolute !== undefined;
|
|
116732
|
+
}
|
|
116733
|
+
|
|
116734
|
+
class ActivityMasteryAward {
|
|
116735
|
+
mastery;
|
|
116736
|
+
events;
|
|
116737
|
+
constructor(mastery, events) {
|
|
116738
|
+
this.mastery = mastery;
|
|
116739
|
+
this.events = events;
|
|
116740
|
+
}
|
|
116741
|
+
async evaluate(input) {
|
|
116742
|
+
const ids = deriveSourcedIds(input.courseId);
|
|
116743
|
+
const progress = await this.mastery.checkProgress({
|
|
116744
|
+
studentId: input.studentId,
|
|
116745
|
+
courseId: input.courseId,
|
|
116746
|
+
resourceId: ids.resource,
|
|
116747
|
+
masteredUnits: input.masteredUnits ?? 0,
|
|
116748
|
+
masteredUnitsAbsolute: input.masteredUnitsAbsolute
|
|
116749
|
+
});
|
|
116750
|
+
if (!progress) {
|
|
116751
|
+
return {
|
|
116752
|
+
masteredUnitsApplied: input.masteredUnits ?? 0,
|
|
116753
|
+
masteryAchieved: false,
|
|
116754
|
+
masteryRevoked: false
|
|
116755
|
+
};
|
|
116756
|
+
}
|
|
116757
|
+
return {
|
|
116758
|
+
masteredUnitsApplied: progress.effectiveDelta,
|
|
116759
|
+
pctCompleteApp: progress.pctCompleteApp,
|
|
116760
|
+
masteryAchieved: progress.masteryAchieved,
|
|
116761
|
+
masteryRevoked: progress.masteryRevoked,
|
|
116762
|
+
...progress.writeWarning ? { warnings: [progress.writeWarning] } : {}
|
|
116763
|
+
};
|
|
116764
|
+
}
|
|
116765
|
+
async settle(evaluation, input) {
|
|
116766
|
+
let completionEntryWritten = false;
|
|
116767
|
+
if (evaluation.masteryAchieved) {
|
|
116768
|
+
setAttributes({ "app.timeback.mastery_achieved": true });
|
|
116769
|
+
completionEntryWritten = await this.mastery.createCompletionEntry(input.studentId, input.courseId, input.classId, input.appName);
|
|
116770
|
+
await this.emitCourseCompletionHistoryEvent(input);
|
|
116771
|
+
}
|
|
116772
|
+
if (evaluation.masteryRevoked) {
|
|
116773
|
+
await this.mastery.revokeCompletionEntry(input.studentId, input.courseId, input.classId, input.appName);
|
|
116774
|
+
}
|
|
116775
|
+
return { completionEntryWritten };
|
|
116776
|
+
}
|
|
116777
|
+
async emitCourseCompletionHistoryEvent(input) {
|
|
116778
|
+
const ids = deriveSourcedIds(input.courseId);
|
|
116779
|
+
await this.events.emitActivityEvent({
|
|
116780
|
+
eventId: input.completionHistoryEventId,
|
|
116781
|
+
studentId: input.studentId,
|
|
116782
|
+
studentEmail: input.studentEmail,
|
|
116783
|
+
gameId: input.gameId,
|
|
116784
|
+
activityId: input.activityId,
|
|
116785
|
+
activityName: "Course completed",
|
|
116786
|
+
courseId: ids.course,
|
|
116787
|
+
courseName: input.courseName,
|
|
116788
|
+
subject: input.subject,
|
|
116789
|
+
appName: input.appName,
|
|
116790
|
+
sensorUrl: input.sensorUrl,
|
|
116791
|
+
process: false,
|
|
116792
|
+
includeAttempt: false,
|
|
116793
|
+
eventExtensions: {
|
|
116794
|
+
playcademy: {
|
|
116795
|
+
eventKind: "course-completed",
|
|
116796
|
+
source: "gameplay"
|
|
116797
|
+
}
|
|
116798
|
+
},
|
|
116799
|
+
generatedExtensions: {
|
|
116800
|
+
playcademy: {
|
|
116801
|
+
eventKind: "course-completed",
|
|
116802
|
+
source: "gameplay",
|
|
116803
|
+
activityId: input.activityId
|
|
116804
|
+
}
|
|
116805
|
+
}
|
|
116806
|
+
}).catch(catchEvent("timeback.caliper_completion_event_failed"));
|
|
116807
|
+
}
|
|
116808
|
+
}
|
|
116711
116809
|
function validateProgressData(progressData) {
|
|
116712
116810
|
if (!progressData.subject) {
|
|
116713
116811
|
throw new ConfigurationError("subject", "Subject is required for Caliper events. Provide it in progressData.subject");
|
|
@@ -116723,12 +116821,12 @@ function validateProgressData(progressData) {
|
|
|
116723
116821
|
class ActivityRecord {
|
|
116724
116822
|
core;
|
|
116725
116823
|
students;
|
|
116726
|
-
|
|
116824
|
+
masteryAward;
|
|
116727
116825
|
events;
|
|
116728
|
-
constructor(core3, students,
|
|
116826
|
+
constructor(core3, students, masteryAward, events) {
|
|
116729
116827
|
this.core = core3;
|
|
116730
116828
|
this.students = students;
|
|
116731
|
-
this.
|
|
116829
|
+
this.masteryAward = masteryAward;
|
|
116732
116830
|
this.events = events;
|
|
116733
116831
|
}
|
|
116734
116832
|
async record(courseId, studentIdentifier, progressData) {
|
|
@@ -116754,52 +116852,32 @@ class ActivityRecord {
|
|
|
116754
116852
|
throw error88;
|
|
116755
116853
|
}
|
|
116756
116854
|
const legacyLineItemId = `${ids.course}-${activityId}-assessment`;
|
|
116757
|
-
const [currentAttemptNumber,
|
|
116855
|
+
const [currentAttemptNumber, mastery] = await Promise.all([
|
|
116758
116856
|
this.resolveAttemptNumber(attemptNumber, studentId, caliperLineItemId, legacyLineItemId),
|
|
116759
|
-
this.
|
|
116857
|
+
this.masteryAward.evaluate({
|
|
116760
116858
|
studentId,
|
|
116761
116859
|
courseId,
|
|
116762
|
-
|
|
116763
|
-
masteredUnits: progressData.masteredUnits ?? 0,
|
|
116860
|
+
masteredUnits: progressData.masteredUnits,
|
|
116764
116861
|
masteredUnitsAbsolute: progressData.masteredUnitsAbsolute
|
|
116765
116862
|
})
|
|
116766
116863
|
]);
|
|
116767
116864
|
setAttributes({ "app.timeback.attempt_number": currentAttemptNumber });
|
|
116768
|
-
|
|
116769
|
-
const
|
|
116770
|
-
|
|
116771
|
-
|
|
116772
|
-
|
|
116773
|
-
|
|
116774
|
-
|
|
116775
|
-
|
|
116776
|
-
|
|
116777
|
-
|
|
116778
|
-
|
|
116779
|
-
|
|
116780
|
-
|
|
116781
|
-
|
|
116782
|
-
|
|
116783
|
-
|
|
116784
|
-
}
|
|
116785
|
-
if (masteryAchieved) {
|
|
116786
|
-
completionEntryWritten = await this.mastery.createCompletionEntry(studentId, courseId, progressData.classId, progressData.appName);
|
|
116787
|
-
await this.emitCourseCompletionHistoryEvent({
|
|
116788
|
-
studentId,
|
|
116789
|
-
studentEmail,
|
|
116790
|
-
gameId: progressData.gameId,
|
|
116791
|
-
activityId,
|
|
116792
|
-
courseId: ids.course,
|
|
116793
|
-
courseName,
|
|
116794
|
-
subject: progressData.subject,
|
|
116795
|
-
appName: progressData.appName,
|
|
116796
|
-
sensorUrl: progressData.sensorUrl,
|
|
116797
|
-
eventId: progressData.completionHistoryEventId
|
|
116798
|
-
});
|
|
116799
|
-
}
|
|
116800
|
-
if (masteryProgress?.masteryRevoked) {
|
|
116801
|
-
await this.mastery.revokeCompletionEntry(studentId, courseId, progressData.classId, progressData.appName);
|
|
116802
|
-
}
|
|
116865
|
+
const effectiveMasteredUnits = mastery.masteredUnitsApplied;
|
|
116866
|
+
const { pctCompleteApp, warnings } = mastery;
|
|
116867
|
+
const extensions = pctCompleteApp !== undefined ? { ...progressData.extensions, pctCompleteApp } : progressData.extensions;
|
|
116868
|
+
const { completionEntryWritten } = await this.masteryAward.settle(mastery, {
|
|
116869
|
+
studentId,
|
|
116870
|
+
studentEmail,
|
|
116871
|
+
courseId,
|
|
116872
|
+
classId: progressData.classId,
|
|
116873
|
+
gameId: progressData.gameId,
|
|
116874
|
+
activityId,
|
|
116875
|
+
courseName,
|
|
116876
|
+
subject: progressData.subject,
|
|
116877
|
+
appName: progressData.appName,
|
|
116878
|
+
sensorUrl: progressData.sensorUrl,
|
|
116879
|
+
completionHistoryEventId: progressData.completionHistoryEventId
|
|
116880
|
+
});
|
|
116803
116881
|
try {
|
|
116804
116882
|
await this.events.emitActivityEvent({
|
|
116805
116883
|
studentId,
|
|
@@ -116820,7 +116898,7 @@ class ActivityRecord {
|
|
|
116820
116898
|
appName: progressData.appName,
|
|
116821
116899
|
sensorUrl: progressData.sensorUrl,
|
|
116822
116900
|
eventId: progressData.eventId,
|
|
116823
|
-
extensions
|
|
116901
|
+
extensions,
|
|
116824
116902
|
...progressData.runId ? { runId: progressData.runId } : {}
|
|
116825
116903
|
}).catch((error88) => {
|
|
116826
116904
|
setAttributes({ "app.timeback.caliper_emit_failed": true });
|
|
@@ -116841,36 +116919,6 @@ class ActivityRecord {
|
|
|
116841
116919
|
...warnings ? { warnings } : {}
|
|
116842
116920
|
};
|
|
116843
116921
|
}
|
|
116844
|
-
async emitCourseCompletionHistoryEvent(data) {
|
|
116845
|
-
await this.events.emitActivityEvent({
|
|
116846
|
-
eventId: data.eventId,
|
|
116847
|
-
studentId: data.studentId,
|
|
116848
|
-
studentEmail: data.studentEmail,
|
|
116849
|
-
gameId: data.gameId,
|
|
116850
|
-
activityId: data.activityId,
|
|
116851
|
-
activityName: "Course completed",
|
|
116852
|
-
courseId: data.courseId,
|
|
116853
|
-
courseName: data.courseName,
|
|
116854
|
-
subject: data.subject,
|
|
116855
|
-
appName: data.appName,
|
|
116856
|
-
sensorUrl: data.sensorUrl,
|
|
116857
|
-
process: false,
|
|
116858
|
-
includeAttempt: false,
|
|
116859
|
-
eventExtensions: {
|
|
116860
|
-
playcademy: {
|
|
116861
|
-
eventKind: "course-completed",
|
|
116862
|
-
source: "gameplay"
|
|
116863
|
-
}
|
|
116864
|
-
},
|
|
116865
|
-
generatedExtensions: {
|
|
116866
|
-
playcademy: {
|
|
116867
|
-
eventKind: "course-completed",
|
|
116868
|
-
source: "gameplay",
|
|
116869
|
-
activityId: data.activityId
|
|
116870
|
-
}
|
|
116871
|
-
}
|
|
116872
|
-
}).catch(catchEvent("timeback.caliper_completion_event_failed"));
|
|
116873
|
-
}
|
|
116874
116922
|
async resolveAttemptNumber(providedAttemptNumber, studentId, caliperLineItemId, legacyLineItemId) {
|
|
116875
116923
|
if (providedAttemptNumber) {
|
|
116876
116924
|
setAttributes({ "app.timeback.attempt_source": "provided" });
|
|
@@ -117268,9 +117316,12 @@ class ActivityEvents {
|
|
|
117268
117316
|
}
|
|
117269
117317
|
}
|
|
117270
117318
|
function createActivityNamespace(core3, deps) {
|
|
117271
|
-
const
|
|
117319
|
+
const masteryAward = new ActivityMasteryAward(deps.mastery, deps.events);
|
|
117320
|
+
const record3 = new ActivityRecord(core3, deps.students, masteryAward, deps.events);
|
|
117272
117321
|
const session2 = new ActivitySession(deps.students, deps.events);
|
|
117273
117322
|
return {
|
|
117323
|
+
evaluateMasteryAward: (input) => masteryAward.evaluate(input),
|
|
117324
|
+
settleMasteryAward: (evaluation, input) => masteryAward.settle(evaluation, input),
|
|
117274
117325
|
record: (courseId, studentIdentifier, progressData) => record3.record(courseId, studentIdentifier, progressData),
|
|
117275
117326
|
session: (courseId, studentIdentifier, sessionData) => session2.record(courseId, studentIdentifier, sessionData),
|
|
117276
117327
|
listEvents: (params) => deps.events.listEvents(params),
|
|
@@ -118560,6 +118611,7 @@ var StudentNotFoundError;
|
|
|
118560
118611
|
var ConfigurationError;
|
|
118561
118612
|
var UUID_REGEX2;
|
|
118562
118613
|
var ONEROSTER_PATHS;
|
|
118614
|
+
var NO_MASTERY_AWARD;
|
|
118563
118615
|
var TIMEBACK_API_URLS;
|
|
118564
118616
|
var QTI_API_URL = "https://qti.alpha-1edtech.ai/api";
|
|
118565
118617
|
var TIMEBACK_AUTH_URLS;
|
|
@@ -118576,6 +118628,7 @@ var GRADE_VALUES3;
|
|
|
118576
118628
|
var MASTERY_WRITE_CAPPED_WARNING_CODE = "MASTERY_WRITE_CAPPED";
|
|
118577
118629
|
var EmailSchema;
|
|
118578
118630
|
var init_dist5 = __esm(async () => {
|
|
118631
|
+
init_spans();
|
|
118579
118632
|
init_spans();
|
|
118580
118633
|
init_src();
|
|
118581
118634
|
init_spans();
|
|
@@ -118636,6 +118689,11 @@ var init_dist5 = __esm(async () => {
|
|
|
118636
118689
|
courses: "/ims/oneroster/rostering/v1p2/courses",
|
|
118637
118690
|
componentResources: "/ims/oneroster/rostering/v1p2/courses/component-resources"
|
|
118638
118691
|
};
|
|
118692
|
+
NO_MASTERY_AWARD = {
|
|
118693
|
+
masteredUnitsApplied: 0,
|
|
118694
|
+
masteryAchieved: false,
|
|
118695
|
+
masteryRevoked: false
|
|
118696
|
+
};
|
|
118639
118697
|
TIMEBACK_API_URLS = {
|
|
118640
118698
|
production: "https://api.alpha-1edtech.ai",
|
|
118641
118699
|
staging: "https://api.staging.alpha-1edtech.com"
|
|
@@ -121390,6 +121448,62 @@ function stringField2(value) {
|
|
|
121390
121448
|
function firstStringField2(...values) {
|
|
121391
121449
|
return values.map(stringField2).find(Boolean) ?? "";
|
|
121392
121450
|
}
|
|
121451
|
+
function normalizedWhitespace2(value) {
|
|
121452
|
+
return value.normalize("NFKC").trim().replace(/\s+/g, " ");
|
|
121453
|
+
}
|
|
121454
|
+
function normalizedIdentityCase2(value) {
|
|
121455
|
+
return value.toLocaleUpperCase("en-US");
|
|
121456
|
+
}
|
|
121457
|
+
function frameworkAliasKey2(value) {
|
|
121458
|
+
return value.normalize("NFKC").toLocaleLowerCase("en-US").replace(/[^a-z0-9]+/g, "");
|
|
121459
|
+
}
|
|
121460
|
+
function isCommonCoreMathIdentifier2(identifier) {
|
|
121461
|
+
const canonical = normalizedIdentityCase2(normalizedWhitespace2(identifier));
|
|
121462
|
+
return canonical.startsWith("CCSS.MATH.") || /^(?:K|[1-8])\.(?:CC|OA|NBT|NF|MD|RP|NS|EE|F|G|SP)\./.test(canonical) || /^(?:HS)?(?:N|A|F|G|S)-[A-Z]+\./.test(canonical) || /^MP\.?\d/.test(canonical);
|
|
121463
|
+
}
|
|
121464
|
+
function isCommonCoreElaIdentifier2(identifier) {
|
|
121465
|
+
const canonical = normalizedIdentityCase2(normalizedWhitespace2(identifier));
|
|
121466
|
+
return canonical.startsWith("CCSS.ELA-LITERACY.") || /^(?:RL|RI|RF|W|SL|L)\.(?:K|[1-9]|1[0-2])\./.test(canonical) || /^(?:RH|RST|WHST)\.(?:6-8|9-10|11-12)\./.test(canonical) || /^CCRA\.(?:R|W|SL|L)\./.test(canonical);
|
|
121467
|
+
}
|
|
121468
|
+
function canonicalFramework2(authoredFramework, identifier) {
|
|
121469
|
+
const aliasKey = frameworkAliasKey2(authoredFramework);
|
|
121470
|
+
if (COMMON_CORE_MATH_FRAMEWORK_ALIASES2.has(aliasKey) || COMMON_CORE_FRAMEWORK_ALIASES2.has(aliasKey) && isCommonCoreMathIdentifier2(identifier)) {
|
|
121471
|
+
return "CCSS.Math";
|
|
121472
|
+
}
|
|
121473
|
+
if (COMMON_CORE_ELA_FRAMEWORK_ALIASES2.has(aliasKey) || COMMON_CORE_FRAMEWORK_ALIASES2.has(aliasKey) && isCommonCoreElaIdentifier2(identifier)) {
|
|
121474
|
+
return "CCSS.ELA-Literacy";
|
|
121475
|
+
}
|
|
121476
|
+
if (COMMON_CORE_FRAMEWORK_ALIASES2.has(aliasKey)) {
|
|
121477
|
+
return "CCSS";
|
|
121478
|
+
}
|
|
121479
|
+
return normalizedIdentityCase2(authoredFramework);
|
|
121480
|
+
}
|
|
121481
|
+
function canonicalIdentifier2(framework, authoredIdentifier) {
|
|
121482
|
+
const identifier = normalizedIdentityCase2(normalizedWhitespace2(authoredIdentifier));
|
|
121483
|
+
if (framework === "CCSS.Math") {
|
|
121484
|
+
return identifier.replace(/^CCSS\.MATH\.CONTENT\./, "").replace(/^CCSS\.MATH\.PRACTICE\./, "").replace(/^CCSS\.MATH\./, "");
|
|
121485
|
+
}
|
|
121486
|
+
if (framework === "CCSS.ELA-Literacy") {
|
|
121487
|
+
return identifier.replace(/^CCSS\.ELA-LITERACY\./, "");
|
|
121488
|
+
}
|
|
121489
|
+
return identifier;
|
|
121490
|
+
}
|
|
121491
|
+
function canonicalAssessmentStandardRef2(standard) {
|
|
121492
|
+
const authoredFramework = normalizedWhitespace2(standard.framework);
|
|
121493
|
+
const framework = canonicalFramework2(authoredFramework, standard.identifier);
|
|
121494
|
+
const identifier = canonicalIdentifier2(framework, standard.identifier);
|
|
121495
|
+
return {
|
|
121496
|
+
framework,
|
|
121497
|
+
identifier
|
|
121498
|
+
};
|
|
121499
|
+
}
|
|
121500
|
+
function assessmentStandardRefKey2(standard) {
|
|
121501
|
+
const canonical = canonicalAssessmentStandardRef2(standard);
|
|
121502
|
+
return JSON.stringify([
|
|
121503
|
+
normalizedIdentityCase2(canonical.framework),
|
|
121504
|
+
normalizedIdentityCase2(canonical.identifier)
|
|
121505
|
+
]);
|
|
121506
|
+
}
|
|
121393
121507
|
function dedupeStandards2(standards) {
|
|
121394
121508
|
const deduped = new Map;
|
|
121395
121509
|
for (const standard of standards) {
|
|
@@ -124161,6 +124275,22 @@ var init_locks = __esm(() => {
|
|
|
124161
124275
|
}
|
|
124162
124276
|
};
|
|
124163
124277
|
});
|
|
124278
|
+
async function crossAssessmentAttemptLockBarrier(lock, db2, attemptId) {
|
|
124279
|
+
const maxAttempts = 5;
|
|
124280
|
+
for (let attempt = 1;attempt <= maxAttempts; attempt += 1) {
|
|
124281
|
+
try {
|
|
124282
|
+
await lock(db2, [assessmentRuntimeAttemptLockKey(attemptId)], async () => {
|
|
124283
|
+
return;
|
|
124284
|
+
});
|
|
124285
|
+
return;
|
|
124286
|
+
} catch (error88) {
|
|
124287
|
+
if (!(error88 instanceof ServiceUnavailableError) || attempt === maxAttempts) {
|
|
124288
|
+
throw error88;
|
|
124289
|
+
}
|
|
124290
|
+
await sleep(25);
|
|
124291
|
+
}
|
|
124292
|
+
}
|
|
124293
|
+
}
|
|
124164
124294
|
function normalizeKeys(keys) {
|
|
124165
124295
|
return [...new Set(keys)].toSorted();
|
|
124166
124296
|
}
|
|
@@ -125176,6 +125306,97 @@ var init_timeback_qti_authoring_util = __esm(() => {
|
|
|
125176
125306
|
buildMatchQuestionXml
|
|
125177
125307
|
];
|
|
125178
125308
|
});
|
|
125309
|
+
function assessmentPreparationContentionError(expectedResponseVersion, responseVersion, kind) {
|
|
125310
|
+
if (expectedResponseVersion === responseVersion) {
|
|
125311
|
+
return new ServiceUnavailableError(`The ${kind} response state changed repeatedly while this item was being prepared. Try again shortly.`, {
|
|
125312
|
+
retryable: true,
|
|
125313
|
+
reason: "PREPARATION_CONTENTION",
|
|
125314
|
+
expectedResponseVersion,
|
|
125315
|
+
responseVersion
|
|
125316
|
+
});
|
|
125317
|
+
}
|
|
125318
|
+
return AssessmentRuntimeError.from(assessmentResponseVersionConflict(expectedResponseVersion, responseVersion));
|
|
125319
|
+
}
|
|
125320
|
+
function assessmentTrackableCourse(integration) {
|
|
125321
|
+
if (!isTimebackGrade2(integration.grade) || !isTimebackSubject(integration.subject)) {
|
|
125322
|
+
return null;
|
|
125323
|
+
}
|
|
125324
|
+
return { grade: integration.grade, subject: integration.subject };
|
|
125325
|
+
}
|
|
125326
|
+
function buildAssessmentActivityData(input) {
|
|
125327
|
+
if (!input.course) {
|
|
125328
|
+
return;
|
|
125329
|
+
}
|
|
125330
|
+
return {
|
|
125331
|
+
activityId: input.activityId,
|
|
125332
|
+
activityName: input.activityName,
|
|
125333
|
+
...input.course,
|
|
125334
|
+
courseId: input.courseId
|
|
125335
|
+
};
|
|
125336
|
+
}
|
|
125337
|
+
function buildDiagnosticAssessmentSnapshot(input) {
|
|
125338
|
+
return {
|
|
125339
|
+
attemptId: input.attemptId,
|
|
125340
|
+
responseVersion: input.metadata.responseVersion,
|
|
125341
|
+
status: "in_progress",
|
|
125342
|
+
flow: "platform-routed-item-submit",
|
|
125343
|
+
...input.activityData ? { activityData: input.activityData } : {},
|
|
125344
|
+
assessment: assessmentPresentationForAttempt(input.assessment, input.attemptId),
|
|
125345
|
+
selection: {
|
|
125346
|
+
kind: "platform-routed-diagnostic",
|
|
125347
|
+
purpose: "diagnostic",
|
|
125348
|
+
definitionId: input.metadata.diagnostic.definitionId,
|
|
125349
|
+
diagnosticKey: input.metadata.diagnostic.diagnosticKey,
|
|
125350
|
+
routingRevision: input.metadata.diagnostic.routingRevision
|
|
125351
|
+
},
|
|
125352
|
+
routing: projectDiagnosticRoutingSnapshot(input.metadata.diagnostic.routingRevision, input.state),
|
|
125353
|
+
completion: null
|
|
125354
|
+
};
|
|
125355
|
+
}
|
|
125356
|
+
function buildConventionalAssessmentSnapshot(input) {
|
|
125357
|
+
const metadata2 = input.metadata;
|
|
125358
|
+
let selection;
|
|
125359
|
+
if (metadata2.purpose === "review") {
|
|
125360
|
+
selection = {
|
|
125361
|
+
kind: "standards-review",
|
|
125362
|
+
purpose: metadata2.purpose,
|
|
125363
|
+
standards: [...metadata2.review.standards],
|
|
125364
|
+
candidateItemsPerStandard: metadata2.review.candidateItemsPerStandard,
|
|
125365
|
+
selections: [...metadata2.review.selections],
|
|
125366
|
+
fulfillment: metadata2.review.fulfillment
|
|
125367
|
+
};
|
|
125368
|
+
} else if (metadata2.purpose === "mastery") {
|
|
125369
|
+
selection = {
|
|
125370
|
+
kind: "standard-quiz",
|
|
125371
|
+
purpose: metadata2.purpose,
|
|
125372
|
+
standard: metadata2.mastery.standard
|
|
125373
|
+
};
|
|
125374
|
+
} else {
|
|
125375
|
+
selection = { kind: "fixed-test", purpose: metadata2.purpose };
|
|
125376
|
+
}
|
|
125377
|
+
return {
|
|
125378
|
+
attemptId: input.attemptId,
|
|
125379
|
+
responseVersion: metadata2.responseVersion,
|
|
125380
|
+
status: "in_progress",
|
|
125381
|
+
flow: assessmentFlowForPurpose(metadata2.purpose),
|
|
125382
|
+
...input.activityData ? { activityData: input.activityData } : {},
|
|
125383
|
+
assessment: assessmentPresentationForAttempt(input.assessment, input.attemptId),
|
|
125384
|
+
responses: metadata2.responses,
|
|
125385
|
+
itemSubmissions: metadata2.itemSubmissions,
|
|
125386
|
+
score: null,
|
|
125387
|
+
selection
|
|
125388
|
+
};
|
|
125389
|
+
}
|
|
125390
|
+
function buildAssessmentAwardRecord(input, mastery) {
|
|
125391
|
+
return {
|
|
125392
|
+
xpAwarded: input.xpAwarded,
|
|
125393
|
+
...input.masteredUnits !== undefined ? { masteredUnits: input.masteredUnits } : {},
|
|
125394
|
+
...input.masteredUnitsAbsolute !== undefined ? { masteredUnitsAbsolute: input.masteredUnitsAbsolute } : {},
|
|
125395
|
+
masteredUnitsApplied: mastery.masteredUnitsApplied,
|
|
125396
|
+
...mastery.pctCompleteApp !== undefined ? { pctCompleteApp: mastery.pctCompleteApp } : {},
|
|
125397
|
+
...mastery.warnings ? { warnings: mastery.warnings } : {}
|
|
125398
|
+
};
|
|
125399
|
+
}
|
|
125179
125400
|
function stageAssessmentAttemptSupersession(attempt) {
|
|
125180
125401
|
Object.assign(attempt.result, ASSESSMENT_ATTEMPT_SUPERSEDED);
|
|
125181
125402
|
Object.assign(attempt.selection, ASSESSMENT_ATTEMPT_SUPERSEDED);
|
|
@@ -125386,13 +125607,35 @@ function buildAssessmentSubmitResult(input) {
|
|
|
125386
125607
|
}
|
|
125387
125608
|
return { ...base, purpose: input.metadata.purpose };
|
|
125388
125609
|
}
|
|
125610
|
+
function assessmentAwardFromResult(result, metadata2) {
|
|
125611
|
+
if (metadata2.award) {
|
|
125612
|
+
return metadata2.award;
|
|
125613
|
+
}
|
|
125614
|
+
const xp = result.metadata?.xp;
|
|
125615
|
+
return typeof xp === "number" && Number.isFinite(xp) && xp >= 0 ? { xpAwarded: xp, masteredUnitsApplied: 0 } : null;
|
|
125616
|
+
}
|
|
125389
125617
|
function buildCompletedAssessmentSubmitResult(input) {
|
|
125390
|
-
const
|
|
125391
|
-
|
|
125618
|
+
const attemptId = input.result.sourcedId;
|
|
125619
|
+
const award = assessmentAwardFromResult(input.result, input.metadata);
|
|
125620
|
+
if (award === null) {
|
|
125621
|
+
throw new GoneError("This completed assessment is missing its recorded XP.", {
|
|
125622
|
+
attemptId
|
|
125623
|
+
});
|
|
125624
|
+
}
|
|
125625
|
+
const submitted = buildAssessmentSubmitResult({ attemptId, metadata: input.metadata });
|
|
125626
|
+
if (!submitted) {
|
|
125627
|
+
throw new GoneError("This completed assessment has incomplete persisted results.", {
|
|
125628
|
+
attemptId
|
|
125629
|
+
});
|
|
125630
|
+
}
|
|
125631
|
+
return {
|
|
125392
125632
|
...submitted,
|
|
125393
125633
|
status: "completed",
|
|
125394
|
-
xpAwarded:
|
|
125395
|
-
|
|
125634
|
+
xpAwarded: award.xpAwarded,
|
|
125635
|
+
masteredUnitsApplied: award.masteredUnitsApplied,
|
|
125636
|
+
...award.pctCompleteApp !== undefined ? { pctCompleteApp: award.pctCompleteApp } : {},
|
|
125637
|
+
...award.warnings ? { warnings: award.warnings } : {}
|
|
125638
|
+
};
|
|
125396
125639
|
}
|
|
125397
125640
|
function assessmentAttemptId(input) {
|
|
125398
125641
|
return deterministicUUID([
|
|
@@ -125719,6 +125962,22 @@ function assessmentFixtureScoringKeys(assessment, questions) {
|
|
|
125719
125962
|
...Object.keys(responseAreas).length > 0 ? { responseAreas } : {}
|
|
125720
125963
|
};
|
|
125721
125964
|
}
|
|
125965
|
+
function prepareDiagnosticAssessmentResponses(assessment, current, input) {
|
|
125966
|
+
const update2 = { [input.itemIdentifier]: input.responses };
|
|
125967
|
+
validateAssessmentResponseUpdate(assessment, update2);
|
|
125968
|
+
const responses = applyAssessmentResponseUpdate(current, update2);
|
|
125969
|
+
const item = assessment.items.find((candidate) => candidate.identifier === input.itemIdentifier);
|
|
125970
|
+
const itemResponses = responses[input.itemIdentifier];
|
|
125971
|
+
const missingResponse = item?.interactions.find((interaction) => itemResponses?.[interaction.responseIdentifier] === undefined);
|
|
125972
|
+
if (!item || !itemResponses || missingResponse) {
|
|
125973
|
+
throw new AssessmentRuntimeError("INVALID_RESPONSE", `Diagnostic item ${input.itemIdentifier} requires a response for every interaction.`, {
|
|
125974
|
+
itemIdentifier: input.itemIdentifier,
|
|
125975
|
+
...missingResponse ? { responseIdentifier: missingResponse.responseIdentifier } : {}
|
|
125976
|
+
});
|
|
125977
|
+
}
|
|
125978
|
+
validateAssessmentResponses(assessment, { [input.itemIdentifier]: itemResponses });
|
|
125979
|
+
return responses;
|
|
125980
|
+
}
|
|
125722
125981
|
function validateAssessmentResponseUpdate(assessment, update2) {
|
|
125723
125982
|
const message = assessmentResponseUpdateValidationMessage(assessment, update2);
|
|
125724
125983
|
if (message) {
|
|
@@ -125805,7 +126064,7 @@ function buildAssessmentResultSubmission(input) {
|
|
|
125805
126064
|
}
|
|
125806
126065
|
};
|
|
125807
126066
|
}
|
|
125808
|
-
function
|
|
126067
|
+
function buildAssessmentResultAwardFinalization(input) {
|
|
125809
126068
|
const completion = input.metadata.completion;
|
|
125810
126069
|
if (!completion || completion.totalQuestions === undefined) {
|
|
125811
126070
|
throw new Error("A scored assessment must retain its completion facts before finalization");
|
|
@@ -125813,8 +126072,10 @@ function buildAssessmentResultXpFinalization(input) {
|
|
|
125813
126072
|
const metadata2 = {
|
|
125814
126073
|
...input.metadata,
|
|
125815
126074
|
finalizedAt: input.timestamp,
|
|
125816
|
-
updatedAt: input.timestamp
|
|
126075
|
+
updatedAt: input.timestamp,
|
|
126076
|
+
award: input.award
|
|
125817
126077
|
};
|
|
126078
|
+
const masteryRequested = input.award.masteredUnits !== undefined || input.award.masteredUnitsAbsolute !== undefined;
|
|
125818
126079
|
return {
|
|
125819
126080
|
metadata: metadata2,
|
|
125820
126081
|
resultUpdate: {
|
|
@@ -125826,7 +126087,8 @@ function buildAssessmentResultXpFinalization(input) {
|
|
|
125826
126087
|
...ASSESSMENT_ATTEMPT_COMPLETED,
|
|
125827
126088
|
metadata: {
|
|
125828
126089
|
...mergeAssessmentRuntimeMetadata(input.result.metadata, metadata2),
|
|
125829
|
-
xp: input.xpAwarded,
|
|
126090
|
+
xp: input.award.xpAwarded,
|
|
126091
|
+
...masteryRequested ? { masteredUnits: input.award.masteredUnitsApplied } : {},
|
|
125830
126092
|
totalQuestions: completion.totalQuestions,
|
|
125831
126093
|
correctQuestions: completion.correctQuestions,
|
|
125832
126094
|
appName: input.appName
|
|
@@ -125839,12 +126101,217 @@ var init_timeback_assessment_runtime_util = __esm(() => {
|
|
|
125839
126101
|
init_src();
|
|
125840
126102
|
init_assessment_runtime2();
|
|
125841
126103
|
init_qti();
|
|
126104
|
+
init_types2();
|
|
125842
126105
|
init_utils6();
|
|
125843
126106
|
init_timeback4();
|
|
125844
126107
|
init_uuid();
|
|
125845
126108
|
init_errors2();
|
|
125846
126109
|
PLAYABLE_SHAPE_SET = new Set(PLAYABLE_ASSESSMENT_SHAPES);
|
|
125847
126110
|
});
|
|
126111
|
+
function reviewBankTestItemIdentifiers(test) {
|
|
126112
|
+
return qtiTestParts(test).flatMap((part) => part["qti-assessment-section"].flatMap((section) => (section["qti-assessment-item-ref"] ?? []).map((reference) => reference.identifier)));
|
|
126113
|
+
}
|
|
126114
|
+
function canonicalSyncContributors(contributors) {
|
|
126115
|
+
return contributors.map((contributor) => {
|
|
126116
|
+
const canonical = canonicalContributorIdentity(contributor);
|
|
126117
|
+
return {
|
|
126118
|
+
...contributor,
|
|
126119
|
+
...canonical
|
|
126120
|
+
};
|
|
126121
|
+
}).toSorted((left, right) => compareCodeUnits(left.standardKey, right.standardKey) || compareCodeUnits(left.qtiTestIdentifier, right.qtiTestIdentifier) || compareCodeUnits(left.assessmentKey, right.assessmentKey));
|
|
126122
|
+
}
|
|
126123
|
+
function canonicalContributorIdentity(contributor) {
|
|
126124
|
+
const standard = canonicalReviewStandardRef(contributor.standard);
|
|
126125
|
+
if (!standard) {
|
|
126126
|
+
throw new ValidationError(`Mastery assessment ${contributor.qtiTestIdentifier} has an invalid standard`);
|
|
126127
|
+
}
|
|
126128
|
+
return {
|
|
126129
|
+
qtiTestIdentifier: contributor.qtiTestIdentifier,
|
|
126130
|
+
standard,
|
|
126131
|
+
standardKey: assessmentStandardRefKey2(standard)
|
|
126132
|
+
};
|
|
126133
|
+
}
|
|
126134
|
+
async function reviewBankContributorFingerprint(contributors) {
|
|
126135
|
+
const uniqueTuples = new Map;
|
|
126136
|
+
for (const contributor of contributors) {
|
|
126137
|
+
const { qtiTestIdentifier, standardKey } = canonicalContributorIdentity(contributor);
|
|
126138
|
+
const tuple3 = { qtiTestIdentifier, standardKey };
|
|
126139
|
+
uniqueTuples.set(canonicalJson2(tuple3), tuple3);
|
|
126140
|
+
}
|
|
126141
|
+
const digest = await sha256Hex(canonicalJson2([...uniqueTuples.values()].toSorted((left, right) => compareCodeUnits(left.standardKey, right.standardKey) || compareCodeUnits(left.qtiTestIdentifier, right.qtiTestIdentifier))));
|
|
126142
|
+
return `review-bank-contributors-v1:${digest}`;
|
|
126143
|
+
}
|
|
126144
|
+
function conflictWarning(input) {
|
|
126145
|
+
const retained = `${input.retainedStandard.framework} · ${input.retainedStandard.identifier}`;
|
|
126146
|
+
const skipped = `${input.skippedStandard.framework} · ${input.skippedStandard.identifier}`;
|
|
126147
|
+
return {
|
|
126148
|
+
code: "QUESTION_STANDARD_CONFLICT",
|
|
126149
|
+
...input,
|
|
126150
|
+
message: `Question ${input.itemIdentifier} is referenced by multiple mastery standards; retained ${retained} and skipped ${skipped} from ${input.skippedQtiTestIdentifier}.`
|
|
126151
|
+
};
|
|
126152
|
+
}
|
|
126153
|
+
async function prepareReviewBankSynchronization(input) {
|
|
126154
|
+
const contributors = canonicalSyncContributors(input.contributors);
|
|
126155
|
+
const selectedByItem = new Map;
|
|
126156
|
+
const warningKeys = new Set;
|
|
126157
|
+
const warnings = [];
|
|
126158
|
+
for (const contributor of contributors) {
|
|
126159
|
+
const itemIdentifiers2 = [
|
|
126160
|
+
...new Set(reviewBankTestItemIdentifiers(contributor.test))
|
|
126161
|
+
].toSorted();
|
|
126162
|
+
for (const itemIdentifier of itemIdentifiers2) {
|
|
126163
|
+
const selected = selectedByItem.get(itemIdentifier);
|
|
126164
|
+
if (!selected) {
|
|
126165
|
+
selectedByItem.set(itemIdentifier, {
|
|
126166
|
+
standard: contributor.standard,
|
|
126167
|
+
standardKey: contributor.standardKey
|
|
126168
|
+
});
|
|
126169
|
+
} else if (selected.standardKey !== contributor.standardKey) {
|
|
126170
|
+
const warningKey = `${itemIdentifier}\x00${contributor.standardKey}`;
|
|
126171
|
+
if (!warningKeys.has(warningKey)) {
|
|
126172
|
+
warningKeys.add(warningKey);
|
|
126173
|
+
warnings.push(conflictWarning({
|
|
126174
|
+
itemIdentifier,
|
|
126175
|
+
retainedStandard: selected.standard,
|
|
126176
|
+
skippedStandard: contributor.standard,
|
|
126177
|
+
skippedQtiTestIdentifier: contributor.qtiTestIdentifier
|
|
126178
|
+
}));
|
|
126179
|
+
}
|
|
126180
|
+
}
|
|
126181
|
+
}
|
|
126182
|
+
}
|
|
126183
|
+
const itemIdentifiers = [...selectedByItem.keys()].toSorted();
|
|
126184
|
+
const sourceContentRevision = input.contributorFingerprint ?? await reviewBankContributorFingerprint(contributors);
|
|
126185
|
+
const bank = await buildReviewBankIndexFromReferences({
|
|
126186
|
+
bankIdentifier: input.bankIdentifier,
|
|
126187
|
+
sourceContentRevision,
|
|
126188
|
+
items: itemIdentifiers.map((itemIdentifier) => ({
|
|
126189
|
+
itemIdentifier,
|
|
126190
|
+
standards: [selectedByItem.get(itemIdentifier).standard]
|
|
126191
|
+
}))
|
|
126192
|
+
});
|
|
126193
|
+
const manifest = await buildReviewBankManifest(bank);
|
|
126194
|
+
return {
|
|
126195
|
+
bankIdentifier: input.bankIdentifier,
|
|
126196
|
+
contributorFingerprint: sourceContentRevision,
|
|
126197
|
+
itemIdentifiers,
|
|
126198
|
+
manifest,
|
|
126199
|
+
warnings,
|
|
126200
|
+
summary: {
|
|
126201
|
+
itemCount: itemIdentifiers.length,
|
|
126202
|
+
standardCount: Object.keys(manifest.itemsByStandard).length,
|
|
126203
|
+
contributorCount: contributors.length,
|
|
126204
|
+
sourceFingerprint: manifest.sourceFingerprint
|
|
126205
|
+
}
|
|
126206
|
+
};
|
|
126207
|
+
}
|
|
126208
|
+
function reviewBankSinkMetadata(integrationId, plan) {
|
|
126209
|
+
return {
|
|
126210
|
+
ownerSystem: PLAYCADEMY_QTI_OWNER_SYSTEM,
|
|
126211
|
+
integrationId,
|
|
126212
|
+
[PLAYCADEMY_REVIEW_BANK_SINK_METADATA_KEY]: {
|
|
126213
|
+
version: PLAYCADEMY_REVIEW_BANK_SINK_VERSION,
|
|
126214
|
+
integrationId,
|
|
126215
|
+
contributorFingerprint: plan.contributorFingerprint,
|
|
126216
|
+
warnings: plan.warnings
|
|
126217
|
+
},
|
|
126218
|
+
[PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY]: plan.manifest
|
|
126219
|
+
};
|
|
126220
|
+
}
|
|
126221
|
+
function reviewBankSinkMarker(test) {
|
|
126222
|
+
const parsed = ReviewBankSinkMarkerSchema.safeParse(test.metadata?.[PLAYCADEMY_REVIEW_BANK_SINK_METADATA_KEY]);
|
|
126223
|
+
return parsed.success ? parsed.data : null;
|
|
126224
|
+
}
|
|
126225
|
+
function reviewBankSinkState(test, integrationId) {
|
|
126226
|
+
assertReviewBankSinkOwnership(test, integrationId);
|
|
126227
|
+
const marker = reviewBankSinkMarker(test);
|
|
126228
|
+
return {
|
|
126229
|
+
contributorFingerprint: marker?.contributorFingerprint ?? null,
|
|
126230
|
+
warnings: marker?.warnings ?? []
|
|
126231
|
+
};
|
|
126232
|
+
}
|
|
126233
|
+
async function unchangedReviewBankSynchronizationResult(input) {
|
|
126234
|
+
const sinkState = reviewBankSinkState(input.sink, input.integrationId);
|
|
126235
|
+
if (sinkState.contributorFingerprint !== input.contributorFingerprint) {
|
|
126236
|
+
return null;
|
|
126237
|
+
}
|
|
126238
|
+
const membershipItemIdentifiers = reviewBankTestItemIdentifiers(input.sink);
|
|
126239
|
+
const bank = await reviewBankIndexFromManifest(input.sink.metadata, {
|
|
126240
|
+
bankIdentifier: input.sink.identifier,
|
|
126241
|
+
membershipItemIdentifiers
|
|
126242
|
+
});
|
|
126243
|
+
const manifest = input.sink.metadata?.[PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY];
|
|
126244
|
+
if (!bank || typeof manifest?.sourceFingerprint !== "string") {
|
|
126245
|
+
return null;
|
|
126246
|
+
}
|
|
126247
|
+
const standardCount = new Set(bank.items.flatMap((item) => item.standards.map(assessmentStandardRefKey2))).size;
|
|
126248
|
+
return {
|
|
126249
|
+
qtiTestIdentifier: input.sink.identifier,
|
|
126250
|
+
itemCount: bank.items.length,
|
|
126251
|
+
standardCount,
|
|
126252
|
+
contributorCount: input.contributorCount,
|
|
126253
|
+
sourceFingerprint: manifest.sourceFingerprint,
|
|
126254
|
+
addedItemCount: 0,
|
|
126255
|
+
removedItemCount: 0,
|
|
126256
|
+
unchanged: true,
|
|
126257
|
+
warnings: sinkState.warnings
|
|
126258
|
+
};
|
|
126259
|
+
}
|
|
126260
|
+
function reviewBankSynchronizationDiff(input) {
|
|
126261
|
+
let previousMembershipReadable = true;
|
|
126262
|
+
let previousItemIdentifiers = [];
|
|
126263
|
+
if (input.existingSink) {
|
|
126264
|
+
try {
|
|
126265
|
+
previousItemIdentifiers = reviewBankTestItemIdentifiers(input.existingSink);
|
|
126266
|
+
} catch {
|
|
126267
|
+
previousMembershipReadable = false;
|
|
126268
|
+
}
|
|
126269
|
+
}
|
|
126270
|
+
const previousItems = new Set(previousItemIdentifiers);
|
|
126271
|
+
const desiredItems = new Set(input.plan.itemIdentifiers);
|
|
126272
|
+
const addedItemCount = input.plan.itemIdentifiers.filter((identifier) => !previousItems.has(identifier)).length;
|
|
126273
|
+
const removedItemCount = [...previousItems].filter((identifier) => !desiredItems.has(identifier)).length;
|
|
126274
|
+
const currentManifest = input.existingSink?.metadata?.[PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY];
|
|
126275
|
+
const currentContributorFingerprint = input.existingSink ? reviewBankSinkMarker(input.existingSink)?.contributorFingerprint ?? null : null;
|
|
126276
|
+
const canonicalMembership = previousItemIdentifiers.length === input.plan.itemIdentifiers.length && previousItemIdentifiers.every((identifier, index2) => identifier === input.plan.itemIdentifiers[index2]);
|
|
126277
|
+
const unchanged = input.existingSink !== null && previousMembershipReadable && canonicalMembership && canonicalJson2(currentManifest) === canonicalJson2(input.plan.manifest) && currentContributorFingerprint === input.plan.contributorFingerprint;
|
|
126278
|
+
return { addedItemCount, removedItemCount, unchanged };
|
|
126279
|
+
}
|
|
126280
|
+
function assertReviewBankSinkOwnership(test, integrationId) {
|
|
126281
|
+
const marker = reviewBankSinkMarker(test);
|
|
126282
|
+
if (marker?.integrationId !== integrationId || test.metadata?.ownerSystem !== PLAYCADEMY_QTI_OWNER_SYSTEM || test.identifier !== reviewBankSinkIdentifier(integrationId)) {
|
|
126283
|
+
throw new ValidationError(`QTI assessment ${test.identifier} is not the review-bank sink for this integration`);
|
|
126284
|
+
}
|
|
126285
|
+
}
|
|
126286
|
+
function buildReviewBankSinkInput(input) {
|
|
126287
|
+
return {
|
|
126288
|
+
...input.includeIdentifier ? { identifier: input.identifier } : {},
|
|
126289
|
+
title: input.title,
|
|
126290
|
+
metadata: input.metadata,
|
|
126291
|
+
"qti-test-part": [
|
|
126292
|
+
{
|
|
126293
|
+
identifier: `${input.identifier}-part1`,
|
|
126294
|
+
navigationMode: "linear",
|
|
126295
|
+
submissionMode: "individual",
|
|
126296
|
+
"qti-assessment-section": [
|
|
126297
|
+
{
|
|
126298
|
+
identifier: `${input.identifier}-section1`,
|
|
126299
|
+
title: "Questions",
|
|
126300
|
+
visible: true,
|
|
126301
|
+
required: true,
|
|
126302
|
+
fixed: false,
|
|
126303
|
+
sequence: 1,
|
|
126304
|
+
"qti-assessment-item-ref": input.itemIdentifiers.map((itemIdentifier, index2) => ({
|
|
126305
|
+
identifier: itemIdentifier,
|
|
126306
|
+
href: input.itemHref(itemIdentifier),
|
|
126307
|
+
sequence: index2 + 1
|
|
126308
|
+
}))
|
|
126309
|
+
}
|
|
126310
|
+
]
|
|
126311
|
+
}
|
|
126312
|
+
]
|
|
126313
|
+
};
|
|
126314
|
+
}
|
|
125848
126315
|
function reviewMappingIssueSummary(label, identifiers) {
|
|
125849
126316
|
if (identifiers.length === 0) {
|
|
125850
126317
|
return null;
|
|
@@ -125893,11 +126360,50 @@ async function prepareReviewMappingUpdate(test, questions) {
|
|
|
125893
126360
|
}
|
|
125894
126361
|
};
|
|
125895
126362
|
}
|
|
126363
|
+
var PLAYCADEMY_REVIEW_BANK_SINK_METADATA_KEY = "playcademyReviewBankSink";
|
|
126364
|
+
var PLAYCADEMY_REVIEW_BANK_SINK_VERSION = 1;
|
|
126365
|
+
var StoredSinkWarningSchema;
|
|
126366
|
+
var ReviewBankSinkMarkerSchema;
|
|
125896
126367
|
var init_timeback_review_mapping_util = __esm(() => {
|
|
126368
|
+
init_esm();
|
|
125897
126369
|
init_assessment_runtime2();
|
|
126370
|
+
init_qti();
|
|
126371
|
+
init_timeback3();
|
|
125898
126372
|
init_errors2();
|
|
125899
126373
|
init_timeback_assessment_runtime_util();
|
|
125900
126374
|
init_timeback_qti_authoring_util();
|
|
126375
|
+
StoredSinkWarningSchema = exports_external.object({
|
|
126376
|
+
code: exports_external.literal("QUESTION_STANDARD_CONFLICT"),
|
|
126377
|
+
itemIdentifier: exports_external.string(),
|
|
126378
|
+
retainedStandard: exports_external.object({ framework: exports_external.string(), identifier: exports_external.string() }),
|
|
126379
|
+
skippedStandard: exports_external.object({ framework: exports_external.string(), identifier: exports_external.string() }),
|
|
126380
|
+
skippedQtiTestIdentifier: exports_external.string()
|
|
126381
|
+
});
|
|
126382
|
+
ReviewBankSinkMarkerSchema = exports_external.object({
|
|
126383
|
+
version: exports_external.literal(PLAYCADEMY_REVIEW_BANK_SINK_VERSION),
|
|
126384
|
+
integrationId: exports_external.string(),
|
|
126385
|
+
contributorFingerprint: exports_external.string().nullable().catch(null),
|
|
126386
|
+
warnings: exports_external.array(StoredSinkWarningSchema).nullable().catch(null)
|
|
126387
|
+
}).transform((marker) => {
|
|
126388
|
+
if (!marker.contributorFingerprint || !marker.warnings) {
|
|
126389
|
+
return { ...marker, contributorFingerprint: null, warnings: [] };
|
|
126390
|
+
}
|
|
126391
|
+
const warnings = [];
|
|
126392
|
+
for (const warning of marker.warnings) {
|
|
126393
|
+
const retained = canonicalReviewStandardRef(warning.retainedStandard);
|
|
126394
|
+
const skipped = canonicalReviewStandardRef(warning.skippedStandard);
|
|
126395
|
+
if (!retained || !skipped) {
|
|
126396
|
+
return { ...marker, contributorFingerprint: null, warnings: [] };
|
|
126397
|
+
}
|
|
126398
|
+
warnings.push(conflictWarning({
|
|
126399
|
+
itemIdentifier: warning.itemIdentifier,
|
|
126400
|
+
retainedStandard: retained,
|
|
126401
|
+
skippedStandard: skipped,
|
|
126402
|
+
skippedQtiTestIdentifier: warning.skippedQtiTestIdentifier
|
|
126403
|
+
}));
|
|
126404
|
+
}
|
|
126405
|
+
return { ...marker, warnings };
|
|
126406
|
+
});
|
|
125901
126407
|
});
|
|
125902
126408
|
function assessmentKeyFromManagedQtiIdentifier(identifier) {
|
|
125903
126409
|
const assessmentKey = /^playcademy-test\.(.+)\.[a-f0-9]{64}$/.exec(identifier)?.[1];
|
|
@@ -126201,11 +126707,6 @@ function assertAssessmentHasQuestions(questions) {
|
|
|
126201
126707
|
throw new ValidationError("An assessment must contain at least one question to publish");
|
|
126202
126708
|
}
|
|
126203
126709
|
}
|
|
126204
|
-
function assertReviewAssessmentHasStandards(standardCounts) {
|
|
126205
|
-
if (standardCounts.some((count) => count !== 1)) {
|
|
126206
|
-
throw new ValidationError("Every question in a review assessment must have exactly one standards alignment");
|
|
126207
|
-
}
|
|
126208
|
-
}
|
|
126209
126710
|
function planAssessmentRemoval(status) {
|
|
126210
126711
|
if (status === "draft") {
|
|
126211
126712
|
return { kind: "delete", action: "discarded", operation: "discard_draft" };
|
|
@@ -126223,9 +126724,6 @@ function buildAssessmentAssociationUpdates(row, input) {
|
|
|
126223
126724
|
updates.standardFramework = null;
|
|
126224
126725
|
updates.standardIdentifier = null;
|
|
126225
126726
|
}
|
|
126226
|
-
if (row.status === "live" && input.purpose !== row.purpose) {
|
|
126227
|
-
updates.sortOrder = null;
|
|
126228
|
-
}
|
|
126229
126727
|
}
|
|
126230
126728
|
if (input.standard !== undefined) {
|
|
126231
126729
|
updates.standardFramework = input.standard.framework;
|
|
@@ -126233,9 +126731,6 @@ function buildAssessmentAssociationUpdates(row, input) {
|
|
|
126233
126731
|
}
|
|
126234
126732
|
if (input.status !== undefined) {
|
|
126235
126733
|
updates.status = input.status;
|
|
126236
|
-
if (input.status === "archived") {
|
|
126237
|
-
updates.sortOrder = null;
|
|
126238
|
-
}
|
|
126239
126734
|
}
|
|
126240
126735
|
return updates;
|
|
126241
126736
|
}
|
|
@@ -126244,28 +126739,15 @@ function assessmentStandardForRow(row) {
|
|
|
126244
126739
|
}
|
|
126245
126740
|
function validateUniqueAssessmentIdentifiers(testIdentifiers) {
|
|
126246
126741
|
if (new Set(testIdentifiers).size !== testIdentifiers.length) {
|
|
126247
|
-
throw new ValidationError("Assessment
|
|
126248
|
-
}
|
|
126249
|
-
}
|
|
126250
|
-
function assertAssessmentOrderUpdateSucceeded(updatedRow) {
|
|
126251
|
-
if (!updatedRow) {
|
|
126252
|
-
throw new ValidationError("Assessment order changed while it was being saved. Refresh and try again.");
|
|
126253
|
-
}
|
|
126254
|
-
return updatedRow;
|
|
126255
|
-
}
|
|
126256
|
-
function lockOrderAssessmentRows(rows) {
|
|
126257
|
-
return rows.toSorted((left, right) => left.id.localeCompare(right.id));
|
|
126258
|
-
}
|
|
126259
|
-
function orderLiveAssessmentRows(liveRows, purpose, testIdentifiers) {
|
|
126260
|
-
const rowsByIdentifier = new Map(liveRows.map((row) => [row.qtiTestIdentifier, row]));
|
|
126261
|
-
if (liveRows.length !== testIdentifiers.length || testIdentifiers.some((identifier) => !rowsByIdentifier.has(identifier))) {
|
|
126262
|
-
throw new ValidationError(`Assessment order must include every live ${purpose} assessment exactly once`);
|
|
126742
|
+
throw new ValidationError("Assessment manifests must contain unique QTI test identifiers");
|
|
126263
126743
|
}
|
|
126264
|
-
return testIdentifiers.map((identifier) => rowsByIdentifier.get(identifier));
|
|
126265
126744
|
}
|
|
126266
126745
|
var init_timeback_assessment_rules_util = __esm(() => {
|
|
126267
126746
|
init_errors2();
|
|
126268
126747
|
});
|
|
126748
|
+
function qtiItemHref(client2, itemIdentifier) {
|
|
126749
|
+
return `${client2.getQtiBaseUrl()}/assessment-items/${itemIdentifier}`;
|
|
126750
|
+
}
|
|
126269
126751
|
async function hydrateQtiTestQuestions(client2, references) {
|
|
126270
126752
|
const questions = await runWithConcurrency(references.questions, QTI_HYDRATION_CONCURRENCY, async (reference) => ({
|
|
126271
126753
|
...reference,
|
|
@@ -126288,6 +126770,28 @@ async function hydrateQtiTestQuestionSelection(client2, references, itemIdentifi
|
|
|
126288
126770
|
questions: selectedReferences
|
|
126289
126771
|
});
|
|
126290
126772
|
}
|
|
126773
|
+
async function hydrateQtiItemSelection(client2, test, itemIdentifiers) {
|
|
126774
|
+
const testPart = qtiTestParts(test)[0];
|
|
126775
|
+
const section = testPart?.["qti-assessment-section"][0];
|
|
126776
|
+
if (!testPart || !section) {
|
|
126777
|
+
throw new Error(`QTI assessment ${test.identifier} has no section to reference items from`);
|
|
126778
|
+
}
|
|
126779
|
+
const questions = await runWithConcurrency(itemIdentifiers, QTI_HYDRATION_CONCURRENCY, async (itemIdentifier) => ({
|
|
126780
|
+
reference: {
|
|
126781
|
+
identifier: itemIdentifier,
|
|
126782
|
+
href: qtiItemHref(client2, itemIdentifier),
|
|
126783
|
+
testPart: testPart.identifier,
|
|
126784
|
+
section: section.identifier
|
|
126785
|
+
},
|
|
126786
|
+
question: await client2.qtiApi.assessmentItems.get(itemIdentifier)
|
|
126787
|
+
}));
|
|
126788
|
+
return {
|
|
126789
|
+
assessmentTest: test.identifier,
|
|
126790
|
+
title: test.title,
|
|
126791
|
+
totalQuestions: questions.length,
|
|
126792
|
+
questions
|
|
126793
|
+
};
|
|
126794
|
+
}
|
|
126291
126795
|
async function loadQtiTestReferences(client2, identifier) {
|
|
126292
126796
|
const [test, references] = await Promise.all([
|
|
126293
126797
|
client2.qtiApi.assessmentTests.get(identifier),
|
|
@@ -126303,7 +126807,9 @@ async function loadHydratedQtiTest(client2, identifier) {
|
|
|
126303
126807
|
};
|
|
126304
126808
|
}
|
|
126305
126809
|
var QTI_HYDRATION_CONCURRENCY = 8;
|
|
126306
|
-
var init_timeback_qti_hydration_util = () => {
|
|
126810
|
+
var init_timeback_qti_hydration_util = __esm(() => {
|
|
126811
|
+
init_timeback_qti_authoring_util();
|
|
126812
|
+
});
|
|
126307
126813
|
var TimebackAssessmentRuntimeService;
|
|
126308
126814
|
var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
126309
126815
|
init_drizzle_orm();
|
|
@@ -126331,8 +126837,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
126331
126837
|
static EXPORT_CONCURRENCY = 4;
|
|
126332
126838
|
static ITEM_SUBMISSION_MAX_PREPARATION_ATTEMPTS = 3;
|
|
126333
126839
|
static ASSESSMENT_VERSION_CONFIRMATION_DELAY_MS = 25;
|
|
126334
|
-
static PROJECTION_BARRIER_MAX_ATTEMPTS = 5;
|
|
126335
|
-
static PROJECTION_BARRIER_RETRY_DELAY_MS = 25;
|
|
126336
126840
|
static SCORING_CONCURRENCY = 4;
|
|
126337
126841
|
static STALE_AWARD_AGE_MS = 900000;
|
|
126338
126842
|
deps;
|
|
@@ -126875,15 +127379,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
126875
127379
|
this.assertInProgress(attempt.result);
|
|
126876
127380
|
if (!preview || preview.responseVersion !== attempt.metadata.responseVersion || preview.submissionId !== input.submissionId || preview.itemIdentifier !== input.itemIdentifier) {
|
|
126877
127381
|
if (preparationAttempts >= TimebackAssessmentRuntimeService2.ITEM_SUBMISSION_MAX_PREPARATION_ATTEMPTS) {
|
|
126878
|
-
|
|
126879
|
-
throw new ServiceUnavailableError("The assessment response state changed repeatedly while this item was being prepared. Try again shortly.", {
|
|
126880
|
-
retryable: true,
|
|
126881
|
-
reason: "PREPARATION_CONTENTION",
|
|
126882
|
-
expectedResponseVersion: input.expectedResponseVersion,
|
|
126883
|
-
responseVersion: attempt.metadata.responseVersion
|
|
126884
|
-
});
|
|
126885
|
-
}
|
|
126886
|
-
throw AssessmentRuntimeError.from(assessmentResponseVersionConflict(input.expectedResponseVersion, attempt.metadata.responseVersion));
|
|
127382
|
+
throw assessmentPreparationContentionError(input.expectedResponseVersion, attempt.metadata.responseVersion, "assessment");
|
|
126887
127383
|
}
|
|
126888
127384
|
return { action: "prepare" };
|
|
126889
127385
|
}
|
|
@@ -127006,15 +127502,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
127006
127502
|
this.assertInProgress(attempt.result);
|
|
127007
127503
|
if (!preview || preview.responseVersion !== metadata2.responseVersion || preview.submissionId !== input.submissionId || preview.routingNodeKey !== input.routingNodeKey || preview.itemIdentifier !== input.itemIdentifier) {
|
|
127008
127504
|
if (preparationAttempts >= TimebackAssessmentRuntimeService2.ITEM_SUBMISSION_MAX_PREPARATION_ATTEMPTS) {
|
|
127009
|
-
|
|
127010
|
-
throw new ServiceUnavailableError("The diagnostic response state changed repeatedly while this item was being prepared. Try again shortly.", {
|
|
127011
|
-
retryable: true,
|
|
127012
|
-
reason: "PREPARATION_CONTENTION",
|
|
127013
|
-
expectedResponseVersion: input.expectedResponseVersion,
|
|
127014
|
-
responseVersion: metadata2.responseVersion
|
|
127015
|
-
});
|
|
127016
|
-
}
|
|
127017
|
-
throw AssessmentRuntimeError.from(assessmentResponseVersionConflict(input.expectedResponseVersion, metadata2.responseVersion));
|
|
127505
|
+
throw assessmentPreparationContentionError(input.expectedResponseVersion, metadata2.responseVersion, "diagnostic");
|
|
127018
127506
|
}
|
|
127019
127507
|
return { action: "prepare" };
|
|
127020
127508
|
}
|
|
@@ -127029,7 +127517,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
127029
127517
|
itemIdentifier: input.itemIdentifier
|
|
127030
127518
|
}));
|
|
127031
127519
|
}
|
|
127032
|
-
const responses =
|
|
127520
|
+
const responses = prepareDiagnosticAssessmentResponses(preview.assessment, metadata2.responses, input);
|
|
127033
127521
|
const scoring = preview.scoring;
|
|
127034
127522
|
if (!scoring || typeof scoring.isCorrect !== "boolean") {
|
|
127035
127523
|
throw AssessmentRuntimeError.from(assessmentFlowViolation(`Diagnostic item ${input.itemIdentifier} did not produce determinate binary grading.`, { itemIdentifier: input.itemIdentifier }));
|
|
@@ -127102,22 +127590,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
127102
127590
|
answered: submission.answered
|
|
127103
127591
|
};
|
|
127104
127592
|
}
|
|
127105
|
-
prepareDiagnosticResponses(assessment, current, input) {
|
|
127106
|
-
const update2 = { [input.itemIdentifier]: input.responses };
|
|
127107
|
-
validateAssessmentResponseUpdate(assessment, update2);
|
|
127108
|
-
const responses = applyAssessmentResponseUpdate(current, update2);
|
|
127109
|
-
const item = assessment.items.find((candidate) => candidate.identifier === input.itemIdentifier);
|
|
127110
|
-
const itemResponses = responses[input.itemIdentifier];
|
|
127111
|
-
const missingResponse = item?.interactions.find((interaction) => itemResponses?.[interaction.responseIdentifier] === undefined);
|
|
127112
|
-
if (!item || !itemResponses || missingResponse) {
|
|
127113
|
-
throw new AssessmentRuntimeError("INVALID_RESPONSE", `Diagnostic item ${input.itemIdentifier} requires a response for every interaction.`, {
|
|
127114
|
-
itemIdentifier: input.itemIdentifier,
|
|
127115
|
-
...missingResponse ? { responseIdentifier: missingResponse.responseIdentifier } : {}
|
|
127116
|
-
});
|
|
127117
|
-
}
|
|
127118
|
-
validateAssessmentResponses(assessment, { [input.itemIdentifier]: itemResponses });
|
|
127119
|
-
return responses;
|
|
127120
|
-
}
|
|
127121
127593
|
async prepareDiagnosticItemSubmission(params, input) {
|
|
127122
127594
|
try {
|
|
127123
127595
|
return await this.scoreDiagnosticItemSubmission(params, input);
|
|
@@ -127151,7 +127623,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
127151
127623
|
const expected = loaded.state.next;
|
|
127152
127624
|
let scoring = null;
|
|
127153
127625
|
if (loaded.state.status === "in-progress" && expected?.nodeKey === input.routingNodeKey && expected.itemIdentifier === input.itemIdentifier && attempt.metadata.responseVersion === input.expectedResponseVersion) {
|
|
127154
|
-
const responses =
|
|
127626
|
+
const responses = prepareDiagnosticAssessmentResponses(assessment, attempt.metadata.responses, input);
|
|
127155
127627
|
scoring = await this.scoreItem(assessment, responses, input.itemIdentifier);
|
|
127156
127628
|
}
|
|
127157
127629
|
return {
|
|
@@ -127422,11 +127894,14 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
127422
127894
|
inProgress: attempt.result.inProgress ?? "",
|
|
127423
127895
|
scoreStatus: attempt.result.scoreStatus,
|
|
127424
127896
|
submissionId: attempt.metadata.submissionId,
|
|
127425
|
-
|
|
127897
|
+
award: assessmentAwardFromResult(attempt.result, attempt.metadata) ?? undefined
|
|
127426
127898
|
}, input);
|
|
127427
127899
|
if (disposition === "replay") {
|
|
127428
127900
|
return {
|
|
127429
|
-
response:
|
|
127901
|
+
response: buildCompletedAssessmentSubmitResult({
|
|
127902
|
+
result: attempt.result,
|
|
127903
|
+
metadata: attempt.metadata
|
|
127904
|
+
}),
|
|
127430
127905
|
completion: this.replayCompletion(attempt, input.submissionId, params, game2)
|
|
127431
127906
|
};
|
|
127432
127907
|
}
|
|
@@ -127434,20 +127909,27 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
127434
127909
|
throw AssessmentRuntimeError.from(assessmentAwardConflict({
|
|
127435
127910
|
attemptId: attempt.result.sourcedId,
|
|
127436
127911
|
submissionId: input.submissionId,
|
|
127437
|
-
xpAwarded: input.xpAwarded
|
|
127912
|
+
xpAwarded: input.xpAwarded,
|
|
127913
|
+
...input.masteredUnits !== undefined ? { masteredUnits: input.masteredUnits } : {},
|
|
127914
|
+
...input.masteredUnitsAbsolute !== undefined ? { masteredUnitsAbsolute: input.masteredUnitsAbsolute } : {}
|
|
127438
127915
|
}));
|
|
127439
127916
|
}
|
|
127440
127917
|
if (disposition === "reject") {
|
|
127441
127918
|
throw AssessmentRuntimeError.from(assessmentAlreadySubmitted(attempt.result.sourcedId));
|
|
127442
127919
|
}
|
|
127443
|
-
const
|
|
127444
|
-
const
|
|
127920
|
+
const mastery = await this.evaluateMasteryAward(attempt, params.studentId, input);
|
|
127921
|
+
const awardRecord = buildAssessmentAwardRecord(input, mastery);
|
|
127922
|
+
const finalization = buildAssessmentResultAwardFinalization({
|
|
127445
127923
|
result: attempt.result,
|
|
127446
127924
|
metadata: attempt.metadata,
|
|
127447
|
-
|
|
127925
|
+
award: awardRecord,
|
|
127448
127926
|
appName: game2.appName,
|
|
127449
127927
|
timestamp: new Date().toISOString()
|
|
127450
127928
|
});
|
|
127929
|
+
const response = buildCompletedAssessmentSubmitResult({
|
|
127930
|
+
result: attempt.result,
|
|
127931
|
+
metadata: finalization.metadata
|
|
127932
|
+
});
|
|
127451
127933
|
const finalized = await this.requireClient().api.oneroster.assessmentResults.upsert(attempt.result.sourcedId, finalization.resultUpdate);
|
|
127452
127934
|
const finalizedForEmission = {
|
|
127453
127935
|
...finalized,
|
|
@@ -127466,25 +127948,78 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
127466
127948
|
"app.assessment.submission_id": input.submissionId,
|
|
127467
127949
|
"app.assessment.purpose": finalization.metadata.purpose,
|
|
127468
127950
|
"app.assessment.award_age_ms": this.awardAgeMs(attempt.result.scoreDate),
|
|
127469
|
-
"app.assessment.xp_awarded": input.xpAwarded
|
|
127951
|
+
"app.assessment.xp_awarded": input.xpAwarded,
|
|
127952
|
+
"app.assessment.mastered_units_applied": mastery.masteredUnitsApplied,
|
|
127953
|
+
"app.assessment.mastery_achieved": mastery.masteryAchieved,
|
|
127954
|
+
"app.assessment.mastery_revoked": mastery.masteryRevoked
|
|
127470
127955
|
});
|
|
127956
|
+
await this.settleMasteryAward(finalizedAttempt, mastery, input.submissionId, game2);
|
|
127471
127957
|
return {
|
|
127472
|
-
response
|
|
127473
|
-
...submitted,
|
|
127474
|
-
status: "completed",
|
|
127475
|
-
xpAwarded: input.xpAwarded
|
|
127476
|
-
},
|
|
127958
|
+
response,
|
|
127477
127959
|
completion: this.replayCompletion(finalizedAttempt, input.submissionId, params, game2)
|
|
127478
127960
|
};
|
|
127479
127961
|
});
|
|
127480
127962
|
if (award.completion) {
|
|
127481
127963
|
await this.emitCompletionBestEffort({
|
|
127482
127964
|
...award.completion,
|
|
127483
|
-
|
|
127965
|
+
award: award.response
|
|
127484
127966
|
});
|
|
127485
127967
|
}
|
|
127486
127968
|
return award.response;
|
|
127487
127969
|
}
|
|
127970
|
+
async evaluateMasteryAward(attempt, studentIdentifier, input) {
|
|
127971
|
+
if (!hasMasteryAwardRequest(input)) {
|
|
127972
|
+
return NO_MASTERY_AWARD;
|
|
127973
|
+
}
|
|
127974
|
+
const student = await this.requireClient().roster.resolveStudent(studentIdentifier);
|
|
127975
|
+
return this.requireClient().activity.evaluateMasteryAward({
|
|
127976
|
+
studentId: student.id,
|
|
127977
|
+
courseId: attempt.integration.courseId,
|
|
127978
|
+
...input.masteredUnits !== undefined ? { masteredUnits: input.masteredUnits } : {},
|
|
127979
|
+
...input.masteredUnitsAbsolute !== undefined ? { masteredUnitsAbsolute: input.masteredUnitsAbsolute } : {}
|
|
127980
|
+
});
|
|
127981
|
+
}
|
|
127982
|
+
async settleMasteryAward(attempt, mastery, submissionId, game2) {
|
|
127983
|
+
if (!mastery.masteryAchieved && !mastery.masteryRevoked) {
|
|
127984
|
+
return;
|
|
127985
|
+
}
|
|
127986
|
+
const integration = attempt.integration;
|
|
127987
|
+
const course = this.trackableCourse(integration, "assessment.mastery_settlement_invalid_course_metadata", { "app.assessment.attempt_id": attempt.result.sourcedId });
|
|
127988
|
+
if (!course) {
|
|
127989
|
+
setAttributes({ "app.assessment.mastery_completion_entry_missing": true });
|
|
127990
|
+
return;
|
|
127991
|
+
}
|
|
127992
|
+
try {
|
|
127993
|
+
const student = await this.requireClient().roster.resolveStudent(attempt.result.student.sourcedId);
|
|
127994
|
+
const completionHistoryEventId = `urn:uuid:${await deterministicUUID([
|
|
127995
|
+
"playcademy-assessment-course-completed-event-v1",
|
|
127996
|
+
attempt.result.sourcedId,
|
|
127997
|
+
submissionId
|
|
127998
|
+
].join("\x00"))}`;
|
|
127999
|
+
const settlement = await this.requireClient().activity.settleMasteryAward(mastery, {
|
|
128000
|
+
studentId: student.id,
|
|
128001
|
+
studentEmail: student.email,
|
|
128002
|
+
courseId: integration.courseId,
|
|
128003
|
+
gameId: integration.gameId,
|
|
128004
|
+
activityId: attempt.metadata.activityId,
|
|
128005
|
+
courseName: attempt.metadata.completion?.courseName ?? "Game Course",
|
|
128006
|
+
subject: course.subject,
|
|
128007
|
+
appName: game2.appName,
|
|
128008
|
+
sensorUrl: game2.sensorUrl,
|
|
128009
|
+
completionHistoryEventId
|
|
128010
|
+
});
|
|
128011
|
+
if (mastery.masteryAchieved && !settlement.completionEntryWritten) {
|
|
128012
|
+
setAttributes({ "app.assessment.mastery_completion_entry_missing": true });
|
|
128013
|
+
}
|
|
128014
|
+
} catch (error88) {
|
|
128015
|
+
setAttributes({ "app.assessment.mastery_completion_entry_missing": true });
|
|
128016
|
+
addEvent("assessment.mastery_settlement_failed", {
|
|
128017
|
+
"app.assessment.attempt_id": attempt.result.sourcedId,
|
|
128018
|
+
"exception.type": errorType(error88),
|
|
128019
|
+
"app.error.message": errorMessage2(error88)
|
|
128020
|
+
});
|
|
128021
|
+
}
|
|
128022
|
+
}
|
|
127488
128023
|
async finalizeDiagnosticSubmission(input) {
|
|
127489
128024
|
const { attempt } = input;
|
|
127490
128025
|
const routing = await this.loadAttemptDiagnosticManifest(attempt.metadata, input.db);
|
|
@@ -127710,8 +128245,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
127710
128245
|
id: row.id,
|
|
127711
128246
|
assessmentKey: row.assessmentKey,
|
|
127712
128247
|
qtiTestIdentifier: row.qtiTestIdentifier,
|
|
127713
|
-
sortOrder: row.sortOrder,
|
|
127714
|
-
updatedAt: row.updatedAt.toISOString(),
|
|
127715
128248
|
standard: assessmentStandardForRow(row)
|
|
127716
128249
|
})).filter((test) => !matchesMasteryStandard || matchesMasteryStandard(test.standard));
|
|
127717
128250
|
}
|
|
@@ -127931,9 +128464,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
127931
128464
|
if (this.isSettled(resumed.result)) {
|
|
127932
128465
|
return this.snapshotForResult(resumed.result, resumed.metadata, context2.integration);
|
|
127933
128466
|
}
|
|
127934
|
-
const
|
|
127935
|
-
const currentSource = await this.hydrateReviewBankSelection(catalog, resumed.metadata.review.selections.map((selection) => selection.itemIdentifier));
|
|
127936
|
-
const source = this.pinnedReviewBankSource(currentSource, resumed.metadata);
|
|
128467
|
+
const source = await this.loadPinnedReviewBankSource(resumed.metadata);
|
|
127937
128468
|
const assessment = projectReviewAssessment(source.assessment, source.bank, resumed.metadata.review.selections);
|
|
127938
128469
|
await this.ensureReviewChildResults({
|
|
127939
128470
|
result: resumed.result,
|
|
@@ -127977,7 +128508,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
127977
128508
|
membershipItemIdentifiers: loaded.references.questions.map((question) => question.reference.identifier)
|
|
127978
128509
|
});
|
|
127979
128510
|
} catch (error88) {
|
|
127980
|
-
throw new ServiceUnavailableError(`Review bank ${identifier} has stale or invalid routing metadata. Use
|
|
128511
|
+
throw new ServiceUnavailableError(`Review bank ${identifier} has stale or invalid routing metadata. Use Sync Review Bank in assessment authoring.`, { reason: errorMessage2(error88) });
|
|
127981
128512
|
}
|
|
127982
128513
|
if (!bank) {
|
|
127983
128514
|
throw new ServiceUnavailableError(`Review bank ${identifier} needs its authoring mapping updated before it can serve review questions.`);
|
|
@@ -127993,28 +128524,60 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
127993
128524
|
this.reviewBankCache.set(`${identifier}\x00${bank.sourceContentRevision}`, catalog);
|
|
127994
128525
|
return catalog;
|
|
127995
128526
|
}
|
|
127996
|
-
async
|
|
127997
|
-
const cacheKey2 = [
|
|
127998
|
-
catalog.test.identifier,
|
|
127999
|
-
catalog.bank.sourceContentRevision,
|
|
128000
|
-
...itemIdentifiers
|
|
128001
|
-
].join("\x00");
|
|
128527
|
+
async cachedReviewSelection(input) {
|
|
128528
|
+
const cacheKey2 = [input.identifier, input.contentRevision, ...input.itemIdentifiers].join("\x00");
|
|
128002
128529
|
const cached3 = this.reviewSelectionCache.get(cacheKey2);
|
|
128003
128530
|
if (cached3) {
|
|
128004
|
-
return
|
|
128531
|
+
return cached3;
|
|
128005
128532
|
}
|
|
128006
|
-
const questions = await
|
|
128007
|
-
const assessment = await buildPlayableAssessment(
|
|
128008
|
-
assessment.contentRevision =
|
|
128533
|
+
const { test, questions } = await input.hydrate();
|
|
128534
|
+
const assessment = await buildPlayableAssessment(test, questions);
|
|
128535
|
+
assessment.contentRevision = input.contentRevision;
|
|
128009
128536
|
this.reviewSelectionCache.set(cacheKey2, assessment);
|
|
128537
|
+
return assessment;
|
|
128538
|
+
}
|
|
128539
|
+
async hydrateReviewBankSelection(catalog, itemIdentifiers) {
|
|
128540
|
+
const assessment = await this.cachedReviewSelection({
|
|
128541
|
+
identifier: catalog.test.identifier,
|
|
128542
|
+
contentRevision: catalog.bank.sourceContentRevision,
|
|
128543
|
+
itemIdentifiers,
|
|
128544
|
+
hydrate: async () => ({
|
|
128545
|
+
test: catalog.test,
|
|
128546
|
+
questions: await hydrateQtiTestQuestionSelection(this.requireClient(), catalog.references, itemIdentifiers)
|
|
128547
|
+
})
|
|
128548
|
+
});
|
|
128010
128549
|
return { assessment, bank: catalog.bank };
|
|
128011
128550
|
}
|
|
128012
|
-
|
|
128551
|
+
async loadPinnedReviewBankSource(metadata2) {
|
|
128552
|
+
const itemIdentifiers = metadata2.review.selections.map((selection) => selection.itemIdentifier);
|
|
128553
|
+
let assessment;
|
|
128554
|
+
try {
|
|
128555
|
+
assessment = await this.cachedReviewSelection({
|
|
128556
|
+
identifier: metadata2.selectedTest.identifier,
|
|
128557
|
+
contentRevision: metadata2.selectedTest.contentRevision,
|
|
128558
|
+
itemIdentifiers,
|
|
128559
|
+
hydrate: async () => {
|
|
128560
|
+
const test = await this.requireClient().qtiApi.assessmentTests.get(metadata2.selectedTest.identifier);
|
|
128561
|
+
return {
|
|
128562
|
+
test,
|
|
128563
|
+
questions: await hydrateQtiItemSelection(this.requireClient(), test, itemIdentifiers)
|
|
128564
|
+
};
|
|
128565
|
+
}
|
|
128566
|
+
});
|
|
128567
|
+
} catch (error88) {
|
|
128568
|
+
if (isApiError(error88) && error88.statusCode === 404) {
|
|
128569
|
+
throw new AssessmentRuntimeError("SELECTED_TEST_VERSION_UNAVAILABLE", `A selected review item for ${metadata2.selectedTest.identifier} is no longer available.`, {
|
|
128570
|
+
identifier: metadata2.selectedTest.identifier,
|
|
128571
|
+
expectedRevision: metadata2.selectedTest.contentRevision
|
|
128572
|
+
});
|
|
128573
|
+
}
|
|
128574
|
+
throw error88;
|
|
128575
|
+
}
|
|
128013
128576
|
return {
|
|
128014
|
-
assessment
|
|
128577
|
+
assessment,
|
|
128015
128578
|
bank: {
|
|
128016
|
-
bankIdentifier:
|
|
128017
|
-
sourceContentRevision:
|
|
128579
|
+
bankIdentifier: assessment.identifier,
|
|
128580
|
+
sourceContentRevision: metadata2.selectedTest.contentRevision,
|
|
128018
128581
|
bankRevision: metadata2.review.bankRevision,
|
|
128019
128582
|
items: metadata2.review.selections.map((selection) => ({
|
|
128020
128583
|
itemIdentifier: selection.itemIdentifier,
|
|
@@ -128038,9 +128601,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
128038
128601
|
if (metadata2.purpose !== "review") {
|
|
128039
128602
|
return this.loadAssessment(metadata2.selectedTest.identifier, metadata2.selectedTest.contentRevision);
|
|
128040
128603
|
}
|
|
128041
|
-
const
|
|
128042
|
-
const currentSource = await this.hydrateReviewBankSelection(catalog, metadata2.review.selections.map((selection) => selection.itemIdentifier));
|
|
128043
|
-
const source = this.pinnedReviewBankSource(currentSource, metadata2);
|
|
128604
|
+
const source = await this.loadPinnedReviewBankSource(metadata2);
|
|
128044
128605
|
return projectReviewAssessment(source.assessment, source.bank, metadata2.review.selections);
|
|
128045
128606
|
}
|
|
128046
128607
|
isPlatformRoutedDiagnosticMetadata(metadata2) {
|
|
@@ -128526,7 +129087,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
128526
129087
|
async projectReviewItemResponse(params, projection) {
|
|
128527
129088
|
await this.putReviewChildResponse(projection.result, projection.metadata, projection.itemIdentifier);
|
|
128528
129089
|
try {
|
|
128529
|
-
await this.
|
|
129090
|
+
await crossAssessmentAttemptLockBarrier(this.deps.assessmentRuntimeLock, this.deps.db, params.attemptId);
|
|
128530
129091
|
const latest = await this.peekAttempt(params);
|
|
128531
129092
|
if (this.isSettled(latest.result) && latest.metadata.purpose === "review") {
|
|
128532
129093
|
await this.restoreCompletedReviewChildResponses(latest.result, latest.metadata, new Set([projection.itemIdentifier]));
|
|
@@ -128540,21 +129101,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
128540
129101
|
});
|
|
128541
129102
|
}
|
|
128542
129103
|
}
|
|
128543
|
-
async crossAttemptLockBarrier(attemptId) {
|
|
128544
|
-
for (let attempt = 1;attempt <= TimebackAssessmentRuntimeService2.PROJECTION_BARRIER_MAX_ATTEMPTS; attempt += 1) {
|
|
128545
|
-
try {
|
|
128546
|
-
await this.deps.assessmentRuntimeLock(this.deps.db, [assessmentRuntimeAttemptLockKey(attemptId)], async () => {
|
|
128547
|
-
return;
|
|
128548
|
-
});
|
|
128549
|
-
return;
|
|
128550
|
-
} catch (error88) {
|
|
128551
|
-
if (!(error88 instanceof ServiceUnavailableError) || attempt === TimebackAssessmentRuntimeService2.PROJECTION_BARRIER_MAX_ATTEMPTS) {
|
|
128552
|
-
throw error88;
|
|
128553
|
-
}
|
|
128554
|
-
await sleep(TimebackAssessmentRuntimeService2.PROJECTION_BARRIER_RETRY_DELAY_MS);
|
|
128555
|
-
}
|
|
128556
|
-
}
|
|
128557
|
-
}
|
|
128558
129104
|
async restoreCompletedReviewChildResponses(result, metadata2, changedItemIdentifiers) {
|
|
128559
129105
|
const outcomes = metadata2.completion?.itemOutcomes;
|
|
128560
129106
|
const submissionId = metadata2.submissionId;
|
|
@@ -128700,7 +129246,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
128700
129246
|
attemptId: result.sourcedId,
|
|
128701
129247
|
responseVersion: metadata2.responseVersion,
|
|
128702
129248
|
status: "completed",
|
|
128703
|
-
result:
|
|
129249
|
+
result: buildCompletedAssessmentSubmitResult({ result, metadata: metadata2 })
|
|
128704
129250
|
};
|
|
128705
129251
|
}
|
|
128706
129252
|
const assessment = await this.loadAttemptAssessment(metadata2);
|
|
@@ -128717,58 +129263,37 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
128717
129263
|
return this.snapshot(result, metadata2, assessment, integration);
|
|
128718
129264
|
}
|
|
128719
129265
|
diagnosticSnapshot(result, metadata2, assessment, integration, state) {
|
|
128720
|
-
|
|
128721
|
-
return {
|
|
129266
|
+
return buildDiagnosticAssessmentSnapshot({
|
|
128722
129267
|
attemptId: result.sourcedId,
|
|
128723
|
-
|
|
128724
|
-
|
|
128725
|
-
|
|
128726
|
-
|
|
128727
|
-
|
|
128728
|
-
selection: {
|
|
128729
|
-
kind: "platform-routed-diagnostic",
|
|
128730
|
-
purpose: "diagnostic",
|
|
128731
|
-
definitionId: metadata2.diagnostic.definitionId,
|
|
128732
|
-
diagnosticKey: metadata2.diagnostic.diagnosticKey,
|
|
128733
|
-
routingRevision: metadata2.diagnostic.routingRevision
|
|
128734
|
-
},
|
|
128735
|
-
routing: this.diagnosticRoutingSnapshot(metadata2, state),
|
|
128736
|
-
completion: null
|
|
128737
|
-
};
|
|
129268
|
+
metadata: metadata2,
|
|
129269
|
+
assessment,
|
|
129270
|
+
state,
|
|
129271
|
+
activityData: this.activityData(metadata2, assessment, integration)
|
|
129272
|
+
});
|
|
128738
129273
|
}
|
|
128739
129274
|
diagnosticRoutingSnapshot(metadata2, state) {
|
|
128740
129275
|
return projectDiagnosticRoutingSnapshot(metadata2.diagnostic.routingRevision, state);
|
|
128741
129276
|
}
|
|
128742
129277
|
snapshot(result, metadata2, assessment, integration) {
|
|
128743
|
-
|
|
128744
|
-
const activityData = this.activityData(metadata2, assessment, integration);
|
|
128745
|
-
return {
|
|
129278
|
+
return buildConventionalAssessmentSnapshot({
|
|
128746
129279
|
attemptId: result.sourcedId,
|
|
128747
|
-
|
|
128748
|
-
|
|
128749
|
-
|
|
128750
|
-
|
|
128751
|
-
assessment: assessmentPresentationForAttempt(assessment, result.sourcedId),
|
|
128752
|
-
responses: metadata2.responses,
|
|
128753
|
-
itemSubmissions: metadata2.itemSubmissions,
|
|
128754
|
-
score: null,
|
|
128755
|
-
selection
|
|
128756
|
-
};
|
|
129280
|
+
metadata: metadata2,
|
|
129281
|
+
assessment,
|
|
129282
|
+
activityData: this.activityData(metadata2, assessment, integration)
|
|
129283
|
+
});
|
|
128757
129284
|
}
|
|
128758
129285
|
activityData(metadata2, assessment, integration) {
|
|
128759
129286
|
const course = this.trackableCourse(integration, "assessment.activity_tracking_invalid_course_metadata");
|
|
128760
|
-
|
|
128761
|
-
return;
|
|
128762
|
-
}
|
|
128763
|
-
return {
|
|
129287
|
+
return buildAssessmentActivityData({
|
|
128764
129288
|
activityId: metadata2.activityId,
|
|
128765
129289
|
activityName: assessment.title,
|
|
128766
|
-
|
|
128767
|
-
|
|
128768
|
-
};
|
|
129290
|
+
courseId: integration.courseId,
|
|
129291
|
+
course
|
|
129292
|
+
});
|
|
128769
129293
|
}
|
|
128770
129294
|
trackableCourse(integration, event, attributes2 = {}) {
|
|
128771
|
-
|
|
129295
|
+
const course = assessmentTrackableCourse(integration);
|
|
129296
|
+
if (!course) {
|
|
128772
129297
|
addEvent(event, {
|
|
128773
129298
|
...attributes2,
|
|
128774
129299
|
"app.timeback.integration_id": integration.id,
|
|
@@ -128777,27 +129302,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
128777
129302
|
});
|
|
128778
129303
|
return null;
|
|
128779
129304
|
}
|
|
128780
|
-
return
|
|
128781
|
-
}
|
|
128782
|
-
selectionContext(metadata2) {
|
|
128783
|
-
if (metadata2.purpose === "review") {
|
|
128784
|
-
return {
|
|
128785
|
-
kind: "standards-review",
|
|
128786
|
-
purpose: metadata2.purpose,
|
|
128787
|
-
standards: [...metadata2.review.standards],
|
|
128788
|
-
candidateItemsPerStandard: metadata2.review.candidateItemsPerStandard,
|
|
128789
|
-
selections: [...metadata2.review.selections],
|
|
128790
|
-
fulfillment: metadata2.review.fulfillment
|
|
128791
|
-
};
|
|
128792
|
-
}
|
|
128793
|
-
if (metadata2.purpose === "mastery") {
|
|
128794
|
-
return {
|
|
128795
|
-
kind: "standard-quiz",
|
|
128796
|
-
purpose: metadata2.purpose,
|
|
128797
|
-
standard: metadata2.mastery.standard
|
|
128798
|
-
};
|
|
128799
|
-
}
|
|
128800
|
-
return { kind: "fixed-test", purpose: metadata2.purpose };
|
|
129305
|
+
return course;
|
|
128801
129306
|
}
|
|
128802
129307
|
submittedResult(result, metadata2) {
|
|
128803
129308
|
const response = buildAssessmentSubmitResult({
|
|
@@ -128811,29 +129316,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
128811
129316
|
}
|
|
128812
129317
|
return response;
|
|
128813
129318
|
}
|
|
128814
|
-
completedResult(result, metadata2) {
|
|
128815
|
-
const xpAwarded = this.xpFromResult(result);
|
|
128816
|
-
if (xpAwarded === null) {
|
|
128817
|
-
throw new GoneError("This completed assessment is missing its recorded XP.", {
|
|
128818
|
-
attemptId: result.sourcedId
|
|
128819
|
-
});
|
|
128820
|
-
}
|
|
128821
|
-
const response = buildCompletedAssessmentSubmitResult({
|
|
128822
|
-
attemptId: result.sourcedId,
|
|
128823
|
-
metadata: metadata2,
|
|
128824
|
-
xpAwarded
|
|
128825
|
-
});
|
|
128826
|
-
if (!response) {
|
|
128827
|
-
throw new GoneError("This completed assessment has incomplete persisted results.", {
|
|
128828
|
-
attemptId: result.sourcedId
|
|
128829
|
-
});
|
|
128830
|
-
}
|
|
128831
|
-
return response;
|
|
128832
|
-
}
|
|
128833
|
-
xpFromResult(result) {
|
|
128834
|
-
const xp = result.metadata?.xp;
|
|
128835
|
-
return typeof xp === "number" && Number.isFinite(xp) && xp >= 0 ? xp : null;
|
|
128836
|
-
}
|
|
128837
129319
|
awardAgeMs(scoreDate) {
|
|
128838
129320
|
const submittedAt = Date.parse(scoreDate);
|
|
128839
129321
|
return Number.isFinite(submittedAt) ? Math.max(0, Date.now() - submittedAt) : -1;
|
|
@@ -128960,7 +129442,9 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
128960
129442
|
attemptNumber: attempt.metadata.attemptNumber,
|
|
128961
129443
|
totalQuestions: input.totalQuestions,
|
|
128962
129444
|
correctQuestions: input.correctQuestions,
|
|
128963
|
-
xpAwarded: input.xpAwarded,
|
|
129445
|
+
xpAwarded: input.award.xpAwarded,
|
|
129446
|
+
...input.award.masteredUnitsApplied !== 0 ? { masteredUnits: input.award.masteredUnitsApplied } : {},
|
|
129447
|
+
...input.award.pctCompleteApp !== undefined ? { pctCompleteApp: input.award.pctCompleteApp } : {},
|
|
128964
129448
|
submittedAt: attempt.result.scoreDate,
|
|
128965
129449
|
eventId,
|
|
128966
129450
|
...session2 ? { resumeId: session2.resumeId } : {}
|
|
@@ -129081,6 +129565,9 @@ function assertAssessmentImportCourseMatches(manifest, integration) {
|
|
|
129081
129565
|
throw new ValidationError(`This manifest is for ${manifest.subject} grade ${manifest.grade}, not ${integration.subject} grade ${integration.grade}.`);
|
|
129082
129566
|
}
|
|
129083
129567
|
}
|
|
129568
|
+
function assessmentImportsAffectReviewBank(targetStatus, entries) {
|
|
129569
|
+
return targetStatus === "live" && entries.some((entry2) => entry2.purpose === "mastery");
|
|
129570
|
+
}
|
|
129084
129571
|
function associationMetadataMatches(row, entry2) {
|
|
129085
129572
|
return row.purpose === entry2.purpose && (row.standardFramework ?? undefined) === entry2.standard?.framework && (row.standardIdentifier ?? undefined) === entry2.standard?.identifier;
|
|
129086
129573
|
}
|
|
@@ -129130,18 +129617,6 @@ function attachedAssessmentImportMessage(targetStatus, editable) {
|
|
|
129130
129617
|
}
|
|
129131
129618
|
return editable ? "Attached as a draft." : "Attached as a read-only draft owned by another app.";
|
|
129132
129619
|
}
|
|
129133
|
-
function liveReviewAssessmentImportFailures(targetStatus, candidates, existingRows) {
|
|
129134
|
-
if (targetStatus === "draft") {
|
|
129135
|
-
return new Map;
|
|
129136
|
-
}
|
|
129137
|
-
const reviews = candidates.filter((candidate) => candidate.purpose === "review");
|
|
129138
|
-
const hasExistingLiveReview = existingRows.some((row) => row.purpose === "review" && row.status === "live");
|
|
129139
|
-
if (reviews.length <= 1 && !hasExistingLiveReview) {
|
|
129140
|
-
return new Map;
|
|
129141
|
-
}
|
|
129142
|
-
const message = hasExistingLiveReview ? "Another review assessment is already live for this course." : "Only one review assessment can be imported live at a time.";
|
|
129143
|
-
return new Map(reviews.map((review) => [review.qtiTestIdentifier, message]));
|
|
129144
|
-
}
|
|
129145
129620
|
var init_timeback_assessment_import_util = __esm(() => {
|
|
129146
129621
|
init_errors2();
|
|
129147
129622
|
init_timeback_qti_authoring_util();
|
|
@@ -129210,7 +129685,9 @@ class TimebackAssessmentsService {
|
|
|
129210
129685
|
const rows = await this.deps.db.query.gameTimebackAssessmentTests.findMany({
|
|
129211
129686
|
where: eq(gameTimebackAssessmentTests.integrationId, integrationId)
|
|
129212
129687
|
});
|
|
129213
|
-
const
|
|
129688
|
+
const sinkIdentifier = reviewBankSinkIdentifier(integrationId);
|
|
129689
|
+
const visibleRows = rows.filter((row) => row.purpose !== "review" || row.qtiTestIdentifier === sinkIdentifier);
|
|
129690
|
+
const assessments2 = await runWithConcurrency(visibleRows, QTI_HYDRATION_CONCURRENCY, async (row) => {
|
|
129214
129691
|
try {
|
|
129215
129692
|
const test = await client2.qtiApi.assessmentTests.get(row.qtiTestIdentifier);
|
|
129216
129693
|
return {
|
|
@@ -129218,7 +129695,7 @@ class TimebackAssessmentsService {
|
|
|
129218
129695
|
title: test.title,
|
|
129219
129696
|
questionCount: countQtiTestItems(test),
|
|
129220
129697
|
available: true,
|
|
129221
|
-
editable: isQtiTestOwnedByGame(test, ownership.gameSlug)
|
|
129698
|
+
editable: row.purpose !== "review" && isQtiTestOwnedByGame(test, ownership.gameSlug)
|
|
129222
129699
|
};
|
|
129223
129700
|
} catch (error88) {
|
|
129224
129701
|
addEvent("assessment.qti_fetch_failed", {
|
|
@@ -129238,8 +129715,11 @@ class TimebackAssessmentsService {
|
|
|
129238
129715
|
return assessments2.toSorted((a, b) => a.title.localeCompare(b.title));
|
|
129239
129716
|
}
|
|
129240
129717
|
async createAssessment(integrationId, input) {
|
|
129241
|
-
if (input.purpose === "diagnostic") {
|
|
129242
|
-
throw new ValidationError("Adaptive diagnostics must be created by importing assessment JSON with a routing sidecar.");
|
|
129718
|
+
if (input.purpose === "diagnostic" || input.purpose === "review") {
|
|
129719
|
+
throw new ValidationError(input.purpose === "review" ? "The review bank is system-managed. Use Sync Review Bank instead." : "Adaptive diagnostics must be created by importing assessment JSON with a routing sidecar.");
|
|
129720
|
+
}
|
|
129721
|
+
if (input.assessmentKey === reviewBankSinkIdentifier(integrationId)) {
|
|
129722
|
+
throw new ValidationError("This assessment key is reserved for the review bank.");
|
|
129243
129723
|
}
|
|
129244
129724
|
const client2 = this.requireClient();
|
|
129245
129725
|
const ownership = await this.requireQtiTestOwnershipContext(integrationId);
|
|
@@ -129315,24 +129795,41 @@ class TimebackAssessmentsService {
|
|
|
129315
129795
|
index: index2,
|
|
129316
129796
|
standard: this.requirePurposeStandard(assessment.purpose, assessment.standard)
|
|
129317
129797
|
}));
|
|
129318
|
-
const
|
|
129319
|
-
|
|
129320
|
-
|
|
129321
|
-
|
|
129322
|
-
const existingByIdentifier = new Map(existingRows.map((row) => [row.qtiTestIdentifier, row]));
|
|
129798
|
+
const sinkIdentifier = reviewBankSinkIdentifier(integrationId);
|
|
129799
|
+
if (entries.some((entry2) => entry2.assessmentKey === sinkIdentifier || entry2.qtiTestIdentifier === sinkIdentifier || entry2.purpose === "review")) {
|
|
129800
|
+
throw new ValidationError("The review bank is system-managed and cannot be attached manually.");
|
|
129801
|
+
}
|
|
129323
129802
|
const validated = await this.validateAssessmentImportEntries(client2, entries, manifest.targetStatus);
|
|
129324
129803
|
const results = [];
|
|
129325
|
-
const
|
|
129326
|
-
|
|
129327
|
-
existingByKey,
|
|
129328
|
-
existingByIdentifier,
|
|
129329
|
-
|
|
129330
|
-
|
|
129331
|
-
|
|
129332
|
-
|
|
129333
|
-
|
|
129334
|
-
|
|
129335
|
-
|
|
129804
|
+
const affectsReviewBank = assessmentImportsAffectReviewBank(manifest.targetStatus, entries);
|
|
129805
|
+
const planAndInsert = async (db2, existingRows) => {
|
|
129806
|
+
const existingByKey = new Map(existingRows.flatMap((row) => row.assessmentKey ? [[row.assessmentKey, row]] : []));
|
|
129807
|
+
const existingByIdentifier = new Map(existingRows.map((row) => [row.qtiTestIdentifier, row]));
|
|
129808
|
+
const pendingInserts = this.planAssessmentImports({
|
|
129809
|
+
validated,
|
|
129810
|
+
existingByKey,
|
|
129811
|
+
existingByIdentifier,
|
|
129812
|
+
gameSlug,
|
|
129813
|
+
results
|
|
129814
|
+
});
|
|
129815
|
+
await this.insertImportedAssessments({
|
|
129816
|
+
db: db2,
|
|
129817
|
+
integrationId,
|
|
129818
|
+
pending: pendingInserts,
|
|
129819
|
+
targetStatus: manifest.targetStatus,
|
|
129820
|
+
gameSlug,
|
|
129821
|
+
results,
|
|
129822
|
+
abortOnError: affectsReviewBank
|
|
129823
|
+
});
|
|
129824
|
+
};
|
|
129825
|
+
if (affectsReviewBank) {
|
|
129826
|
+
await this.withLockedIntegrationAssessmentRows(integrationId, async (tx, lockedAssociations) => planAndInsert(tx, lockedAssociations));
|
|
129827
|
+
} else {
|
|
129828
|
+
const existingRows = await this.deps.db.query.gameTimebackAssessmentTests.findMany({
|
|
129829
|
+
where: eq(gameTimebackAssessmentTests.integrationId, integrationId)
|
|
129830
|
+
});
|
|
129831
|
+
await planAndInsert(this.deps.db, existingRows);
|
|
129832
|
+
}
|
|
129336
129833
|
setAttribute("app.assessment.operation", "bulk_attach_existing");
|
|
129337
129834
|
setAttribute("app.assessment.bulk_attach.target_status", manifest.targetStatus);
|
|
129338
129835
|
setAttribute("app.assessment.bulk_attach.created", results.filter((result) => result.status === "created").length);
|
|
@@ -129345,11 +129842,6 @@ class TimebackAssessmentsService {
|
|
|
129345
129842
|
assertManagedAssessmentIdentity(loaded.test, entry2.assessmentKey);
|
|
129346
129843
|
assertAssessmentHasQuestions(loaded.questions.questions);
|
|
129347
129844
|
assertPlayableAssessmentImportQuestions(targetStatus, loaded.questions.questions.map(({ question }) => question));
|
|
129348
|
-
if (entry2.purpose === "review") {
|
|
129349
|
-
assertReviewAssessmentHasStandards(loaded.questions.questions.map(({ question }) => reviewRoutingStandardRefsFromMetadata(question.metadata, {
|
|
129350
|
-
customOnly: true
|
|
129351
|
-
}).length));
|
|
129352
|
-
}
|
|
129353
129845
|
return { entry: entry2, test: loaded.test };
|
|
129354
129846
|
} catch (error88) {
|
|
129355
129847
|
return { entry: entry2, error: error88 };
|
|
@@ -129363,7 +129855,6 @@ class TimebackAssessmentsService {
|
|
|
129363
129855
|
...test ? { title: test.title } : {},
|
|
129364
129856
|
purpose: entry2.purpose,
|
|
129365
129857
|
...entry2.standard ? { standard: entry2.standard } : {},
|
|
129366
|
-
sourceSortOrder: entry2.sortOrder,
|
|
129367
129858
|
...test && gameSlug ? { editable: isQtiTestOwnedByGame(test, gameSlug) } : {}
|
|
129368
129859
|
};
|
|
129369
129860
|
}
|
|
@@ -129395,25 +129886,13 @@ class TimebackAssessmentsService {
|
|
|
129395
129886
|
}
|
|
129396
129887
|
return pendingInserts;
|
|
129397
129888
|
}
|
|
129398
|
-
|
|
129399
|
-
for (const { entry: entry2, test } of pending) {
|
|
129400
|
-
const message = failures.get(entry2.qtiTestIdentifier);
|
|
129401
|
-
if (message) {
|
|
129402
|
-
results[entry2.index] = {
|
|
129403
|
-
...this.associationImportResultBase(entry2, test, gameSlug),
|
|
129404
|
-
status: "failed",
|
|
129405
|
-
message
|
|
129406
|
-
};
|
|
129407
|
-
}
|
|
129408
|
-
}
|
|
129409
|
-
}
|
|
129410
|
-
async concurrentAssessmentImportDecision(integrationId, entry2) {
|
|
129889
|
+
async concurrentAssessmentImportDecision(db2, integrationId, entry2) {
|
|
129411
129890
|
try {
|
|
129412
129891
|
const [byKey, byIdentifier] = await Promise.all([
|
|
129413
|
-
|
|
129892
|
+
db2.query.gameTimebackAssessmentTests.findFirst({
|
|
129414
129893
|
where: and(eq(gameTimebackAssessmentTests.integrationId, integrationId), eq(gameTimebackAssessmentTests.assessmentKey, entry2.assessmentKey))
|
|
129415
129894
|
}),
|
|
129416
|
-
|
|
129895
|
+
db2.query.gameTimebackAssessmentTests.findFirst({
|
|
129417
129896
|
where: and(eq(gameTimebackAssessmentTests.integrationId, integrationId), eq(gameTimebackAssessmentTests.qtiTestIdentifier, entry2.qtiTestIdentifier))
|
|
129418
129897
|
})
|
|
129419
129898
|
]);
|
|
@@ -129425,39 +129904,52 @@ class TimebackAssessmentsService {
|
|
|
129425
129904
|
return null;
|
|
129426
129905
|
}
|
|
129427
129906
|
}
|
|
129428
|
-
async insertImportedAssessments(
|
|
129429
|
-
for (const { entry: entry2, test } of pending) {
|
|
129430
|
-
const base = this.associationImportResultBase(entry2, test, gameSlug);
|
|
129907
|
+
async insertImportedAssessments(input) {
|
|
129908
|
+
for (const { entry: entry2, test } of input.pending) {
|
|
129909
|
+
const base = this.associationImportResultBase(entry2, test, input.gameSlug);
|
|
129431
129910
|
try {
|
|
129432
|
-
const [row] = await
|
|
129433
|
-
integrationId,
|
|
129911
|
+
const [row] = await input.db.insert(gameTimebackAssessmentTests).values({
|
|
129912
|
+
integrationId: input.integrationId,
|
|
129434
129913
|
assessmentKey: entry2.assessmentKey,
|
|
129435
129914
|
qtiTestIdentifier: entry2.qtiTestIdentifier,
|
|
129436
129915
|
purpose: entry2.purpose,
|
|
129437
|
-
status: targetStatus,
|
|
129438
|
-
sortOrder: entry2.sortOrder,
|
|
129916
|
+
status: input.targetStatus,
|
|
129439
129917
|
standardFramework: entry2.standard?.framework,
|
|
129440
129918
|
standardIdentifier: entry2.standard?.identifier
|
|
129441
|
-
}).returning();
|
|
129919
|
+
}).onConflictDoNothing().returning();
|
|
129442
129920
|
if (!row) {
|
|
129443
|
-
|
|
129921
|
+
const concurrent = await this.concurrentAssessmentImportDecision(input.db, input.integrationId, entry2);
|
|
129922
|
+
if (concurrent?.decision.kind === "skip" || concurrent?.decision.kind === "fail") {
|
|
129923
|
+
input.results[entry2.index] = {
|
|
129924
|
+
...base,
|
|
129925
|
+
status: concurrent.decision.status,
|
|
129926
|
+
...concurrent.byKey ? { association: this.associationImportSummary(concurrent.byKey) } : {},
|
|
129927
|
+
message: concurrent.decision.message
|
|
129928
|
+
};
|
|
129929
|
+
} else {
|
|
129930
|
+
throw new Error("Assessment association insert conflicted");
|
|
129931
|
+
}
|
|
129932
|
+
} else {
|
|
129933
|
+
input.results[entry2.index] = {
|
|
129934
|
+
...base,
|
|
129935
|
+
status: "created",
|
|
129936
|
+
association: this.associationImportSummary(row),
|
|
129937
|
+
message: attachedAssessmentImportMessage(input.targetStatus, base.editable ?? false)
|
|
129938
|
+
};
|
|
129444
129939
|
}
|
|
129445
|
-
results[entry2.index] = {
|
|
129446
|
-
...base,
|
|
129447
|
-
status: "created",
|
|
129448
|
-
association: this.associationImportSummary(row),
|
|
129449
|
-
message: attachedAssessmentImportMessage(targetStatus, base.editable ?? false)
|
|
129450
|
-
};
|
|
129451
129940
|
} catch (error88) {
|
|
129452
|
-
|
|
129941
|
+
if (input.abortOnError) {
|
|
129942
|
+
throw error88;
|
|
129943
|
+
}
|
|
129944
|
+
input.results[entry2.index] = {
|
|
129453
129945
|
...base,
|
|
129454
129946
|
status: "failed",
|
|
129455
129947
|
message: `Association attach failed: ${errorMessage2(error88)}`
|
|
129456
129948
|
};
|
|
129457
129949
|
if (isUniqueViolation(error88)) {
|
|
129458
|
-
const concurrent = await this.concurrentAssessmentImportDecision(integrationId, entry2);
|
|
129950
|
+
const concurrent = await this.concurrentAssessmentImportDecision(input.db, input.integrationId, entry2);
|
|
129459
129951
|
if (concurrent?.decision.kind === "skip" || concurrent?.decision.kind === "fail") {
|
|
129460
|
-
results[entry2.index] = {
|
|
129952
|
+
input.results[entry2.index] = {
|
|
129461
129953
|
...base,
|
|
129462
129954
|
status: concurrent.decision.status,
|
|
129463
129955
|
...concurrent.byKey ? { association: this.associationImportSummary(concurrent.byKey) } : {},
|
|
@@ -129471,6 +129963,12 @@ class TimebackAssessmentsService {
|
|
|
129471
129963
|
async updateAssessment(integrationId, qtiTestIdentifier, input) {
|
|
129472
129964
|
try {
|
|
129473
129965
|
return await this.withLockedAssessmentAssociations(integrationId, qtiTestIdentifier, async (row, tx, associations) => {
|
|
129966
|
+
if (row.purpose === "review" || qtiTestIdentifier === reviewBankSinkIdentifier(integrationId)) {
|
|
129967
|
+
throw new ValidationError("The review bank is system-managed and cannot be edited.");
|
|
129968
|
+
}
|
|
129969
|
+
if (input.purpose === "review") {
|
|
129970
|
+
throw new ValidationError("Assessment purpose cannot be changed to the system-managed review bank.");
|
|
129971
|
+
}
|
|
129474
129972
|
const nextPurpose = input.purpose ?? row.purpose;
|
|
129475
129973
|
assertPurposeChangeDraft(row, nextPurpose);
|
|
129476
129974
|
const requestedStandard = input.standard ? this.canonicalAssessmentStandard(input.standard) : undefined;
|
|
@@ -129489,19 +129987,10 @@ class TimebackAssessmentsService {
|
|
|
129489
129987
|
...associationUpdates,
|
|
129490
129988
|
...diagnosticChanges.updates
|
|
129491
129989
|
};
|
|
129492
|
-
const nextStatus = input.status ?? row.status;
|
|
129493
129990
|
const publishing = input.status !== undefined && isAssessmentPublicationTransition(row.status, input.status);
|
|
129494
|
-
const activatingReview = nextPurpose === "review" && nextStatus === "live" && (row.purpose !== "review" || row.status !== "live");
|
|
129495
129991
|
if (input.status !== undefined) {
|
|
129496
129992
|
validateAssessmentStatusTransition(row.status, input.status);
|
|
129497
129993
|
}
|
|
129498
|
-
if (!publishing && !activatingReview && nextPurpose === "review" && row.purpose !== "review") {
|
|
129499
|
-
const ownership = await this.requireQtiTestOwnershipContext(integrationId, tx);
|
|
129500
|
-
await this.rebuildReviewMapping(this.requireClient(), qtiTestIdentifier, {
|
|
129501
|
-
kind: "owned",
|
|
129502
|
-
gameSlug: ownership.gameSlug
|
|
129503
|
-
});
|
|
129504
|
-
}
|
|
129505
129994
|
if (input.title !== undefined) {
|
|
129506
129995
|
assertDraftAssessment(row);
|
|
129507
129996
|
assertAllAssessmentAssociationsDraft(associations);
|
|
@@ -129511,7 +130000,7 @@ class TimebackAssessmentsService {
|
|
|
129511
130000
|
assertQtiTestOwnedByGame(test, ownership.gameSlug);
|
|
129512
130001
|
await client2.qtiApi.assessmentTests.update(qtiTestIdentifier, buildQtiTestUpdateInput(test, input.title));
|
|
129513
130002
|
}
|
|
129514
|
-
if (publishing
|
|
130003
|
+
if (publishing) {
|
|
129515
130004
|
if (nextPurpose === "diagnostic" && !nextDiagnostic) {
|
|
129516
130005
|
throw new ValidationError("Platform-routed diagnostics require a diagnostic key and routing manifest before publication.");
|
|
129517
130006
|
}
|
|
@@ -129521,8 +130010,7 @@ class TimebackAssessmentsService {
|
|
|
129521
130010
|
updates.diagnosticKey = nextDiagnostic.diagnosticKey;
|
|
129522
130011
|
updates.diagnosticRoutingManifest = nextDiagnostic.routingManifest;
|
|
129523
130012
|
} else {
|
|
129524
|
-
|
|
129525
|
-
await this.validateAssessmentHasQuestions(loaded, reviewOwnership?.gameSlug);
|
|
130013
|
+
await this.validateAssessmentHasQuestions(loaded);
|
|
129526
130014
|
}
|
|
129527
130015
|
if (publishing) {
|
|
129528
130016
|
const publication = await this.publishManagedAssessment(row, nextPurpose, nextDiagnostic?.routingManifest ?? null, loaded);
|
|
@@ -129545,20 +130033,20 @@ class TimebackAssessmentsService {
|
|
|
129545
130033
|
if (databaseConstraintName(error88) === "game_timeback_assessment_tests_one_live_diagnostic_key_idx") {
|
|
129546
130034
|
throw new ValidationError("Only one live revision of a diagnostic key is allowed. Archive the current revision before publishing another.");
|
|
129547
130035
|
}
|
|
129548
|
-
if (databaseConstraintName(error88) === "game_timeback_assessment_tests_one_live_review_idx") {
|
|
129549
|
-
throw new ValidationError("Only one live review assessment is allowed. Archive the current review bank before publishing another.");
|
|
129550
|
-
}
|
|
129551
130036
|
}
|
|
129552
130037
|
throw error88;
|
|
129553
130038
|
}
|
|
129554
130039
|
}
|
|
129555
130040
|
async removeAssessment(integrationId, qtiTestIdentifier) {
|
|
129556
130041
|
return this.withLockedAssessmentAssociations(integrationId, qtiTestIdentifier, async (row, tx) => {
|
|
130042
|
+
if (qtiTestIdentifier === reviewBankSinkIdentifier(integrationId)) {
|
|
130043
|
+
throw new ValidationError("The review bank is permanent and cannot be removed or archived.");
|
|
130044
|
+
}
|
|
129557
130045
|
const plan = planAssessmentRemoval(row.status);
|
|
129558
130046
|
if (plan.kind === "delete") {
|
|
129559
130047
|
await tx.delete(gameTimebackAssessmentTests).where(eq(gameTimebackAssessmentTests.id, row.id));
|
|
129560
130048
|
} else if (plan.kind === "archive") {
|
|
129561
|
-
await tx.update(gameTimebackAssessmentTests).set({ status: "archived",
|
|
130049
|
+
await tx.update(gameTimebackAssessmentTests).set({ status: "archived", updatedAt: new Date }).where(eq(gameTimebackAssessmentTests.id, row.id));
|
|
129562
130050
|
}
|
|
129563
130051
|
if (plan.kind !== "none") {
|
|
129564
130052
|
setAttribute("app.assessment.operation", plan.operation);
|
|
@@ -129566,24 +130054,6 @@ class TimebackAssessmentsService {
|
|
|
129566
130054
|
return { action: plan.action };
|
|
129567
130055
|
});
|
|
129568
130056
|
}
|
|
129569
|
-
async reorderAssessments(integrationId, purpose, testIdentifiers) {
|
|
129570
|
-
await this.requireIntegration(integrationId);
|
|
129571
|
-
validateUniqueAssessmentIdentifiers(testIdentifiers);
|
|
129572
|
-
const updatedAt = new Date;
|
|
129573
|
-
await this.deps.db.transaction(async (tx) => {
|
|
129574
|
-
const liveRows = await tx.query.gameTimebackAssessmentTests.findMany({
|
|
129575
|
-
where: and(eq(gameTimebackAssessmentTests.integrationId, integrationId), eq(gameTimebackAssessmentTests.purpose, purpose), eq(gameTimebackAssessmentTests.status, "live"))
|
|
129576
|
-
});
|
|
129577
|
-
const orderedRows = orderLiveAssessmentRows(liveRows, purpose, testIdentifiers);
|
|
129578
|
-
const positionByRowId = new Map(orderedRows.map((row, index2) => [row.id, index2 + 1]));
|
|
129579
|
-
for (const row of lockOrderAssessmentRows(orderedRows)) {
|
|
129580
|
-
const testIdentifier = row.qtiTestIdentifier;
|
|
129581
|
-
const [updatedRow] = await tx.update(gameTimebackAssessmentTests).set({ sortOrder: positionByRowId.get(row.id), updatedAt }).where(and(eq(gameTimebackAssessmentTests.id, row.id), eq(gameTimebackAssessmentTests.integrationId, integrationId), eq(gameTimebackAssessmentTests.qtiTestIdentifier, testIdentifier), eq(gameTimebackAssessmentTests.purpose, purpose), eq(gameTimebackAssessmentTests.status, "live"))).returning({ id: gameTimebackAssessmentTests.id });
|
|
129582
|
-
assertAssessmentOrderUpdateSucceeded(updatedRow);
|
|
129583
|
-
}
|
|
129584
|
-
});
|
|
129585
|
-
setAttribute("app.assessment.operation", "reorder_live_assessments");
|
|
129586
|
-
}
|
|
129587
130057
|
async listQuestions(integrationId, qtiTestIdentifier) {
|
|
129588
130058
|
const client2 = this.requireClient();
|
|
129589
130059
|
await this.requireAssessmentRow(integrationId, qtiTestIdentifier);
|
|
@@ -129624,19 +130094,96 @@ class TimebackAssessmentsService {
|
|
|
129624
130094
|
});
|
|
129625
130095
|
return { ...result, questions };
|
|
129626
130096
|
}
|
|
129627
|
-
async
|
|
129628
|
-
|
|
129629
|
-
|
|
129630
|
-
|
|
130097
|
+
async synchronizeReviewBank(integrationId) {
|
|
130098
|
+
const client2 = this.requireClient();
|
|
130099
|
+
await this.requireIntegration(integrationId);
|
|
130100
|
+
const sinkIdentifier = reviewBankSinkIdentifier(integrationId);
|
|
130101
|
+
for (let attempt = 0;attempt < REVIEW_BANK_SYNC_ATTEMPTS; attempt++) {
|
|
130102
|
+
const associations = await this.deps.db.query.gameTimebackAssessmentTests.findMany({
|
|
130103
|
+
where: eq(gameTimebackAssessmentTests.integrationId, integrationId)
|
|
130104
|
+
});
|
|
130105
|
+
const masterySources = this.reviewBankMasterySources(associations);
|
|
130106
|
+
const contributorFingerprint = await reviewBankContributorFingerprint(masterySources);
|
|
130107
|
+
const existingSink = await this.findReviewBankSink(client2, integrationId);
|
|
130108
|
+
if (existingSink) {
|
|
130109
|
+
assertReviewBankSinkOwnership(existingSink, integrationId);
|
|
130110
|
+
}
|
|
130111
|
+
let unchangedResult = null;
|
|
130112
|
+
if (existingSink) {
|
|
130113
|
+
try {
|
|
130114
|
+
unchangedResult = await unchangedReviewBankSynchronizationResult({
|
|
130115
|
+
sink: existingSink,
|
|
130116
|
+
integrationId,
|
|
130117
|
+
contributorFingerprint,
|
|
130118
|
+
contributorCount: masterySources.length
|
|
130119
|
+
});
|
|
130120
|
+
} catch (error88) {
|
|
130121
|
+
addEvent("assessment.review_mapping_noop_validation_failed", {
|
|
130122
|
+
"app.assessment.qti_test_identifier": existingSink.identifier,
|
|
130123
|
+
"exception.type": errorType(error88),
|
|
130124
|
+
"app.error.message": errorMessage2(error88)
|
|
130125
|
+
});
|
|
130126
|
+
}
|
|
129631
130127
|
}
|
|
129632
|
-
|
|
129633
|
-
|
|
129634
|
-
|
|
130128
|
+
let plan = null;
|
|
130129
|
+
let addedItemCount = 0;
|
|
130130
|
+
let removedItemCount = 0;
|
|
130131
|
+
let catalogUnchanged = unchangedResult !== null;
|
|
130132
|
+
if (!unchangedResult) {
|
|
130133
|
+
const contributors = await runWithConcurrency(masterySources, QTI_HYDRATION_CONCURRENCY, async (source) => {
|
|
130134
|
+
const test = await client2.qtiApi.assessmentTests.get(source.qtiTestIdentifier);
|
|
130135
|
+
assertManagedAssessmentIdentity(test, source.assessmentKey);
|
|
130136
|
+
return { ...source, test };
|
|
130137
|
+
});
|
|
130138
|
+
plan = await prepareReviewBankSynchronization({
|
|
130139
|
+
bankIdentifier: sinkIdentifier,
|
|
130140
|
+
contributors,
|
|
130141
|
+
contributorFingerprint
|
|
130142
|
+
});
|
|
130143
|
+
const diff = reviewBankSynchronizationDiff({ existingSink, plan });
|
|
130144
|
+
addedItemCount = diff.addedItemCount;
|
|
130145
|
+
removedItemCount = diff.removedItemCount;
|
|
130146
|
+
catalogUnchanged = diff.unchanged;
|
|
130147
|
+
}
|
|
130148
|
+
const committed = await this.withLockedIntegrationAssessmentRows(integrationId, async (tx, lockedAssociations) => {
|
|
130149
|
+
const lockedSources = this.reviewBankMasterySources(lockedAssociations);
|
|
130150
|
+
const lockedFingerprint = await reviewBankContributorFingerprint(lockedSources);
|
|
130151
|
+
if (lockedFingerprint !== contributorFingerprint) {
|
|
130152
|
+
return null;
|
|
130153
|
+
}
|
|
130154
|
+
const associationChanged = await this.installReviewBankAssociation({
|
|
130155
|
+
tx,
|
|
130156
|
+
integrationId,
|
|
130157
|
+
associations: lockedAssociations,
|
|
130158
|
+
sinkIdentifier,
|
|
130159
|
+
beforeInstall: async () => {
|
|
130160
|
+
if (!catalogUnchanged) {
|
|
130161
|
+
await this.writeReviewBankSink({
|
|
130162
|
+
client: client2,
|
|
130163
|
+
integrationId,
|
|
130164
|
+
existingSink,
|
|
130165
|
+
plan,
|
|
130166
|
+
metadata: reviewBankSinkMetadata(integrationId, plan)
|
|
130167
|
+
});
|
|
130168
|
+
}
|
|
130169
|
+
}
|
|
130170
|
+
});
|
|
130171
|
+
const unchanged = catalogUnchanged && !associationChanged;
|
|
130172
|
+
const result = unchangedResult ? { ...unchangedResult, unchanged } : {
|
|
130173
|
+
qtiTestIdentifier: sinkIdentifier,
|
|
130174
|
+
...plan.summary,
|
|
130175
|
+
addedItemCount,
|
|
130176
|
+
removedItemCount,
|
|
130177
|
+
unchanged,
|
|
130178
|
+
warnings: plan.warnings
|
|
130179
|
+
};
|
|
130180
|
+
return this.reviewMappingResult(result);
|
|
129635
130181
|
});
|
|
129636
|
-
|
|
129637
|
-
|
|
129638
|
-
|
|
129639
|
-
}
|
|
130182
|
+
if (committed) {
|
|
130183
|
+
return committed;
|
|
130184
|
+
}
|
|
130185
|
+
}
|
|
130186
|
+
throw new ValidationError("Mastery assessments changed while the review bank was synchronizing. Try again.");
|
|
129640
130187
|
}
|
|
129641
130188
|
async listQuestionLibrary(integrationId, params) {
|
|
129642
130189
|
const client2 = this.requireClient();
|
|
@@ -129656,15 +130203,18 @@ class TimebackAssessmentsService {
|
|
|
129656
130203
|
purpose,
|
|
129657
130204
|
standard: standardInput
|
|
129658
130205
|
} = input;
|
|
129659
|
-
if (purpose === "diagnostic") {
|
|
129660
|
-
throw new ValidationError("Adaptive diagnostics must be created by importing assessment JSON with a routing sidecar.");
|
|
130206
|
+
if (purpose === "diagnostic" || purpose === "review") {
|
|
130207
|
+
throw new ValidationError(purpose === "review" ? "The review bank is system-managed. Use Sync Review Bank instead." : "Adaptive diagnostics must be created by importing assessment JSON with a routing sidecar.");
|
|
130208
|
+
}
|
|
130209
|
+
if (assessmentKey === reviewBankSinkIdentifier(integrationId) || targetTestIdentifier === reviewBankSinkIdentifier(integrationId)) {
|
|
130210
|
+
throw new ValidationError("This assessment identity is reserved for the review bank.");
|
|
129661
130211
|
}
|
|
129662
130212
|
const client2 = this.requireClient();
|
|
129663
130213
|
const ownership = await this.requireQtiTestOwnershipContext(integrationId);
|
|
129664
130214
|
const { integration } = ownership;
|
|
129665
130215
|
const standard = this.requirePurposeStandard(purpose, standardInput);
|
|
129666
130216
|
const source = await client2.qtiApi.assessmentTests.get(sourceTestIdentifier);
|
|
129667
|
-
const itemPlan = buildQtiAssessmentItemCopyPlan(source, targetTestIdentifier, (identifier) =>
|
|
130217
|
+
const itemPlan = buildQtiAssessmentItemCopyPlan(source, targetTestIdentifier, (identifier) => qtiItemHref(client2, identifier));
|
|
129668
130218
|
const itemCopies = await runWithConcurrency(itemPlan, QTI_HYDRATION_CONCURRENCY, async (plan) => {
|
|
129669
130219
|
const sourceItem = await client2.qtiApi.assessmentItems.get(plan.sourceIdentifier);
|
|
129670
130220
|
return {
|
|
@@ -129789,7 +130339,7 @@ class TimebackAssessmentsService {
|
|
|
129789
130339
|
};
|
|
129790
130340
|
}
|
|
129791
130341
|
const section = await this.qtiSectionItems(client2, qtiTestIdentifier, undefined, undefined, ownership.test);
|
|
129792
|
-
const href =
|
|
130342
|
+
const href = qtiItemHref(client2, itemIdentifier);
|
|
129793
130343
|
let item;
|
|
129794
130344
|
let itemCreationAttempted = false;
|
|
129795
130345
|
let referenceCreationAttempted = false;
|
|
@@ -129899,7 +130449,7 @@ class TimebackAssessmentsService {
|
|
|
129899
130449
|
const result = await section.items.reorder({
|
|
129900
130450
|
items: items.map((item) => ({
|
|
129901
130451
|
identifier: item.identifier,
|
|
129902
|
-
href: item.href ??
|
|
130452
|
+
href: item.href ?? qtiItemHref(client2, item.identifier),
|
|
129903
130453
|
sequence: item.sequence
|
|
129904
130454
|
}))
|
|
129905
130455
|
});
|
|
@@ -129913,6 +130463,118 @@ class TimebackAssessmentsService {
|
|
|
129913
130463
|
}
|
|
129914
130464
|
return this.deps.timeback;
|
|
129915
130465
|
}
|
|
130466
|
+
reviewBankMasterySources(associations) {
|
|
130467
|
+
return associations.filter((association) => association.purpose === "mastery" && association.status === "live").map((association) => {
|
|
130468
|
+
const standard = assessmentStandardForRow(association);
|
|
130469
|
+
if (!association.assessmentKey || !standard) {
|
|
130470
|
+
throw new ValidationError(`Live mastery assessment ${association.qtiTestIdentifier} requires a managed identity and standard`);
|
|
130471
|
+
}
|
|
130472
|
+
if (assessmentKeyFromManagedQtiIdentifier(association.qtiTestIdentifier) !== association.assessmentKey) {
|
|
130473
|
+
throw new ValidationError(`Live mastery assessment ${association.qtiTestIdentifier} is not a managed publication for ${association.assessmentKey}`);
|
|
130474
|
+
}
|
|
130475
|
+
return {
|
|
130476
|
+
assessmentKey: association.assessmentKey,
|
|
130477
|
+
qtiTestIdentifier: association.qtiTestIdentifier,
|
|
130478
|
+
standard
|
|
130479
|
+
};
|
|
130480
|
+
});
|
|
130481
|
+
}
|
|
130482
|
+
async writeReviewBankSink(input) {
|
|
130483
|
+
function sinkInput(title, includeIdentifier = false) {
|
|
130484
|
+
return buildReviewBankSinkInput({
|
|
130485
|
+
identifier: input.plan.bankIdentifier,
|
|
130486
|
+
title,
|
|
130487
|
+
metadata: input.metadata,
|
|
130488
|
+
itemIdentifiers: input.plan.itemIdentifiers,
|
|
130489
|
+
itemHref: (identifier) => qtiItemHref(input.client, identifier),
|
|
130490
|
+
includeIdentifier
|
|
130491
|
+
});
|
|
130492
|
+
}
|
|
130493
|
+
if (input.existingSink) {
|
|
130494
|
+
await input.client.qtiApi.assessmentTests.update(input.plan.bankIdentifier, sinkInput(input.existingSink.title));
|
|
130495
|
+
return;
|
|
130496
|
+
}
|
|
130497
|
+
try {
|
|
130498
|
+
const createInput = sinkInput(REVIEW_BANK_TITLE, true);
|
|
130499
|
+
await input.client.qtiApi.assessmentTests.create(createInput);
|
|
130500
|
+
} catch (error88) {
|
|
130501
|
+
if (!isApiError(error88) || error88.statusCode !== 409) {
|
|
130502
|
+
throw error88;
|
|
130503
|
+
}
|
|
130504
|
+
const concurrentlyCreated = await input.client.qtiApi.assessmentTests.get(input.plan.bankIdentifier);
|
|
130505
|
+
assertReviewBankSinkOwnership(concurrentlyCreated, input.integrationId);
|
|
130506
|
+
await input.client.qtiApi.assessmentTests.update(input.plan.bankIdentifier, sinkInput(concurrentlyCreated.title));
|
|
130507
|
+
}
|
|
130508
|
+
}
|
|
130509
|
+
async installReviewBankAssociation(input) {
|
|
130510
|
+
const singleton = input.associations.find((association) => association.qtiTestIdentifier === input.sinkIdentifier);
|
|
130511
|
+
if (singleton && singleton.purpose !== "review") {
|
|
130512
|
+
throw new ValidationError("The reserved review-bank identifier is associated with another assessment purpose.");
|
|
130513
|
+
}
|
|
130514
|
+
const legacyReviews = input.associations.filter((association) => association.purpose === "review" && association.qtiTestIdentifier !== input.sinkIdentifier);
|
|
130515
|
+
const reservedKeyOwner = input.associations.find((association) => association.assessmentKey === input.sinkIdentifier && association.qtiTestIdentifier !== input.sinkIdentifier);
|
|
130516
|
+
if (reservedKeyOwner && reservedKeyOwner.purpose !== "review") {
|
|
130517
|
+
throw new ValidationError("The reserved review-bank assessment key is already in use.");
|
|
130518
|
+
}
|
|
130519
|
+
const singletonCurrent = Boolean(singleton && singleton.assessmentKey === input.sinkIdentifier && singleton.status === "live" && singleton.standardFramework === null && singleton.standardIdentifier === null && singleton.diagnosticKey === null && singleton.diagnosticRoutingManifest === null);
|
|
130520
|
+
const legacyCurrent = legacyReviews.every((association) => association.status === "archived" && association.assessmentKey !== input.sinkIdentifier);
|
|
130521
|
+
const changed = !singletonCurrent || !legacyCurrent;
|
|
130522
|
+
await input.beforeInstall();
|
|
130523
|
+
for (const legacy of legacyReviews) {
|
|
130524
|
+
if (legacy.status !== "archived" || legacy.assessmentKey === input.sinkIdentifier) {
|
|
130525
|
+
await input.tx.update(gameTimebackAssessmentTests).set({
|
|
130526
|
+
status: "archived",
|
|
130527
|
+
...legacy.assessmentKey === input.sinkIdentifier ? { assessmentKey: null } : {},
|
|
130528
|
+
updatedAt: new Date
|
|
130529
|
+
}).where(eq(gameTimebackAssessmentTests.id, legacy.id));
|
|
130530
|
+
}
|
|
130531
|
+
}
|
|
130532
|
+
if (singletonCurrent) {
|
|
130533
|
+
return changed;
|
|
130534
|
+
}
|
|
130535
|
+
if (singleton) {
|
|
130536
|
+
await input.tx.update(gameTimebackAssessmentTests).set({
|
|
130537
|
+
assessmentKey: input.sinkIdentifier,
|
|
130538
|
+
purpose: "review",
|
|
130539
|
+
status: "live",
|
|
130540
|
+
standardFramework: null,
|
|
130541
|
+
standardIdentifier: null,
|
|
130542
|
+
diagnosticKey: null,
|
|
130543
|
+
diagnosticRoutingManifest: null,
|
|
130544
|
+
updatedAt: new Date
|
|
130545
|
+
}).where(eq(gameTimebackAssessmentTests.id, singleton.id));
|
|
130546
|
+
return changed;
|
|
130547
|
+
}
|
|
130548
|
+
await input.tx.insert(gameTimebackAssessmentTests).values({
|
|
130549
|
+
integrationId: input.integrationId,
|
|
130550
|
+
assessmentKey: input.sinkIdentifier,
|
|
130551
|
+
qtiTestIdentifier: input.sinkIdentifier,
|
|
130552
|
+
purpose: "review",
|
|
130553
|
+
status: "live",
|
|
130554
|
+
standardFramework: null,
|
|
130555
|
+
standardIdentifier: null,
|
|
130556
|
+
diagnosticKey: null,
|
|
130557
|
+
diagnosticRoutingManifest: null
|
|
130558
|
+
});
|
|
130559
|
+
return changed;
|
|
130560
|
+
}
|
|
130561
|
+
reviewMappingResult(result) {
|
|
130562
|
+
setAttribute("app.assessment.operation", "update_review_mapping");
|
|
130563
|
+
setAttribute("app.assessment.review_mapping_item_count", result.itemCount);
|
|
130564
|
+
setAttribute("app.assessment.review_mapping_added_item_count", result.addedItemCount);
|
|
130565
|
+
setAttribute("app.assessment.review_mapping_removed_item_count", result.removedItemCount);
|
|
130566
|
+
return result;
|
|
130567
|
+
}
|
|
130568
|
+
async withLockedIntegrationAssessmentRows(integrationId, action) {
|
|
130569
|
+
return this.deps.db.transaction(async (tx) => {
|
|
130570
|
+
const [integration] = await tx.select({ id: gameTimebackIntegrations.id }).from(gameTimebackIntegrations).where(and(eq(gameTimebackIntegrations.id, integrationId), isActiveGameTimebackIntegrationStatus())).for("update");
|
|
130571
|
+
if (!integration) {
|
|
130572
|
+
throw new NotFoundError(`Integration not found: ${integrationId}`);
|
|
130573
|
+
}
|
|
130574
|
+
const rows = await tx.select().from(gameTimebackAssessmentTests).where(eq(gameTimebackAssessmentTests.integrationId, integrationId)).orderBy(gameTimebackAssessmentTests.id).for("update");
|
|
130575
|
+
return action(tx, rows);
|
|
130576
|
+
});
|
|
130577
|
+
}
|
|
129916
130578
|
async withLockedAssessmentAssociations(integrationId, qtiTestIdentifier, action) {
|
|
129917
130579
|
return this.deps.db.transaction(async (tx) => {
|
|
129918
130580
|
const rows = await tx.select().from(gameTimebackAssessmentTests).where(eq(gameTimebackAssessmentTests.qtiTestIdentifier, qtiTestIdentifier)).orderBy(gameTimebackAssessmentTests.id).for("update");
|
|
@@ -129997,8 +130659,16 @@ class TimebackAssessmentsService {
|
|
|
129997
130659
|
items: client2.qtiApi.assessmentTests.testParts(qtiTestIdentifier).sections(section.partIdentifier).items(section.sectionIdentifier)
|
|
129998
130660
|
};
|
|
129999
130661
|
}
|
|
130000
|
-
|
|
130001
|
-
|
|
130662
|
+
async findReviewBankSink(client2, integrationId) {
|
|
130663
|
+
const identifier = reviewBankSinkIdentifier(integrationId);
|
|
130664
|
+
try {
|
|
130665
|
+
return await client2.qtiApi.assessmentTests.get(identifier);
|
|
130666
|
+
} catch (error88) {
|
|
130667
|
+
if (isApiError(error88) && error88.statusCode === 404) {
|
|
130668
|
+
return null;
|
|
130669
|
+
}
|
|
130670
|
+
throw error88;
|
|
130671
|
+
}
|
|
130002
130672
|
}
|
|
130003
130673
|
async ensureImmutableQtiResource(get, create) {
|
|
130004
130674
|
try {
|
|
@@ -130039,7 +130709,7 @@ class TimebackAssessmentsService {
|
|
|
130039
130709
|
test: loaded.test,
|
|
130040
130710
|
questions: loaded.questions,
|
|
130041
130711
|
diagnosticRoutingManifest,
|
|
130042
|
-
itemHref: (identifier) =>
|
|
130712
|
+
itemHref: (identifier) => qtiItemHref(client2, identifier)
|
|
130043
130713
|
});
|
|
130044
130714
|
await this.ensureManagedQtiPublication(client2, plan);
|
|
130045
130715
|
return plan;
|
|
@@ -130083,7 +130753,6 @@ class TimebackAssessmentsService {
|
|
|
130083
130753
|
qtiTestIdentifier: row.qtiTestIdentifier,
|
|
130084
130754
|
purpose: row.purpose,
|
|
130085
130755
|
status: row.status,
|
|
130086
|
-
sortOrder: row.sortOrder,
|
|
130087
130756
|
standard: assessmentStandardForRow(row),
|
|
130088
130757
|
diagnostic: diagnosticDefinitionForRow(row),
|
|
130089
130758
|
createdAt: row.createdAt,
|
|
@@ -130121,15 +130790,11 @@ class TimebackAssessmentsService {
|
|
|
130121
130790
|
const plan = buildQtiLibraryListPlan(params);
|
|
130122
130791
|
return list(plan.params);
|
|
130123
130792
|
}
|
|
130124
|
-
async validateAssessmentHasQuestions(loaded
|
|
130793
|
+
async validateAssessmentHasQuestions(loaded) {
|
|
130125
130794
|
assertAssessmentHasQuestions(loaded.questions.questions);
|
|
130126
130795
|
for (const { question } of loaded.questions.questions) {
|
|
130127
130796
|
assertPlayableQtiQuestion(question);
|
|
130128
130797
|
}
|
|
130129
|
-
if (reviewGameSlug) {
|
|
130130
|
-
assertQtiTestOwnedByGame(loaded.test, reviewGameSlug);
|
|
130131
|
-
await this.writeReviewMapping(this.requireClient(), loaded.test, loaded.questions);
|
|
130132
|
-
}
|
|
130133
130798
|
}
|
|
130134
130799
|
async prepareDiagnosticAssociationChanges(row, nextPurpose, requested) {
|
|
130135
130800
|
if (row.purpose !== "diagnostic" && nextPurpose === "diagnostic" && !requested) {
|
|
@@ -130158,22 +130823,9 @@ class TimebackAssessmentsService {
|
|
|
130158
130823
|
}
|
|
130159
130824
|
};
|
|
130160
130825
|
}
|
|
130161
|
-
async rebuildReviewMapping(client2, qtiTestIdentifier, scope) {
|
|
130162
|
-
const [test, references] = await Promise.all([
|
|
130163
|
-
client2.qtiApi.assessmentTests.get(qtiTestIdentifier),
|
|
130164
|
-
client2.qtiApi.assessmentTests.getQuestions(qtiTestIdentifier)
|
|
130165
|
-
]);
|
|
130166
|
-
if (scope.kind === "owned") {
|
|
130167
|
-
assertQtiTestOwnedByGame(test, scope.gameSlug);
|
|
130168
|
-
}
|
|
130169
|
-
return this.writeReviewMapping(client2, test, await hydrateQtiTestQuestions(client2, references));
|
|
130170
|
-
}
|
|
130171
|
-
async writeReviewMapping(client2, test, questions) {
|
|
130172
|
-
const prepared = await prepareReviewMappingUpdate(test, questions);
|
|
130173
|
-
await client2.qtiApi.assessmentTests.update(test.identifier, prepared.update);
|
|
130174
|
-
return prepared.summary;
|
|
130175
|
-
}
|
|
130176
130826
|
}
|
|
130827
|
+
var REVIEW_BANK_TITLE = "Review Bank";
|
|
130828
|
+
var REVIEW_BANK_SYNC_ATTEMPTS = 2;
|
|
130177
130829
|
var init_timeback_assessments_service = __esm(async () => {
|
|
130178
130830
|
init_drizzle_orm();
|
|
130179
130831
|
init_helpers_index();
|
|
@@ -192833,8 +193485,7 @@ var listAssessments;
|
|
|
192833
193485
|
var createAssessment;
|
|
192834
193486
|
var attachExistingAssessments;
|
|
192835
193487
|
var updateAssessment;
|
|
192836
|
-
var
|
|
192837
|
-
var reorderAssessments;
|
|
193488
|
+
var synchronizeReviewBank;
|
|
192838
193489
|
var reorderQuestions;
|
|
192839
193490
|
var removeAssessment;
|
|
192840
193491
|
var listQuestions;
|
|
@@ -193206,7 +193857,9 @@ var init_timeback_controller = __esm(() => {
|
|
|
193206
193857
|
attemptId,
|
|
193207
193858
|
input: {
|
|
193208
193859
|
submissionId: body2.submissionId,
|
|
193209
|
-
xpAwarded: body2.xpAwarded
|
|
193860
|
+
xpAwarded: body2.xpAwarded,
|
|
193861
|
+
...body2.masteredUnits !== undefined ? { masteredUnits: body2.masteredUnits } : {},
|
|
193862
|
+
...body2.masteredUnitsAbsolute !== undefined ? { masteredUnitsAbsolute: body2.masteredUnitsAbsolute } : {}
|
|
193210
193863
|
},
|
|
193211
193864
|
game: {
|
|
193212
193865
|
appName: body2.appName,
|
|
@@ -193540,23 +194193,13 @@ var init_timeback_controller = __esm(() => {
|
|
|
193540
194193
|
const body2 = await parseRequestBody(ctx.request, UpdateAssessmentRequestSchema);
|
|
193541
194194
|
return ctx.services.timebackAssessments.updateAssessment(integrationId, testIdentifier, body2);
|
|
193542
194195
|
});
|
|
193543
|
-
|
|
193544
|
-
const { gameId, courseId, testIdentifier } = ctx.params;
|
|
193545
|
-
if (!gameId || !courseId || !testIdentifier) {
|
|
193546
|
-
throw ApiError.badRequest("Missing gameId, courseId, or testIdentifier parameter");
|
|
193547
|
-
}
|
|
193548
|
-
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
193549
|
-
return ctx.services.timebackAssessments.updateReviewMapping(integrationId, testIdentifier);
|
|
193550
|
-
});
|
|
193551
|
-
reorderAssessments = requireDeveloper(async (ctx) => {
|
|
194196
|
+
synchronizeReviewBank = requireDeveloper(async (ctx) => {
|
|
193552
194197
|
const { gameId, courseId } = ctx.params;
|
|
193553
194198
|
if (!gameId || !courseId) {
|
|
193554
194199
|
throw ApiError.badRequest("Missing gameId or courseId parameter");
|
|
193555
194200
|
}
|
|
193556
|
-
const body2 = await parseRequestBody(ctx.request, ReorderAssessmentsRequestSchema);
|
|
193557
194201
|
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
193558
|
-
|
|
193559
|
-
return { success: true };
|
|
194202
|
+
return ctx.services.timebackAssessments.synchronizeReviewBank(integrationId);
|
|
193560
194203
|
});
|
|
193561
194204
|
reorderQuestions = requireDeveloper(async (ctx) => {
|
|
193562
194205
|
const { gameId, courseId, testIdentifier } = ctx.params;
|
|
@@ -193697,8 +194340,7 @@ var init_timeback_controller = __esm(() => {
|
|
|
193697
194340
|
createAssessment,
|
|
193698
194341
|
attachExistingAssessments,
|
|
193699
194342
|
updateAssessment,
|
|
193700
|
-
|
|
193701
|
-
reorderAssessments,
|
|
194343
|
+
synchronizeReviewBank,
|
|
193702
194344
|
removeAssessment,
|
|
193703
194345
|
reorderQuestions,
|
|
193704
194346
|
listQuestions,
|