@playcademy/sandbox 0.8.1-beta.1 → 0.8.1-beta.2
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1129 -405
- package/dist/server.js +1129 -405
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -1127,7 +1127,7 @@ var package_default;
|
|
|
1127
1127
|
var init_package = __esm(() => {
|
|
1128
1128
|
package_default = {
|
|
1129
1129
|
name: "@playcademy/sandbox",
|
|
1130
|
-
version: "0.8.1-beta.
|
|
1130
|
+
version: "0.8.1-beta.2",
|
|
1131
1131
|
description: "Local development server for Playcademy game development",
|
|
1132
1132
|
type: "module",
|
|
1133
1133
|
exports: {
|
|
@@ -1497,6 +1497,9 @@ async function runWithConcurrency(items, concurrency, worker) {
|
|
|
1497
1497
|
}
|
|
1498
1498
|
|
|
1499
1499
|
// ../utils/src/timeback.ts
|
|
1500
|
+
function reviewBankSinkIdentifier(integrationId) {
|
|
1501
|
+
return `playcademy-review-bank.${integrationId}`;
|
|
1502
|
+
}
|
|
1500
1503
|
function playcademySupportedQtiInteractionType(value) {
|
|
1501
1504
|
if (typeof value !== "string") {
|
|
1502
1505
|
return;
|
|
@@ -9480,9 +9483,12 @@ function classifyAssessmentSubmission(attempt, submissionId) {
|
|
|
9480
9483
|
}
|
|
9481
9484
|
return isAssessmentAttemptOpen(attempt) ? "submit" : "reject";
|
|
9482
9485
|
}
|
|
9486
|
+
function isSameAssessmentAward(recorded, input) {
|
|
9487
|
+
return recorded.xpAwarded === input.xpAwarded && recorded.masteredUnits === input.masteredUnits && recorded.masteredUnitsAbsolute === input.masteredUnitsAbsolute;
|
|
9488
|
+
}
|
|
9483
9489
|
function classifyAssessmentFinalization(attempt, input) {
|
|
9484
9490
|
if (isAssessmentAttemptCompleted(attempt)) {
|
|
9485
|
-
return attempt.submissionId === input.submissionId && attempt.
|
|
9491
|
+
return attempt.submissionId === input.submissionId && attempt.award !== undefined && isSameAssessmentAward(attempt.award, input) ? "replay" : "conflict";
|
|
9486
9492
|
}
|
|
9487
9493
|
if (!isAssessmentAttemptAwaitingAward(attempt) || attempt.submissionId !== input.submissionId) {
|
|
9488
9494
|
return "reject";
|
|
@@ -10963,7 +10969,7 @@ async function reviewBankRevision(input) {
|
|
|
10963
10969
|
})}`);
|
|
10964
10970
|
return `review-bank-v1:${bankRevisionUuid}`;
|
|
10965
10971
|
}
|
|
10966
|
-
async function
|
|
10972
|
+
async function buildReviewBankIndexFromReferences(input) {
|
|
10967
10973
|
return {
|
|
10968
10974
|
...input,
|
|
10969
10975
|
bankRevision: await reviewBankRevision(input)
|
|
@@ -10992,7 +10998,7 @@ async function buildReviewBankIndex(assessment, questions, options = {}) {
|
|
|
10992
10998
|
standards
|
|
10993
10999
|
};
|
|
10994
11000
|
});
|
|
10995
|
-
return
|
|
11001
|
+
return buildReviewBankIndexFromReferences({
|
|
10996
11002
|
bankIdentifier: assessment.identifier,
|
|
10997
11003
|
sourceContentRevision: assessment.contentRevision,
|
|
10998
11004
|
items
|
|
@@ -11097,7 +11103,7 @@ async function reviewBankIndexFromManifest(metadata2, input) {
|
|
|
11097
11103
|
}
|
|
11098
11104
|
}
|
|
11099
11105
|
}
|
|
11100
|
-
const index = await
|
|
11106
|
+
const index = await buildReviewBankIndexFromReferences({
|
|
11101
11107
|
bankIdentifier,
|
|
11102
11108
|
sourceContentRevision,
|
|
11103
11109
|
items: input.membershipItemIdentifiers.map((itemIdentifier) => ({
|
|
@@ -11452,6 +11458,18 @@ function isRoutedDiagnosticAttemptMetadata(value, itemSubmissions, responseVersi
|
|
|
11452
11458
|
function isReviewAttemptMetadata(value) {
|
|
11453
11459
|
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);
|
|
11454
11460
|
}
|
|
11461
|
+
function isOptionalInteger(value) {
|
|
11462
|
+
return value === undefined || Number.isInteger(value);
|
|
11463
|
+
}
|
|
11464
|
+
function isMasteryWriteWarning(value) {
|
|
11465
|
+
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);
|
|
11466
|
+
}
|
|
11467
|
+
function isAssessmentAwardRecord(value) {
|
|
11468
|
+
if (value === undefined) {
|
|
11469
|
+
return true;
|
|
11470
|
+
}
|
|
11471
|
+
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));
|
|
11472
|
+
}
|
|
11455
11473
|
function isMasteryAttemptMetadata(value) {
|
|
11456
11474
|
return isRecord(value) && typeof value.requestFingerprint === "string" && isAssessmentStandardRef(value.standard);
|
|
11457
11475
|
}
|
|
@@ -11476,7 +11494,7 @@ function isPlaycademyAssessmentResultMetadataV1(value) {
|
|
|
11476
11494
|
const review = metadata2.review;
|
|
11477
11495
|
const diagnostic = metadata2.diagnostic;
|
|
11478
11496
|
const routedDiagnostic = metadata2.purpose === "diagnostic" && diagnostic !== undefined && Array.isArray(metadata2.itemSubmissions) && Number.isInteger(metadata2.responseVersion) && isRoutedDiagnosticAttemptMetadata(diagnostic, metadata2.itemSubmissions, metadata2.responseVersion);
|
|
11479
|
-
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);
|
|
11497
|
+
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);
|
|
11480
11498
|
}
|
|
11481
11499
|
function isPlaycademyReviewAssessmentItemResultMetadataV1(value) {
|
|
11482
11500
|
if (!isRecord(value) || !isRecord(value.responses)) {
|
|
@@ -11914,7 +11932,12 @@ var init_assessment_runtime2 = __esm(() => {
|
|
|
11914
11932
|
});
|
|
11915
11933
|
FinalizeAssessmentBodySchema = exports_external.object({
|
|
11916
11934
|
submissionId: exports_external.string().trim().min(1),
|
|
11917
|
-
xpAwarded: exports_external.number().finite().nonnegative()
|
|
11935
|
+
xpAwarded: exports_external.number().finite().nonnegative(),
|
|
11936
|
+
masteredUnits: exports_external.number().finite().optional(),
|
|
11937
|
+
masteredUnitsAbsolute: exports_external.number().finite().int().optional()
|
|
11938
|
+
}).refine((body2) => body2.masteredUnits === undefined || body2.masteredUnitsAbsolute === undefined, {
|
|
11939
|
+
message: "Provide either masteredUnits or masteredUnitsAbsolute, not both",
|
|
11940
|
+
path: ["masteredUnits"]
|
|
11918
11941
|
});
|
|
11919
11942
|
StartRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.and(StartAssessmentBodySchema);
|
|
11920
11943
|
SaveRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.extend(SaveAssessmentBodySchema.shape);
|
|
@@ -11923,10 +11946,10 @@ var init_assessment_runtime2 = __esm(() => {
|
|
|
11923
11946
|
appName: exports_external.string().trim().min(1),
|
|
11924
11947
|
sensorUrl: exports_external.string().url()
|
|
11925
11948
|
});
|
|
11926
|
-
FinalizeRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.extend(
|
|
11949
|
+
FinalizeRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.extend({
|
|
11927
11950
|
appName: exports_external.string().trim().min(1),
|
|
11928
11951
|
sensorUrl: exports_external.string().url()
|
|
11929
|
-
});
|
|
11952
|
+
}).and(FinalizeAssessmentBodySchema);
|
|
11930
11953
|
});
|
|
11931
11954
|
|
|
11932
11955
|
// ../api-core/src/errors/assessment-runtime.error.ts
|
|
@@ -47109,6 +47132,15 @@ function requireMasteryStandard(input, context2) {
|
|
|
47109
47132
|
});
|
|
47110
47133
|
}
|
|
47111
47134
|
}
|
|
47135
|
+
function rejectSystemManagedReview(input, context2) {
|
|
47136
|
+
if (input.purpose === "review") {
|
|
47137
|
+
context2.addIssue({
|
|
47138
|
+
code: "custom",
|
|
47139
|
+
path: ["purpose"],
|
|
47140
|
+
message: "The review bank is system-managed and cannot be managed manually"
|
|
47141
|
+
});
|
|
47142
|
+
}
|
|
47143
|
+
}
|
|
47112
47144
|
var TimebackGradeSchema, TimebackSubjectSchema, CourseGoalsSchema, UpdateGameTimebackIntegrationRequestSchema, CreateGameTimebackIntegrationRequestSchema, TimebackActivityDataSchema, EndActivityRequestSchema, GameRunMetricsSchema, GameCourseMetricsSchema, GameMetricsResponseSchema, AdvanceCourseRequestSchema, UnenrollCourseRequestSchema, HeartbeatRequestSchema, PopulateStudentRequestSchema, DerivedPlatformCourseConfigSchema, TimebackBaseConfigSchema, PlatformTimebackSetupRequestSchema, AdminTimebackMutationBaseSchema, AdminAttributionDateSchema, ADMIN_GRANT_XP_MIN = -1e5, ADMIN_GRANT_XP_MAX = 1e5, ADMIN_GRANT_XP_AMOUNT_RANGE_MESSAGE, GrantTimebackXpRequestSchema, AdjustTimebackTimeRequestSchema, AdjustTimebackMasteryRequestSchema, ReconcileMasteryForConfigChangeSchema, EnrollStudentRequestSchema, UnenrollStudentRequestSchema, ReactivateEnrollmentRequestSchema, VerifyTimebackMetricDiscrepancyRequestSchema, AssessmentPurposeSchema, AssessmentStatusSchema, AssessmentKeySchema, DiagnosticKeySchema, DiagnosticRoutingManifestSchema, DiagnosticAssessmentDefinitionInputSchema, AssessmentStandardRefSchema2, CreateAssessmentRequestSchema, UpdateAssessmentRequestSchema, CopyAssessmentRequestSchema, AssessmentAssociationImportEntrySchema, AttachExistingAssessmentsRequestSchema, ReorderAssessmentsRequestSchema, ReorderQuestionsRequestSchema;
|
|
47113
47145
|
var init_schemas4 = __esm(() => {
|
|
47114
47146
|
init_esm();
|
|
@@ -47370,7 +47402,10 @@ var init_schemas4 = __esm(() => {
|
|
|
47370
47402
|
title: exports_external.string().min(1, "Assessment title is required"),
|
|
47371
47403
|
purpose: AssessmentPurposeSchema,
|
|
47372
47404
|
standard: AssessmentStandardRefSchema2.optional()
|
|
47373
|
-
}).superRefine(
|
|
47405
|
+
}).superRefine((input, context2) => {
|
|
47406
|
+
requireMasteryStandard(input, context2);
|
|
47407
|
+
rejectSystemManagedReview(input, context2);
|
|
47408
|
+
});
|
|
47374
47409
|
UpdateAssessmentRequestSchema = exports_external.object({
|
|
47375
47410
|
title: exports_external.string().trim().min(1, "Assessment title is required").optional(),
|
|
47376
47411
|
purpose: AssessmentPurposeSchema.optional(),
|
|
@@ -47379,13 +47414,16 @@ var init_schemas4 = __esm(() => {
|
|
|
47379
47414
|
status: AssessmentStatusSchema.optional()
|
|
47380
47415
|
}).refine((input) => input.title !== undefined || input.purpose !== undefined || input.standard !== undefined || input.diagnostic !== undefined || input.status !== undefined, {
|
|
47381
47416
|
message: "Title, purpose, standard, diagnostic, or status is required"
|
|
47382
|
-
});
|
|
47417
|
+
}).superRefine(rejectSystemManagedReview);
|
|
47383
47418
|
CopyAssessmentRequestSchema = exports_external.object({
|
|
47384
47419
|
assessmentKey: AssessmentKeySchema,
|
|
47385
47420
|
testIdentifier: exports_external.string().trim().min(1, "Assessment identifier is required"),
|
|
47386
47421
|
purpose: AssessmentPurposeSchema,
|
|
47387
47422
|
standard: AssessmentStandardRefSchema2.optional()
|
|
47388
|
-
}).superRefine(
|
|
47423
|
+
}).superRefine((input, context2) => {
|
|
47424
|
+
requireMasteryStandard(input, context2);
|
|
47425
|
+
rejectSystemManagedReview(input, context2);
|
|
47426
|
+
});
|
|
47389
47427
|
AssessmentAssociationImportEntrySchema = exports_external.object({
|
|
47390
47428
|
assessmentKey: AssessmentKeySchema,
|
|
47391
47429
|
qtiTestIdentifier: exports_external.string().trim().min(1, "Assessment identifier is required"),
|
|
@@ -47394,6 +47432,7 @@ var init_schemas4 = __esm(() => {
|
|
|
47394
47432
|
sortOrder: exports_external.number().int().nonnegative().nullable()
|
|
47395
47433
|
}).superRefine((input, context2) => {
|
|
47396
47434
|
requireMasteryStandard(input, context2);
|
|
47435
|
+
rejectSystemManagedReview(input, context2);
|
|
47397
47436
|
if (input.purpose !== "mastery" && input.standard !== undefined) {
|
|
47398
47437
|
context2.addIssue({
|
|
47399
47438
|
code: "custom",
|
|
@@ -47443,7 +47482,7 @@ var init_schemas4 = __esm(() => {
|
|
|
47443
47482
|
ReorderAssessmentsRequestSchema = exports_external.object({
|
|
47444
47483
|
purpose: AssessmentPurposeSchema,
|
|
47445
47484
|
testIdentifiers: exports_external.array(exports_external.string().trim().min(1, "Assessment identifier is required")).min(1, "At least one assessment is required")
|
|
47446
|
-
});
|
|
47485
|
+
}).superRefine(rejectSystemManagedReview);
|
|
47447
47486
|
ReorderQuestionsRequestSchema = exports_external.object({
|
|
47448
47487
|
items: exports_external.array(exports_external.object({
|
|
47449
47488
|
identifier: exports_external.string().min(1),
|
|
@@ -90872,6 +90911,7 @@ function buildAssessmentCompletionEvent(input) {
|
|
|
90872
90911
|
totalQuestions: input.totalQuestions,
|
|
90873
90912
|
correctQuestions: input.correctQuestions,
|
|
90874
90913
|
xpEarned: input.xpAwarded,
|
|
90914
|
+
...input.masteredUnits ? { masteredUnits: input.masteredUnits } : {},
|
|
90875
90915
|
attemptNumber: input.attemptNumber,
|
|
90876
90916
|
process: false,
|
|
90877
90917
|
subject: input.subject,
|
|
@@ -90880,7 +90920,10 @@ function buildAssessmentCompletionEvent(input) {
|
|
|
90880
90920
|
eventId: input.eventId,
|
|
90881
90921
|
eventTime: input.submittedAt,
|
|
90882
90922
|
runId: input.attemptId,
|
|
90883
|
-
generatedExtensions:
|
|
90923
|
+
generatedExtensions: {
|
|
90924
|
+
...metadata2,
|
|
90925
|
+
...input.pctCompleteApp !== undefined ? { pctCompleteApp: input.pctCompleteApp } : {}
|
|
90926
|
+
},
|
|
90884
90927
|
eventExtensions: {
|
|
90885
90928
|
playcademy: {
|
|
90886
90929
|
assessmentPurpose: input.purpose,
|
|
@@ -90934,6 +90977,85 @@ function deriveSourcedIds(courseId) {
|
|
|
90934
90977
|
componentResource: `${courseId}-cr`
|
|
90935
90978
|
};
|
|
90936
90979
|
}
|
|
90980
|
+
function hasMasteryAwardRequest(request) {
|
|
90981
|
+
return request.masteredUnits !== undefined || request.masteredUnitsAbsolute !== undefined;
|
|
90982
|
+
}
|
|
90983
|
+
|
|
90984
|
+
class ActivityMasteryAward {
|
|
90985
|
+
mastery;
|
|
90986
|
+
events;
|
|
90987
|
+
constructor(mastery, events) {
|
|
90988
|
+
this.mastery = mastery;
|
|
90989
|
+
this.events = events;
|
|
90990
|
+
}
|
|
90991
|
+
async evaluate(input) {
|
|
90992
|
+
const ids = deriveSourcedIds(input.courseId);
|
|
90993
|
+
const progress = await this.mastery.checkProgress({
|
|
90994
|
+
studentId: input.studentId,
|
|
90995
|
+
courseId: input.courseId,
|
|
90996
|
+
resourceId: ids.resource,
|
|
90997
|
+
masteredUnits: input.masteredUnits ?? 0,
|
|
90998
|
+
masteredUnitsAbsolute: input.masteredUnitsAbsolute
|
|
90999
|
+
});
|
|
91000
|
+
if (!progress) {
|
|
91001
|
+
return {
|
|
91002
|
+
masteredUnitsApplied: input.masteredUnits ?? 0,
|
|
91003
|
+
masteryAchieved: false,
|
|
91004
|
+
masteryRevoked: false
|
|
91005
|
+
};
|
|
91006
|
+
}
|
|
91007
|
+
return {
|
|
91008
|
+
masteredUnitsApplied: progress.effectiveDelta,
|
|
91009
|
+
pctCompleteApp: progress.pctCompleteApp,
|
|
91010
|
+
masteryAchieved: progress.masteryAchieved,
|
|
91011
|
+
masteryRevoked: progress.masteryRevoked,
|
|
91012
|
+
...progress.writeWarning ? { warnings: [progress.writeWarning] } : {}
|
|
91013
|
+
};
|
|
91014
|
+
}
|
|
91015
|
+
async settle(evaluation, input) {
|
|
91016
|
+
let completionEntryWritten = false;
|
|
91017
|
+
if (evaluation.masteryAchieved) {
|
|
91018
|
+
setAttributes({ "app.timeback.mastery_achieved": true });
|
|
91019
|
+
completionEntryWritten = await this.mastery.createCompletionEntry(input.studentId, input.courseId, input.classId, input.appName);
|
|
91020
|
+
await this.emitCourseCompletionHistoryEvent(input);
|
|
91021
|
+
}
|
|
91022
|
+
if (evaluation.masteryRevoked) {
|
|
91023
|
+
await this.mastery.revokeCompletionEntry(input.studentId, input.courseId, input.classId, input.appName);
|
|
91024
|
+
}
|
|
91025
|
+
return { completionEntryWritten };
|
|
91026
|
+
}
|
|
91027
|
+
async emitCourseCompletionHistoryEvent(input) {
|
|
91028
|
+
const ids = deriveSourcedIds(input.courseId);
|
|
91029
|
+
await this.events.emitActivityEvent({
|
|
91030
|
+
eventId: input.completionHistoryEventId,
|
|
91031
|
+
studentId: input.studentId,
|
|
91032
|
+
studentEmail: input.studentEmail,
|
|
91033
|
+
gameId: input.gameId,
|
|
91034
|
+
activityId: input.activityId,
|
|
91035
|
+
activityName: "Course completed",
|
|
91036
|
+
courseId: ids.course,
|
|
91037
|
+
courseName: input.courseName,
|
|
91038
|
+
subject: input.subject,
|
|
91039
|
+
appName: input.appName,
|
|
91040
|
+
sensorUrl: input.sensorUrl,
|
|
91041
|
+
process: false,
|
|
91042
|
+
includeAttempt: false,
|
|
91043
|
+
eventExtensions: {
|
|
91044
|
+
playcademy: {
|
|
91045
|
+
eventKind: "course-completed",
|
|
91046
|
+
source: "gameplay"
|
|
91047
|
+
}
|
|
91048
|
+
},
|
|
91049
|
+
generatedExtensions: {
|
|
91050
|
+
playcademy: {
|
|
91051
|
+
eventKind: "course-completed",
|
|
91052
|
+
source: "gameplay",
|
|
91053
|
+
activityId: input.activityId
|
|
91054
|
+
}
|
|
91055
|
+
}
|
|
91056
|
+
}).catch(catchEvent("timeback.caliper_completion_event_failed"));
|
|
91057
|
+
}
|
|
91058
|
+
}
|
|
90937
91059
|
function validateProgressData(progressData) {
|
|
90938
91060
|
if (!progressData.subject) {
|
|
90939
91061
|
throw new ConfigurationError("subject", "Subject is required for Caliper events. Provide it in progressData.subject");
|
|
@@ -90949,12 +91071,12 @@ function validateProgressData(progressData) {
|
|
|
90949
91071
|
class ActivityRecord {
|
|
90950
91072
|
core;
|
|
90951
91073
|
students;
|
|
90952
|
-
|
|
91074
|
+
masteryAward;
|
|
90953
91075
|
events;
|
|
90954
|
-
constructor(core3, students,
|
|
91076
|
+
constructor(core3, students, masteryAward, events) {
|
|
90955
91077
|
this.core = core3;
|
|
90956
91078
|
this.students = students;
|
|
90957
|
-
this.
|
|
91079
|
+
this.masteryAward = masteryAward;
|
|
90958
91080
|
this.events = events;
|
|
90959
91081
|
}
|
|
90960
91082
|
async record(courseId, studentIdentifier, progressData) {
|
|
@@ -90980,52 +91102,32 @@ class ActivityRecord {
|
|
|
90980
91102
|
throw error88;
|
|
90981
91103
|
}
|
|
90982
91104
|
const legacyLineItemId = `${ids.course}-${activityId}-assessment`;
|
|
90983
|
-
const [currentAttemptNumber,
|
|
91105
|
+
const [currentAttemptNumber, mastery] = await Promise.all([
|
|
90984
91106
|
this.resolveAttemptNumber(attemptNumber, studentId, caliperLineItemId, legacyLineItemId),
|
|
90985
|
-
this.
|
|
91107
|
+
this.masteryAward.evaluate({
|
|
90986
91108
|
studentId,
|
|
90987
91109
|
courseId,
|
|
90988
|
-
|
|
90989
|
-
masteredUnits: progressData.masteredUnits ?? 0,
|
|
91110
|
+
masteredUnits: progressData.masteredUnits,
|
|
90990
91111
|
masteredUnitsAbsolute: progressData.masteredUnitsAbsolute
|
|
90991
91112
|
})
|
|
90992
91113
|
]);
|
|
90993
91114
|
setAttributes({ "app.timeback.attempt_number": currentAttemptNumber });
|
|
90994
|
-
|
|
90995
|
-
const
|
|
90996
|
-
|
|
90997
|
-
|
|
90998
|
-
|
|
90999
|
-
|
|
91000
|
-
|
|
91001
|
-
|
|
91002
|
-
|
|
91003
|
-
|
|
91004
|
-
|
|
91005
|
-
|
|
91006
|
-
|
|
91007
|
-
|
|
91008
|
-
|
|
91009
|
-
|
|
91010
|
-
}
|
|
91011
|
-
if (masteryAchieved) {
|
|
91012
|
-
completionEntryWritten = await this.mastery.createCompletionEntry(studentId, courseId, progressData.classId, progressData.appName);
|
|
91013
|
-
await this.emitCourseCompletionHistoryEvent({
|
|
91014
|
-
studentId,
|
|
91015
|
-
studentEmail,
|
|
91016
|
-
gameId: progressData.gameId,
|
|
91017
|
-
activityId,
|
|
91018
|
-
courseId: ids.course,
|
|
91019
|
-
courseName,
|
|
91020
|
-
subject: progressData.subject,
|
|
91021
|
-
appName: progressData.appName,
|
|
91022
|
-
sensorUrl: progressData.sensorUrl,
|
|
91023
|
-
eventId: progressData.completionHistoryEventId
|
|
91024
|
-
});
|
|
91025
|
-
}
|
|
91026
|
-
if (masteryProgress?.masteryRevoked) {
|
|
91027
|
-
await this.mastery.revokeCompletionEntry(studentId, courseId, progressData.classId, progressData.appName);
|
|
91028
|
-
}
|
|
91115
|
+
const effectiveMasteredUnits = mastery.masteredUnitsApplied;
|
|
91116
|
+
const { pctCompleteApp, warnings } = mastery;
|
|
91117
|
+
const extensions = pctCompleteApp !== undefined ? { ...progressData.extensions, pctCompleteApp } : progressData.extensions;
|
|
91118
|
+
const { completionEntryWritten } = await this.masteryAward.settle(mastery, {
|
|
91119
|
+
studentId,
|
|
91120
|
+
studentEmail,
|
|
91121
|
+
courseId,
|
|
91122
|
+
classId: progressData.classId,
|
|
91123
|
+
gameId: progressData.gameId,
|
|
91124
|
+
activityId,
|
|
91125
|
+
courseName,
|
|
91126
|
+
subject: progressData.subject,
|
|
91127
|
+
appName: progressData.appName,
|
|
91128
|
+
sensorUrl: progressData.sensorUrl,
|
|
91129
|
+
completionHistoryEventId: progressData.completionHistoryEventId
|
|
91130
|
+
});
|
|
91029
91131
|
try {
|
|
91030
91132
|
await this.events.emitActivityEvent({
|
|
91031
91133
|
studentId,
|
|
@@ -91046,7 +91148,7 @@ class ActivityRecord {
|
|
|
91046
91148
|
appName: progressData.appName,
|
|
91047
91149
|
sensorUrl: progressData.sensorUrl,
|
|
91048
91150
|
eventId: progressData.eventId,
|
|
91049
|
-
extensions
|
|
91151
|
+
extensions,
|
|
91050
91152
|
...progressData.runId ? { runId: progressData.runId } : {}
|
|
91051
91153
|
}).catch((error88) => {
|
|
91052
91154
|
setAttributes({ "app.timeback.caliper_emit_failed": true });
|
|
@@ -91067,36 +91169,6 @@ class ActivityRecord {
|
|
|
91067
91169
|
...warnings ? { warnings } : {}
|
|
91068
91170
|
};
|
|
91069
91171
|
}
|
|
91070
|
-
async emitCourseCompletionHistoryEvent(data) {
|
|
91071
|
-
await this.events.emitActivityEvent({
|
|
91072
|
-
eventId: data.eventId,
|
|
91073
|
-
studentId: data.studentId,
|
|
91074
|
-
studentEmail: data.studentEmail,
|
|
91075
|
-
gameId: data.gameId,
|
|
91076
|
-
activityId: data.activityId,
|
|
91077
|
-
activityName: "Course completed",
|
|
91078
|
-
courseId: data.courseId,
|
|
91079
|
-
courseName: data.courseName,
|
|
91080
|
-
subject: data.subject,
|
|
91081
|
-
appName: data.appName,
|
|
91082
|
-
sensorUrl: data.sensorUrl,
|
|
91083
|
-
process: false,
|
|
91084
|
-
includeAttempt: false,
|
|
91085
|
-
eventExtensions: {
|
|
91086
|
-
playcademy: {
|
|
91087
|
-
eventKind: "course-completed",
|
|
91088
|
-
source: "gameplay"
|
|
91089
|
-
}
|
|
91090
|
-
},
|
|
91091
|
-
generatedExtensions: {
|
|
91092
|
-
playcademy: {
|
|
91093
|
-
eventKind: "course-completed",
|
|
91094
|
-
source: "gameplay",
|
|
91095
|
-
activityId: data.activityId
|
|
91096
|
-
}
|
|
91097
|
-
}
|
|
91098
|
-
}).catch(catchEvent("timeback.caliper_completion_event_failed"));
|
|
91099
|
-
}
|
|
91100
91172
|
async resolveAttemptNumber(providedAttemptNumber, studentId, caliperLineItemId, legacyLineItemId) {
|
|
91101
91173
|
if (providedAttemptNumber) {
|
|
91102
91174
|
setAttributes({ "app.timeback.attempt_source": "provided" });
|
|
@@ -91494,9 +91566,12 @@ class ActivityEvents {
|
|
|
91494
91566
|
}
|
|
91495
91567
|
}
|
|
91496
91568
|
function createActivityNamespace(core3, deps) {
|
|
91497
|
-
const
|
|
91569
|
+
const masteryAward = new ActivityMasteryAward(deps.mastery, deps.events);
|
|
91570
|
+
const record3 = new ActivityRecord(core3, deps.students, masteryAward, deps.events);
|
|
91498
91571
|
const session2 = new ActivitySession(deps.students, deps.events);
|
|
91499
91572
|
return {
|
|
91573
|
+
evaluateMasteryAward: (input) => masteryAward.evaluate(input),
|
|
91574
|
+
settleMasteryAward: (evaluation, input) => masteryAward.settle(evaluation, input),
|
|
91500
91575
|
record: (courseId, studentIdentifier, progressData) => record3.record(courseId, studentIdentifier, progressData),
|
|
91501
91576
|
session: (courseId, studentIdentifier, sessionData) => session2.record(courseId, studentIdentifier, sessionData),
|
|
91502
91577
|
listEvents: (params) => deps.events.listEvents(params),
|
|
@@ -92780,8 +92855,9 @@ class TimebackClient2 {
|
|
|
92780
92855
|
this.core.caches.clearAll();
|
|
92781
92856
|
}
|
|
92782
92857
|
}
|
|
92783
|
-
var TimebackError, TimebackAuthenticationError, StudentNotFoundError, ConfigurationError, UUID_REGEX2, ONEROSTER_PATHS, TIMEBACK_API_URLS, QTI_API_URL = "https://qti.alpha-1edtech.ai/api", TIMEBACK_AUTH_URLS, CALIPER_API_URLS, ENV_VARS, ONEROSTER_STATUS, SCORE_STATUS, CACHE_DEFAULTS, PLAYCADEMY_DEFAULTS, RESOURCE_DEFAULTS, DEFAULT_TIMEOUT = 30000, SUBJECT_VALUES2, GRADE_VALUES3, MASTERY_WRITE_CAPPED_WARNING_CODE = "MASTERY_WRITE_CAPPED", EmailSchema;
|
|
92858
|
+
var TimebackError, TimebackAuthenticationError, StudentNotFoundError, ConfigurationError, UUID_REGEX2, ONEROSTER_PATHS, NO_MASTERY_AWARD, TIMEBACK_API_URLS, QTI_API_URL = "https://qti.alpha-1edtech.ai/api", TIMEBACK_AUTH_URLS, CALIPER_API_URLS, ENV_VARS, ONEROSTER_STATUS, SCORE_STATUS, CACHE_DEFAULTS, PLAYCADEMY_DEFAULTS, RESOURCE_DEFAULTS, DEFAULT_TIMEOUT = 30000, SUBJECT_VALUES2, GRADE_VALUES3, MASTERY_WRITE_CAPPED_WARNING_CODE = "MASTERY_WRITE_CAPPED", EmailSchema;
|
|
92784
92859
|
var init_dist5 = __esm(async () => {
|
|
92860
|
+
init_spans();
|
|
92785
92861
|
init_spans();
|
|
92786
92862
|
init_src();
|
|
92787
92863
|
init_spans();
|
|
@@ -92842,6 +92918,11 @@ var init_dist5 = __esm(async () => {
|
|
|
92842
92918
|
courses: "/ims/oneroster/rostering/v1p2/courses",
|
|
92843
92919
|
componentResources: "/ims/oneroster/rostering/v1p2/courses/component-resources"
|
|
92844
92920
|
};
|
|
92921
|
+
NO_MASTERY_AWARD = {
|
|
92922
|
+
masteredUnitsApplied: 0,
|
|
92923
|
+
masteryAchieved: false,
|
|
92924
|
+
masteryRevoked: false
|
|
92925
|
+
};
|
|
92845
92926
|
TIMEBACK_API_URLS = {
|
|
92846
92927
|
production: "https://api.alpha-1edtech.ai",
|
|
92847
92928
|
staging: "https://api.staging.alpha-1edtech.com"
|
|
@@ -95607,6 +95688,62 @@ function stringField2(value) {
|
|
|
95607
95688
|
function firstStringField2(...values) {
|
|
95608
95689
|
return values.map(stringField2).find(Boolean) ?? "";
|
|
95609
95690
|
}
|
|
95691
|
+
function normalizedWhitespace2(value) {
|
|
95692
|
+
return value.normalize("NFKC").trim().replace(/\s+/g, " ");
|
|
95693
|
+
}
|
|
95694
|
+
function normalizedIdentityCase2(value) {
|
|
95695
|
+
return value.toLocaleUpperCase("en-US");
|
|
95696
|
+
}
|
|
95697
|
+
function frameworkAliasKey2(value) {
|
|
95698
|
+
return value.normalize("NFKC").toLocaleLowerCase("en-US").replace(/[^a-z0-9]+/g, "");
|
|
95699
|
+
}
|
|
95700
|
+
function isCommonCoreMathIdentifier2(identifier) {
|
|
95701
|
+
const canonical = normalizedIdentityCase2(normalizedWhitespace2(identifier));
|
|
95702
|
+
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);
|
|
95703
|
+
}
|
|
95704
|
+
function isCommonCoreElaIdentifier2(identifier) {
|
|
95705
|
+
const canonical = normalizedIdentityCase2(normalizedWhitespace2(identifier));
|
|
95706
|
+
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);
|
|
95707
|
+
}
|
|
95708
|
+
function canonicalFramework2(authoredFramework, identifier) {
|
|
95709
|
+
const aliasKey = frameworkAliasKey2(authoredFramework);
|
|
95710
|
+
if (COMMON_CORE_MATH_FRAMEWORK_ALIASES2.has(aliasKey) || COMMON_CORE_FRAMEWORK_ALIASES2.has(aliasKey) && isCommonCoreMathIdentifier2(identifier)) {
|
|
95711
|
+
return "CCSS.Math";
|
|
95712
|
+
}
|
|
95713
|
+
if (COMMON_CORE_ELA_FRAMEWORK_ALIASES2.has(aliasKey) || COMMON_CORE_FRAMEWORK_ALIASES2.has(aliasKey) && isCommonCoreElaIdentifier2(identifier)) {
|
|
95714
|
+
return "CCSS.ELA-Literacy";
|
|
95715
|
+
}
|
|
95716
|
+
if (COMMON_CORE_FRAMEWORK_ALIASES2.has(aliasKey)) {
|
|
95717
|
+
return "CCSS";
|
|
95718
|
+
}
|
|
95719
|
+
return normalizedIdentityCase2(authoredFramework);
|
|
95720
|
+
}
|
|
95721
|
+
function canonicalIdentifier2(framework, authoredIdentifier) {
|
|
95722
|
+
const identifier = normalizedIdentityCase2(normalizedWhitespace2(authoredIdentifier));
|
|
95723
|
+
if (framework === "CCSS.Math") {
|
|
95724
|
+
return identifier.replace(/^CCSS\.MATH\.CONTENT\./, "").replace(/^CCSS\.MATH\.PRACTICE\./, "").replace(/^CCSS\.MATH\./, "");
|
|
95725
|
+
}
|
|
95726
|
+
if (framework === "CCSS.ELA-Literacy") {
|
|
95727
|
+
return identifier.replace(/^CCSS\.ELA-LITERACY\./, "");
|
|
95728
|
+
}
|
|
95729
|
+
return identifier;
|
|
95730
|
+
}
|
|
95731
|
+
function canonicalAssessmentStandardRef2(standard) {
|
|
95732
|
+
const authoredFramework = normalizedWhitespace2(standard.framework);
|
|
95733
|
+
const framework = canonicalFramework2(authoredFramework, standard.identifier);
|
|
95734
|
+
const identifier = canonicalIdentifier2(framework, standard.identifier);
|
|
95735
|
+
return {
|
|
95736
|
+
framework,
|
|
95737
|
+
identifier
|
|
95738
|
+
};
|
|
95739
|
+
}
|
|
95740
|
+
function assessmentStandardRefKey2(standard) {
|
|
95741
|
+
const canonical = canonicalAssessmentStandardRef2(standard);
|
|
95742
|
+
return JSON.stringify([
|
|
95743
|
+
normalizedIdentityCase2(canonical.framework),
|
|
95744
|
+
normalizedIdentityCase2(canonical.identifier)
|
|
95745
|
+
]);
|
|
95746
|
+
}
|
|
95610
95747
|
function dedupeStandards2(standards) {
|
|
95611
95748
|
const deduped = new Map;
|
|
95612
95749
|
for (const standard of standards) {
|
|
@@ -98340,6 +98477,22 @@ var init_locks = __esm(() => {
|
|
|
98340
98477
|
});
|
|
98341
98478
|
|
|
98342
98479
|
// ../api-core/src/utils/assessment-runtime-lock.util.ts
|
|
98480
|
+
async function crossAssessmentAttemptLockBarrier(lock, db2, attemptId) {
|
|
98481
|
+
const maxAttempts = 5;
|
|
98482
|
+
for (let attempt = 1;attempt <= maxAttempts; attempt += 1) {
|
|
98483
|
+
try {
|
|
98484
|
+
await lock(db2, [assessmentRuntimeAttemptLockKey(attemptId)], async () => {
|
|
98485
|
+
return;
|
|
98486
|
+
});
|
|
98487
|
+
return;
|
|
98488
|
+
} catch (error88) {
|
|
98489
|
+
if (!(error88 instanceof ServiceUnavailableError) || attempt === maxAttempts) {
|
|
98490
|
+
throw error88;
|
|
98491
|
+
}
|
|
98492
|
+
await sleep(25);
|
|
98493
|
+
}
|
|
98494
|
+
}
|
|
98495
|
+
}
|
|
98343
98496
|
function normalizeKeys(keys) {
|
|
98344
98497
|
return [...new Set(keys)].toSorted();
|
|
98345
98498
|
}
|
|
@@ -99353,6 +99506,97 @@ var init_timeback_qti_authoring_util = __esm(() => {
|
|
|
99353
99506
|
});
|
|
99354
99507
|
|
|
99355
99508
|
// ../api-core/src/utils/timeback-assessment-runtime.util.ts
|
|
99509
|
+
function assessmentPreparationContentionError(expectedResponseVersion, responseVersion, kind) {
|
|
99510
|
+
if (expectedResponseVersion === responseVersion) {
|
|
99511
|
+
return new ServiceUnavailableError(`The ${kind} response state changed repeatedly while this item was being prepared. Try again shortly.`, {
|
|
99512
|
+
retryable: true,
|
|
99513
|
+
reason: "PREPARATION_CONTENTION",
|
|
99514
|
+
expectedResponseVersion,
|
|
99515
|
+
responseVersion
|
|
99516
|
+
});
|
|
99517
|
+
}
|
|
99518
|
+
return AssessmentRuntimeError.from(assessmentResponseVersionConflict(expectedResponseVersion, responseVersion));
|
|
99519
|
+
}
|
|
99520
|
+
function assessmentTrackableCourse(integration) {
|
|
99521
|
+
if (!isTimebackGrade2(integration.grade) || !isTimebackSubject(integration.subject)) {
|
|
99522
|
+
return null;
|
|
99523
|
+
}
|
|
99524
|
+
return { grade: integration.grade, subject: integration.subject };
|
|
99525
|
+
}
|
|
99526
|
+
function buildAssessmentActivityData(input) {
|
|
99527
|
+
if (!input.course) {
|
|
99528
|
+
return;
|
|
99529
|
+
}
|
|
99530
|
+
return {
|
|
99531
|
+
activityId: input.activityId,
|
|
99532
|
+
activityName: input.activityName,
|
|
99533
|
+
...input.course,
|
|
99534
|
+
courseId: input.courseId
|
|
99535
|
+
};
|
|
99536
|
+
}
|
|
99537
|
+
function buildDiagnosticAssessmentSnapshot(input) {
|
|
99538
|
+
return {
|
|
99539
|
+
attemptId: input.attemptId,
|
|
99540
|
+
responseVersion: input.metadata.responseVersion,
|
|
99541
|
+
status: "in_progress",
|
|
99542
|
+
flow: "platform-routed-item-submit",
|
|
99543
|
+
...input.activityData ? { activityData: input.activityData } : {},
|
|
99544
|
+
assessment: assessmentPresentationForAttempt(input.assessment, input.attemptId),
|
|
99545
|
+
selection: {
|
|
99546
|
+
kind: "platform-routed-diagnostic",
|
|
99547
|
+
purpose: "diagnostic",
|
|
99548
|
+
definitionId: input.metadata.diagnostic.definitionId,
|
|
99549
|
+
diagnosticKey: input.metadata.diagnostic.diagnosticKey,
|
|
99550
|
+
routingRevision: input.metadata.diagnostic.routingRevision
|
|
99551
|
+
},
|
|
99552
|
+
routing: projectDiagnosticRoutingSnapshot(input.metadata.diagnostic.routingRevision, input.state),
|
|
99553
|
+
completion: null
|
|
99554
|
+
};
|
|
99555
|
+
}
|
|
99556
|
+
function buildConventionalAssessmentSnapshot(input) {
|
|
99557
|
+
const metadata2 = input.metadata;
|
|
99558
|
+
let selection;
|
|
99559
|
+
if (metadata2.purpose === "review") {
|
|
99560
|
+
selection = {
|
|
99561
|
+
kind: "standards-review",
|
|
99562
|
+
purpose: metadata2.purpose,
|
|
99563
|
+
standards: [...metadata2.review.standards],
|
|
99564
|
+
candidateItemsPerStandard: metadata2.review.candidateItemsPerStandard,
|
|
99565
|
+
selections: [...metadata2.review.selections],
|
|
99566
|
+
fulfillment: metadata2.review.fulfillment
|
|
99567
|
+
};
|
|
99568
|
+
} else if (metadata2.purpose === "mastery") {
|
|
99569
|
+
selection = {
|
|
99570
|
+
kind: "standard-quiz",
|
|
99571
|
+
purpose: metadata2.purpose,
|
|
99572
|
+
standard: metadata2.mastery.standard
|
|
99573
|
+
};
|
|
99574
|
+
} else {
|
|
99575
|
+
selection = { kind: "fixed-test", purpose: metadata2.purpose };
|
|
99576
|
+
}
|
|
99577
|
+
return {
|
|
99578
|
+
attemptId: input.attemptId,
|
|
99579
|
+
responseVersion: metadata2.responseVersion,
|
|
99580
|
+
status: "in_progress",
|
|
99581
|
+
flow: assessmentFlowForPurpose(metadata2.purpose),
|
|
99582
|
+
...input.activityData ? { activityData: input.activityData } : {},
|
|
99583
|
+
assessment: assessmentPresentationForAttempt(input.assessment, input.attemptId),
|
|
99584
|
+
responses: metadata2.responses,
|
|
99585
|
+
itemSubmissions: metadata2.itemSubmissions,
|
|
99586
|
+
score: null,
|
|
99587
|
+
selection
|
|
99588
|
+
};
|
|
99589
|
+
}
|
|
99590
|
+
function buildAssessmentAwardRecord(input, mastery) {
|
|
99591
|
+
return {
|
|
99592
|
+
xpAwarded: input.xpAwarded,
|
|
99593
|
+
...input.masteredUnits !== undefined ? { masteredUnits: input.masteredUnits } : {},
|
|
99594
|
+
...input.masteredUnitsAbsolute !== undefined ? { masteredUnitsAbsolute: input.masteredUnitsAbsolute } : {},
|
|
99595
|
+
masteredUnitsApplied: mastery.masteredUnitsApplied,
|
|
99596
|
+
...mastery.pctCompleteApp !== undefined ? { pctCompleteApp: mastery.pctCompleteApp } : {},
|
|
99597
|
+
...mastery.warnings ? { warnings: mastery.warnings } : {}
|
|
99598
|
+
};
|
|
99599
|
+
}
|
|
99356
99600
|
function stageAssessmentAttemptSupersession(attempt) {
|
|
99357
99601
|
Object.assign(attempt.result, ASSESSMENT_ATTEMPT_SUPERSEDED);
|
|
99358
99602
|
Object.assign(attempt.selection, ASSESSMENT_ATTEMPT_SUPERSEDED);
|
|
@@ -99563,13 +99807,35 @@ function buildAssessmentSubmitResult(input) {
|
|
|
99563
99807
|
}
|
|
99564
99808
|
return { ...base, purpose: input.metadata.purpose };
|
|
99565
99809
|
}
|
|
99810
|
+
function assessmentAwardFromResult(result, metadata2) {
|
|
99811
|
+
if (metadata2.award) {
|
|
99812
|
+
return metadata2.award;
|
|
99813
|
+
}
|
|
99814
|
+
const xp = result.metadata?.xp;
|
|
99815
|
+
return typeof xp === "number" && Number.isFinite(xp) && xp >= 0 ? { xpAwarded: xp, masteredUnitsApplied: 0 } : null;
|
|
99816
|
+
}
|
|
99566
99817
|
function buildCompletedAssessmentSubmitResult(input) {
|
|
99567
|
-
const
|
|
99568
|
-
|
|
99818
|
+
const attemptId = input.result.sourcedId;
|
|
99819
|
+
const award = assessmentAwardFromResult(input.result, input.metadata);
|
|
99820
|
+
if (award === null) {
|
|
99821
|
+
throw new GoneError("This completed assessment is missing its recorded XP.", {
|
|
99822
|
+
attemptId
|
|
99823
|
+
});
|
|
99824
|
+
}
|
|
99825
|
+
const submitted = buildAssessmentSubmitResult({ attemptId, metadata: input.metadata });
|
|
99826
|
+
if (!submitted) {
|
|
99827
|
+
throw new GoneError("This completed assessment has incomplete persisted results.", {
|
|
99828
|
+
attemptId
|
|
99829
|
+
});
|
|
99830
|
+
}
|
|
99831
|
+
return {
|
|
99569
99832
|
...submitted,
|
|
99570
99833
|
status: "completed",
|
|
99571
|
-
xpAwarded:
|
|
99572
|
-
|
|
99834
|
+
xpAwarded: award.xpAwarded,
|
|
99835
|
+
masteredUnitsApplied: award.masteredUnitsApplied,
|
|
99836
|
+
...award.pctCompleteApp !== undefined ? { pctCompleteApp: award.pctCompleteApp } : {},
|
|
99837
|
+
...award.warnings ? { warnings: award.warnings } : {}
|
|
99838
|
+
};
|
|
99573
99839
|
}
|
|
99574
99840
|
function assessmentAttemptId(input) {
|
|
99575
99841
|
return deterministicUUID([
|
|
@@ -99896,6 +100162,22 @@ function assessmentFixtureScoringKeys(assessment, questions) {
|
|
|
99896
100162
|
...Object.keys(responseAreas).length > 0 ? { responseAreas } : {}
|
|
99897
100163
|
};
|
|
99898
100164
|
}
|
|
100165
|
+
function prepareDiagnosticAssessmentResponses(assessment, current, input) {
|
|
100166
|
+
const update2 = { [input.itemIdentifier]: input.responses };
|
|
100167
|
+
validateAssessmentResponseUpdate(assessment, update2);
|
|
100168
|
+
const responses = applyAssessmentResponseUpdate(current, update2);
|
|
100169
|
+
const item = assessment.items.find((candidate) => candidate.identifier === input.itemIdentifier);
|
|
100170
|
+
const itemResponses = responses[input.itemIdentifier];
|
|
100171
|
+
const missingResponse = item?.interactions.find((interaction) => itemResponses?.[interaction.responseIdentifier] === undefined);
|
|
100172
|
+
if (!item || !itemResponses || missingResponse) {
|
|
100173
|
+
throw new AssessmentRuntimeError("INVALID_RESPONSE", `Diagnostic item ${input.itemIdentifier} requires a response for every interaction.`, {
|
|
100174
|
+
itemIdentifier: input.itemIdentifier,
|
|
100175
|
+
...missingResponse ? { responseIdentifier: missingResponse.responseIdentifier } : {}
|
|
100176
|
+
});
|
|
100177
|
+
}
|
|
100178
|
+
validateAssessmentResponses(assessment, { [input.itemIdentifier]: itemResponses });
|
|
100179
|
+
return responses;
|
|
100180
|
+
}
|
|
99899
100181
|
function validateAssessmentResponseUpdate(assessment, update2) {
|
|
99900
100182
|
const message = assessmentResponseUpdateValidationMessage(assessment, update2);
|
|
99901
100183
|
if (message) {
|
|
@@ -99982,7 +100264,7 @@ function buildAssessmentResultSubmission(input) {
|
|
|
99982
100264
|
}
|
|
99983
100265
|
};
|
|
99984
100266
|
}
|
|
99985
|
-
function
|
|
100267
|
+
function buildAssessmentResultAwardFinalization(input) {
|
|
99986
100268
|
const completion = input.metadata.completion;
|
|
99987
100269
|
if (!completion || completion.totalQuestions === undefined) {
|
|
99988
100270
|
throw new Error("A scored assessment must retain its completion facts before finalization");
|
|
@@ -99990,8 +100272,10 @@ function buildAssessmentResultXpFinalization(input) {
|
|
|
99990
100272
|
const metadata2 = {
|
|
99991
100273
|
...input.metadata,
|
|
99992
100274
|
finalizedAt: input.timestamp,
|
|
99993
|
-
updatedAt: input.timestamp
|
|
100275
|
+
updatedAt: input.timestamp,
|
|
100276
|
+
award: input.award
|
|
99994
100277
|
};
|
|
100278
|
+
const masteryRequested = input.award.masteredUnits !== undefined || input.award.masteredUnitsAbsolute !== undefined;
|
|
99995
100279
|
return {
|
|
99996
100280
|
metadata: metadata2,
|
|
99997
100281
|
resultUpdate: {
|
|
@@ -100003,7 +100287,8 @@ function buildAssessmentResultXpFinalization(input) {
|
|
|
100003
100287
|
...ASSESSMENT_ATTEMPT_COMPLETED,
|
|
100004
100288
|
metadata: {
|
|
100005
100289
|
...mergeAssessmentRuntimeMetadata(input.result.metadata, metadata2),
|
|
100006
|
-
xp: input.xpAwarded,
|
|
100290
|
+
xp: input.award.xpAwarded,
|
|
100291
|
+
...masteryRequested ? { masteredUnits: input.award.masteredUnitsApplied } : {},
|
|
100007
100292
|
totalQuestions: completion.totalQuestions,
|
|
100008
100293
|
correctQuestions: completion.correctQuestions,
|
|
100009
100294
|
appName: input.appName
|
|
@@ -100016,6 +100301,7 @@ var init_timeback_assessment_runtime_util = __esm(() => {
|
|
|
100016
100301
|
init_src();
|
|
100017
100302
|
init_assessment_runtime2();
|
|
100018
100303
|
init_qti();
|
|
100304
|
+
init_types2();
|
|
100019
100305
|
init_utils6();
|
|
100020
100306
|
init_timeback4();
|
|
100021
100307
|
init_uuid();
|
|
@@ -100024,6 +100310,210 @@ var init_timeback_assessment_runtime_util = __esm(() => {
|
|
|
100024
100310
|
});
|
|
100025
100311
|
|
|
100026
100312
|
// ../api-core/src/utils/timeback-review-mapping.util.ts
|
|
100313
|
+
function reviewBankTestItemIdentifiers(test) {
|
|
100314
|
+
return qtiTestParts(test).flatMap((part) => part["qti-assessment-section"].flatMap((section) => (section["qti-assessment-item-ref"] ?? []).map((reference) => reference.identifier)));
|
|
100315
|
+
}
|
|
100316
|
+
function canonicalSyncContributors(contributors) {
|
|
100317
|
+
return contributors.map((contributor) => {
|
|
100318
|
+
const canonical = canonicalContributorIdentity(contributor);
|
|
100319
|
+
return {
|
|
100320
|
+
...contributor,
|
|
100321
|
+
...canonical
|
|
100322
|
+
};
|
|
100323
|
+
}).toSorted((left, right) => compareCodeUnits(left.standardKey, right.standardKey) || compareCodeUnits(left.qtiTestIdentifier, right.qtiTestIdentifier) || compareCodeUnits(left.assessmentKey, right.assessmentKey));
|
|
100324
|
+
}
|
|
100325
|
+
function canonicalContributorIdentity(contributor) {
|
|
100326
|
+
const standard = canonicalReviewStandardRef(contributor.standard);
|
|
100327
|
+
if (!standard) {
|
|
100328
|
+
throw new ValidationError(`Mastery assessment ${contributor.qtiTestIdentifier} has an invalid standard`);
|
|
100329
|
+
}
|
|
100330
|
+
return {
|
|
100331
|
+
qtiTestIdentifier: contributor.qtiTestIdentifier,
|
|
100332
|
+
standard,
|
|
100333
|
+
standardKey: assessmentStandardRefKey2(standard)
|
|
100334
|
+
};
|
|
100335
|
+
}
|
|
100336
|
+
async function reviewBankContributorFingerprint(contributors) {
|
|
100337
|
+
const uniqueTuples = new Map;
|
|
100338
|
+
for (const contributor of contributors) {
|
|
100339
|
+
const { qtiTestIdentifier, standardKey } = canonicalContributorIdentity(contributor);
|
|
100340
|
+
const tuple3 = { qtiTestIdentifier, standardKey };
|
|
100341
|
+
uniqueTuples.set(canonicalJson2(tuple3), tuple3);
|
|
100342
|
+
}
|
|
100343
|
+
const digest = await sha256Hex(canonicalJson2([...uniqueTuples.values()].toSorted((left, right) => compareCodeUnits(left.standardKey, right.standardKey) || compareCodeUnits(left.qtiTestIdentifier, right.qtiTestIdentifier))));
|
|
100344
|
+
return `review-bank-contributors-v1:${digest}`;
|
|
100345
|
+
}
|
|
100346
|
+
function conflictWarning(input) {
|
|
100347
|
+
const retained = `${input.retainedStandard.framework} · ${input.retainedStandard.identifier}`;
|
|
100348
|
+
const skipped = `${input.skippedStandard.framework} · ${input.skippedStandard.identifier}`;
|
|
100349
|
+
return {
|
|
100350
|
+
code: "QUESTION_STANDARD_CONFLICT",
|
|
100351
|
+
...input,
|
|
100352
|
+
message: `Question ${input.itemIdentifier} is referenced by multiple mastery standards; retained ${retained} and skipped ${skipped} from ${input.skippedQtiTestIdentifier}.`
|
|
100353
|
+
};
|
|
100354
|
+
}
|
|
100355
|
+
async function prepareReviewBankSynchronization(input) {
|
|
100356
|
+
const contributors = canonicalSyncContributors(input.contributors);
|
|
100357
|
+
const selectedByItem = new Map;
|
|
100358
|
+
const warningKeys = new Set;
|
|
100359
|
+
const warnings = [];
|
|
100360
|
+
for (const contributor of contributors) {
|
|
100361
|
+
const itemIdentifiers2 = [
|
|
100362
|
+
...new Set(reviewBankTestItemIdentifiers(contributor.test))
|
|
100363
|
+
].toSorted();
|
|
100364
|
+
for (const itemIdentifier of itemIdentifiers2) {
|
|
100365
|
+
const selected = selectedByItem.get(itemIdentifier);
|
|
100366
|
+
if (!selected) {
|
|
100367
|
+
selectedByItem.set(itemIdentifier, {
|
|
100368
|
+
standard: contributor.standard,
|
|
100369
|
+
standardKey: contributor.standardKey
|
|
100370
|
+
});
|
|
100371
|
+
} else if (selected.standardKey !== contributor.standardKey) {
|
|
100372
|
+
const warningKey = `${itemIdentifier}\x00${contributor.standardKey}`;
|
|
100373
|
+
if (!warningKeys.has(warningKey)) {
|
|
100374
|
+
warningKeys.add(warningKey);
|
|
100375
|
+
warnings.push(conflictWarning({
|
|
100376
|
+
itemIdentifier,
|
|
100377
|
+
retainedStandard: selected.standard,
|
|
100378
|
+
skippedStandard: contributor.standard,
|
|
100379
|
+
skippedQtiTestIdentifier: contributor.qtiTestIdentifier
|
|
100380
|
+
}));
|
|
100381
|
+
}
|
|
100382
|
+
}
|
|
100383
|
+
}
|
|
100384
|
+
}
|
|
100385
|
+
const itemIdentifiers = [...selectedByItem.keys()].toSorted();
|
|
100386
|
+
const sourceContentRevision = input.contributorFingerprint ?? await reviewBankContributorFingerprint(contributors);
|
|
100387
|
+
const bank = await buildReviewBankIndexFromReferences({
|
|
100388
|
+
bankIdentifier: input.bankIdentifier,
|
|
100389
|
+
sourceContentRevision,
|
|
100390
|
+
items: itemIdentifiers.map((itemIdentifier) => ({
|
|
100391
|
+
itemIdentifier,
|
|
100392
|
+
standards: [selectedByItem.get(itemIdentifier).standard]
|
|
100393
|
+
}))
|
|
100394
|
+
});
|
|
100395
|
+
const manifest = await buildReviewBankManifest(bank);
|
|
100396
|
+
return {
|
|
100397
|
+
bankIdentifier: input.bankIdentifier,
|
|
100398
|
+
contributorFingerprint: sourceContentRevision,
|
|
100399
|
+
itemIdentifiers,
|
|
100400
|
+
manifest,
|
|
100401
|
+
warnings,
|
|
100402
|
+
summary: {
|
|
100403
|
+
itemCount: itemIdentifiers.length,
|
|
100404
|
+
standardCount: Object.keys(manifest.itemsByStandard).length,
|
|
100405
|
+
contributorCount: contributors.length,
|
|
100406
|
+
sourceFingerprint: manifest.sourceFingerprint
|
|
100407
|
+
}
|
|
100408
|
+
};
|
|
100409
|
+
}
|
|
100410
|
+
function reviewBankSinkMetadata(integrationId, plan) {
|
|
100411
|
+
return {
|
|
100412
|
+
ownerSystem: PLAYCADEMY_QTI_OWNER_SYSTEM,
|
|
100413
|
+
integrationId,
|
|
100414
|
+
[PLAYCADEMY_REVIEW_BANK_SINK_METADATA_KEY]: {
|
|
100415
|
+
version: PLAYCADEMY_REVIEW_BANK_SINK_VERSION,
|
|
100416
|
+
integrationId,
|
|
100417
|
+
contributorFingerprint: plan.contributorFingerprint,
|
|
100418
|
+
warnings: plan.warnings
|
|
100419
|
+
},
|
|
100420
|
+
[PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY]: plan.manifest
|
|
100421
|
+
};
|
|
100422
|
+
}
|
|
100423
|
+
function reviewBankSinkMarker(test) {
|
|
100424
|
+
const parsed = ReviewBankSinkMarkerSchema.safeParse(test.metadata?.[PLAYCADEMY_REVIEW_BANK_SINK_METADATA_KEY]);
|
|
100425
|
+
return parsed.success ? parsed.data : null;
|
|
100426
|
+
}
|
|
100427
|
+
function reviewBankSinkState(test, integrationId) {
|
|
100428
|
+
assertReviewBankSinkOwnership(test, integrationId);
|
|
100429
|
+
const marker = reviewBankSinkMarker(test);
|
|
100430
|
+
return {
|
|
100431
|
+
contributorFingerprint: marker?.contributorFingerprint ?? null,
|
|
100432
|
+
warnings: marker?.warnings ?? []
|
|
100433
|
+
};
|
|
100434
|
+
}
|
|
100435
|
+
async function unchangedReviewBankSynchronizationResult(input) {
|
|
100436
|
+
const sinkState = reviewBankSinkState(input.sink, input.integrationId);
|
|
100437
|
+
if (sinkState.contributorFingerprint !== input.contributorFingerprint) {
|
|
100438
|
+
return null;
|
|
100439
|
+
}
|
|
100440
|
+
const membershipItemIdentifiers = reviewBankTestItemIdentifiers(input.sink);
|
|
100441
|
+
const bank = await reviewBankIndexFromManifest(input.sink.metadata, {
|
|
100442
|
+
bankIdentifier: input.sink.identifier,
|
|
100443
|
+
membershipItemIdentifiers
|
|
100444
|
+
});
|
|
100445
|
+
const manifest = input.sink.metadata?.[PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY];
|
|
100446
|
+
if (!bank || typeof manifest?.sourceFingerprint !== "string") {
|
|
100447
|
+
return null;
|
|
100448
|
+
}
|
|
100449
|
+
const standardCount = new Set(bank.items.flatMap((item) => item.standards.map(assessmentStandardRefKey2))).size;
|
|
100450
|
+
return {
|
|
100451
|
+
qtiTestIdentifier: input.sink.identifier,
|
|
100452
|
+
itemCount: bank.items.length,
|
|
100453
|
+
standardCount,
|
|
100454
|
+
contributorCount: input.contributorCount,
|
|
100455
|
+
sourceFingerprint: manifest.sourceFingerprint,
|
|
100456
|
+
addedItemCount: 0,
|
|
100457
|
+
removedItemCount: 0,
|
|
100458
|
+
unchanged: true,
|
|
100459
|
+
warnings: sinkState.warnings
|
|
100460
|
+
};
|
|
100461
|
+
}
|
|
100462
|
+
function reviewBankSynchronizationDiff(input) {
|
|
100463
|
+
let previousMembershipReadable = true;
|
|
100464
|
+
let previousItemIdentifiers = [];
|
|
100465
|
+
if (input.existingSink) {
|
|
100466
|
+
try {
|
|
100467
|
+
previousItemIdentifiers = reviewBankTestItemIdentifiers(input.existingSink);
|
|
100468
|
+
} catch {
|
|
100469
|
+
previousMembershipReadable = false;
|
|
100470
|
+
}
|
|
100471
|
+
}
|
|
100472
|
+
const previousItems = new Set(previousItemIdentifiers);
|
|
100473
|
+
const desiredItems = new Set(input.plan.itemIdentifiers);
|
|
100474
|
+
const addedItemCount = input.plan.itemIdentifiers.filter((identifier) => !previousItems.has(identifier)).length;
|
|
100475
|
+
const removedItemCount = [...previousItems].filter((identifier) => !desiredItems.has(identifier)).length;
|
|
100476
|
+
const currentManifest = input.existingSink?.metadata?.[PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY];
|
|
100477
|
+
const currentContributorFingerprint = input.existingSink ? reviewBankSinkMarker(input.existingSink)?.contributorFingerprint ?? null : null;
|
|
100478
|
+
const canonicalMembership = previousItemIdentifiers.length === input.plan.itemIdentifiers.length && previousItemIdentifiers.every((identifier, index2) => identifier === input.plan.itemIdentifiers[index2]);
|
|
100479
|
+
const unchanged = input.existingSink !== null && previousMembershipReadable && canonicalMembership && canonicalJson2(currentManifest) === canonicalJson2(input.plan.manifest) && currentContributorFingerprint === input.plan.contributorFingerprint;
|
|
100480
|
+
return { addedItemCount, removedItemCount, unchanged };
|
|
100481
|
+
}
|
|
100482
|
+
function assertReviewBankSinkOwnership(test, integrationId) {
|
|
100483
|
+
const marker = reviewBankSinkMarker(test);
|
|
100484
|
+
if (marker?.integrationId !== integrationId || test.metadata?.ownerSystem !== PLAYCADEMY_QTI_OWNER_SYSTEM || test.identifier !== reviewBankSinkIdentifier(integrationId)) {
|
|
100485
|
+
throw new ValidationError(`QTI assessment ${test.identifier} is not the review-bank sink for this integration`);
|
|
100486
|
+
}
|
|
100487
|
+
}
|
|
100488
|
+
function buildReviewBankSinkInput(input) {
|
|
100489
|
+
return {
|
|
100490
|
+
...input.includeIdentifier ? { identifier: input.identifier } : {},
|
|
100491
|
+
title: input.title,
|
|
100492
|
+
metadata: input.metadata,
|
|
100493
|
+
"qti-test-part": [
|
|
100494
|
+
{
|
|
100495
|
+
identifier: `${input.identifier}-part1`,
|
|
100496
|
+
navigationMode: "linear",
|
|
100497
|
+
submissionMode: "individual",
|
|
100498
|
+
"qti-assessment-section": [
|
|
100499
|
+
{
|
|
100500
|
+
identifier: `${input.identifier}-section1`,
|
|
100501
|
+
title: "Questions",
|
|
100502
|
+
visible: true,
|
|
100503
|
+
required: true,
|
|
100504
|
+
fixed: false,
|
|
100505
|
+
sequence: 1,
|
|
100506
|
+
"qti-assessment-item-ref": input.itemIdentifiers.map((itemIdentifier, index2) => ({
|
|
100507
|
+
identifier: itemIdentifier,
|
|
100508
|
+
href: input.itemHref(itemIdentifier),
|
|
100509
|
+
sequence: index2 + 1
|
|
100510
|
+
}))
|
|
100511
|
+
}
|
|
100512
|
+
]
|
|
100513
|
+
}
|
|
100514
|
+
]
|
|
100515
|
+
};
|
|
100516
|
+
}
|
|
100027
100517
|
function reviewMappingIssueSummary(label, identifiers) {
|
|
100028
100518
|
if (identifiers.length === 0) {
|
|
100029
100519
|
return null;
|
|
@@ -100072,11 +100562,47 @@ async function prepareReviewMappingUpdate(test, questions) {
|
|
|
100072
100562
|
}
|
|
100073
100563
|
};
|
|
100074
100564
|
}
|
|
100565
|
+
var PLAYCADEMY_REVIEW_BANK_SINK_METADATA_KEY = "playcademyReviewBankSink", PLAYCADEMY_REVIEW_BANK_SINK_VERSION = 1, StoredSinkWarningSchema, ReviewBankSinkMarkerSchema;
|
|
100075
100566
|
var init_timeback_review_mapping_util = __esm(() => {
|
|
100567
|
+
init_esm();
|
|
100076
100568
|
init_assessment_runtime2();
|
|
100569
|
+
init_qti();
|
|
100570
|
+
init_timeback3();
|
|
100077
100571
|
init_errors2();
|
|
100078
100572
|
init_timeback_assessment_runtime_util();
|
|
100079
100573
|
init_timeback_qti_authoring_util();
|
|
100574
|
+
StoredSinkWarningSchema = exports_external.object({
|
|
100575
|
+
code: exports_external.literal("QUESTION_STANDARD_CONFLICT"),
|
|
100576
|
+
itemIdentifier: exports_external.string(),
|
|
100577
|
+
retainedStandard: exports_external.object({ framework: exports_external.string(), identifier: exports_external.string() }),
|
|
100578
|
+
skippedStandard: exports_external.object({ framework: exports_external.string(), identifier: exports_external.string() }),
|
|
100579
|
+
skippedQtiTestIdentifier: exports_external.string()
|
|
100580
|
+
});
|
|
100581
|
+
ReviewBankSinkMarkerSchema = exports_external.object({
|
|
100582
|
+
version: exports_external.literal(PLAYCADEMY_REVIEW_BANK_SINK_VERSION),
|
|
100583
|
+
integrationId: exports_external.string(),
|
|
100584
|
+
contributorFingerprint: exports_external.string().nullable().catch(null),
|
|
100585
|
+
warnings: exports_external.array(StoredSinkWarningSchema).nullable().catch(null)
|
|
100586
|
+
}).transform((marker) => {
|
|
100587
|
+
if (!marker.contributorFingerprint || !marker.warnings) {
|
|
100588
|
+
return { ...marker, contributorFingerprint: null, warnings: [] };
|
|
100589
|
+
}
|
|
100590
|
+
const warnings = [];
|
|
100591
|
+
for (const warning of marker.warnings) {
|
|
100592
|
+
const retained = canonicalReviewStandardRef(warning.retainedStandard);
|
|
100593
|
+
const skipped = canonicalReviewStandardRef(warning.skippedStandard);
|
|
100594
|
+
if (!retained || !skipped) {
|
|
100595
|
+
return { ...marker, contributorFingerprint: null, warnings: [] };
|
|
100596
|
+
}
|
|
100597
|
+
warnings.push(conflictWarning({
|
|
100598
|
+
itemIdentifier: warning.itemIdentifier,
|
|
100599
|
+
retainedStandard: retained,
|
|
100600
|
+
skippedStandard: skipped,
|
|
100601
|
+
skippedQtiTestIdentifier: warning.skippedQtiTestIdentifier
|
|
100602
|
+
}));
|
|
100603
|
+
}
|
|
100604
|
+
return { ...marker, warnings };
|
|
100605
|
+
});
|
|
100080
100606
|
});
|
|
100081
100607
|
|
|
100082
100608
|
// ../api-core/src/utils/timeback-assessment-publication.util.ts
|
|
@@ -100380,11 +100906,6 @@ function assertAssessmentHasQuestions(questions) {
|
|
|
100380
100906
|
throw new ValidationError("An assessment must contain at least one question to publish");
|
|
100381
100907
|
}
|
|
100382
100908
|
}
|
|
100383
|
-
function assertReviewAssessmentHasStandards(standardCounts) {
|
|
100384
|
-
if (standardCounts.some((count) => count !== 1)) {
|
|
100385
|
-
throw new ValidationError("Every question in a review assessment must have exactly one standards alignment");
|
|
100386
|
-
}
|
|
100387
|
-
}
|
|
100388
100909
|
function planAssessmentRemoval(status) {
|
|
100389
100910
|
if (status === "draft") {
|
|
100390
100911
|
return { kind: "delete", action: "discarded", operation: "discard_draft" };
|
|
@@ -100447,6 +100968,9 @@ var init_timeback_assessment_rules_util = __esm(() => {
|
|
|
100447
100968
|
});
|
|
100448
100969
|
|
|
100449
100970
|
// ../api-core/src/utils/timeback-qti-hydration.util.ts
|
|
100971
|
+
function qtiItemHref(client2, itemIdentifier) {
|
|
100972
|
+
return `${client2.getQtiBaseUrl()}/assessment-items/${itemIdentifier}`;
|
|
100973
|
+
}
|
|
100450
100974
|
async function hydrateQtiTestQuestions(client2, references) {
|
|
100451
100975
|
const questions = await runWithConcurrency(references.questions, QTI_HYDRATION_CONCURRENCY, async (reference) => ({
|
|
100452
100976
|
...reference,
|
|
@@ -100469,6 +100993,28 @@ async function hydrateQtiTestQuestionSelection(client2, references, itemIdentifi
|
|
|
100469
100993
|
questions: selectedReferences
|
|
100470
100994
|
});
|
|
100471
100995
|
}
|
|
100996
|
+
async function hydrateQtiItemSelection(client2, test, itemIdentifiers) {
|
|
100997
|
+
const testPart = qtiTestParts(test)[0];
|
|
100998
|
+
const section = testPart?.["qti-assessment-section"][0];
|
|
100999
|
+
if (!testPart || !section) {
|
|
101000
|
+
throw new Error(`QTI assessment ${test.identifier} has no section to reference items from`);
|
|
101001
|
+
}
|
|
101002
|
+
const questions = await runWithConcurrency(itemIdentifiers, QTI_HYDRATION_CONCURRENCY, async (itemIdentifier) => ({
|
|
101003
|
+
reference: {
|
|
101004
|
+
identifier: itemIdentifier,
|
|
101005
|
+
href: qtiItemHref(client2, itemIdentifier),
|
|
101006
|
+
testPart: testPart.identifier,
|
|
101007
|
+
section: section.identifier
|
|
101008
|
+
},
|
|
101009
|
+
question: await client2.qtiApi.assessmentItems.get(itemIdentifier)
|
|
101010
|
+
}));
|
|
101011
|
+
return {
|
|
101012
|
+
assessmentTest: test.identifier,
|
|
101013
|
+
title: test.title,
|
|
101014
|
+
totalQuestions: questions.length,
|
|
101015
|
+
questions
|
|
101016
|
+
};
|
|
101017
|
+
}
|
|
100472
101018
|
async function loadQtiTestReferences(client2, identifier) {
|
|
100473
101019
|
const [test, references] = await Promise.all([
|
|
100474
101020
|
client2.qtiApi.assessmentTests.get(identifier),
|
|
@@ -100484,7 +101030,9 @@ async function loadHydratedQtiTest(client2, identifier) {
|
|
|
100484
101030
|
};
|
|
100485
101031
|
}
|
|
100486
101032
|
var QTI_HYDRATION_CONCURRENCY = 8;
|
|
100487
|
-
var init_timeback_qti_hydration_util = () => {
|
|
101033
|
+
var init_timeback_qti_hydration_util = __esm(() => {
|
|
101034
|
+
init_timeback_qti_authoring_util();
|
|
101035
|
+
});
|
|
100488
101036
|
|
|
100489
101037
|
// ../api-core/src/services/timeback-assessment-runtime.service.ts
|
|
100490
101038
|
var TimebackAssessmentRuntimeService;
|
|
@@ -100514,8 +101062,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
100514
101062
|
static EXPORT_CONCURRENCY = 4;
|
|
100515
101063
|
static ITEM_SUBMISSION_MAX_PREPARATION_ATTEMPTS = 3;
|
|
100516
101064
|
static ASSESSMENT_VERSION_CONFIRMATION_DELAY_MS = 25;
|
|
100517
|
-
static PROJECTION_BARRIER_MAX_ATTEMPTS = 5;
|
|
100518
|
-
static PROJECTION_BARRIER_RETRY_DELAY_MS = 25;
|
|
100519
101065
|
static SCORING_CONCURRENCY = 4;
|
|
100520
101066
|
static STALE_AWARD_AGE_MS = 15 * 60 * 1000;
|
|
100521
101067
|
deps;
|
|
@@ -101058,15 +101604,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
101058
101604
|
this.assertInProgress(attempt.result);
|
|
101059
101605
|
if (!preview || preview.responseVersion !== attempt.metadata.responseVersion || preview.submissionId !== input.submissionId || preview.itemIdentifier !== input.itemIdentifier) {
|
|
101060
101606
|
if (preparationAttempts >= TimebackAssessmentRuntimeService.ITEM_SUBMISSION_MAX_PREPARATION_ATTEMPTS) {
|
|
101061
|
-
|
|
101062
|
-
throw new ServiceUnavailableError("The assessment response state changed repeatedly while this item was being prepared. Try again shortly.", {
|
|
101063
|
-
retryable: true,
|
|
101064
|
-
reason: "PREPARATION_CONTENTION",
|
|
101065
|
-
expectedResponseVersion: input.expectedResponseVersion,
|
|
101066
|
-
responseVersion: attempt.metadata.responseVersion
|
|
101067
|
-
});
|
|
101068
|
-
}
|
|
101069
|
-
throw AssessmentRuntimeError.from(assessmentResponseVersionConflict(input.expectedResponseVersion, attempt.metadata.responseVersion));
|
|
101607
|
+
throw assessmentPreparationContentionError(input.expectedResponseVersion, attempt.metadata.responseVersion, "assessment");
|
|
101070
101608
|
}
|
|
101071
101609
|
return { action: "prepare" };
|
|
101072
101610
|
}
|
|
@@ -101189,15 +101727,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
101189
101727
|
this.assertInProgress(attempt.result);
|
|
101190
101728
|
if (!preview || preview.responseVersion !== metadata2.responseVersion || preview.submissionId !== input.submissionId || preview.routingNodeKey !== input.routingNodeKey || preview.itemIdentifier !== input.itemIdentifier) {
|
|
101191
101729
|
if (preparationAttempts >= TimebackAssessmentRuntimeService.ITEM_SUBMISSION_MAX_PREPARATION_ATTEMPTS) {
|
|
101192
|
-
|
|
101193
|
-
throw new ServiceUnavailableError("The diagnostic response state changed repeatedly while this item was being prepared. Try again shortly.", {
|
|
101194
|
-
retryable: true,
|
|
101195
|
-
reason: "PREPARATION_CONTENTION",
|
|
101196
|
-
expectedResponseVersion: input.expectedResponseVersion,
|
|
101197
|
-
responseVersion: metadata2.responseVersion
|
|
101198
|
-
});
|
|
101199
|
-
}
|
|
101200
|
-
throw AssessmentRuntimeError.from(assessmentResponseVersionConflict(input.expectedResponseVersion, metadata2.responseVersion));
|
|
101730
|
+
throw assessmentPreparationContentionError(input.expectedResponseVersion, metadata2.responseVersion, "diagnostic");
|
|
101201
101731
|
}
|
|
101202
101732
|
return { action: "prepare" };
|
|
101203
101733
|
}
|
|
@@ -101212,7 +101742,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
101212
101742
|
itemIdentifier: input.itemIdentifier
|
|
101213
101743
|
}));
|
|
101214
101744
|
}
|
|
101215
|
-
const responses =
|
|
101745
|
+
const responses = prepareDiagnosticAssessmentResponses(preview.assessment, metadata2.responses, input);
|
|
101216
101746
|
const scoring = preview.scoring;
|
|
101217
101747
|
if (!scoring || typeof scoring.isCorrect !== "boolean") {
|
|
101218
101748
|
throw AssessmentRuntimeError.from(assessmentFlowViolation(`Diagnostic item ${input.itemIdentifier} did not produce determinate binary grading.`, { itemIdentifier: input.itemIdentifier }));
|
|
@@ -101285,22 +101815,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
101285
101815
|
answered: submission.answered
|
|
101286
101816
|
};
|
|
101287
101817
|
}
|
|
101288
|
-
prepareDiagnosticResponses(assessment, current, input) {
|
|
101289
|
-
const update2 = { [input.itemIdentifier]: input.responses };
|
|
101290
|
-
validateAssessmentResponseUpdate(assessment, update2);
|
|
101291
|
-
const responses = applyAssessmentResponseUpdate(current, update2);
|
|
101292
|
-
const item = assessment.items.find((candidate) => candidate.identifier === input.itemIdentifier);
|
|
101293
|
-
const itemResponses = responses[input.itemIdentifier];
|
|
101294
|
-
const missingResponse = item?.interactions.find((interaction) => itemResponses?.[interaction.responseIdentifier] === undefined);
|
|
101295
|
-
if (!item || !itemResponses || missingResponse) {
|
|
101296
|
-
throw new AssessmentRuntimeError("INVALID_RESPONSE", `Diagnostic item ${input.itemIdentifier} requires a response for every interaction.`, {
|
|
101297
|
-
itemIdentifier: input.itemIdentifier,
|
|
101298
|
-
...missingResponse ? { responseIdentifier: missingResponse.responseIdentifier } : {}
|
|
101299
|
-
});
|
|
101300
|
-
}
|
|
101301
|
-
validateAssessmentResponses(assessment, { [input.itemIdentifier]: itemResponses });
|
|
101302
|
-
return responses;
|
|
101303
|
-
}
|
|
101304
101818
|
async prepareDiagnosticItemSubmission(params, input) {
|
|
101305
101819
|
try {
|
|
101306
101820
|
return await this.scoreDiagnosticItemSubmission(params, input);
|
|
@@ -101334,7 +101848,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
101334
101848
|
const expected = loaded.state.next;
|
|
101335
101849
|
let scoring = null;
|
|
101336
101850
|
if (loaded.state.status === "in-progress" && expected?.nodeKey === input.routingNodeKey && expected.itemIdentifier === input.itemIdentifier && attempt.metadata.responseVersion === input.expectedResponseVersion) {
|
|
101337
|
-
const responses =
|
|
101851
|
+
const responses = prepareDiagnosticAssessmentResponses(assessment, attempt.metadata.responses, input);
|
|
101338
101852
|
scoring = await this.scoreItem(assessment, responses, input.itemIdentifier);
|
|
101339
101853
|
}
|
|
101340
101854
|
return {
|
|
@@ -101605,11 +102119,14 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
101605
102119
|
inProgress: attempt.result.inProgress ?? "",
|
|
101606
102120
|
scoreStatus: attempt.result.scoreStatus,
|
|
101607
102121
|
submissionId: attempt.metadata.submissionId,
|
|
101608
|
-
|
|
102122
|
+
award: assessmentAwardFromResult(attempt.result, attempt.metadata) ?? undefined
|
|
101609
102123
|
}, input);
|
|
101610
102124
|
if (disposition === "replay") {
|
|
101611
102125
|
return {
|
|
101612
|
-
response:
|
|
102126
|
+
response: buildCompletedAssessmentSubmitResult({
|
|
102127
|
+
result: attempt.result,
|
|
102128
|
+
metadata: attempt.metadata
|
|
102129
|
+
}),
|
|
101613
102130
|
completion: this.replayCompletion(attempt, input.submissionId, params, game2)
|
|
101614
102131
|
};
|
|
101615
102132
|
}
|
|
@@ -101617,20 +102134,27 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
101617
102134
|
throw AssessmentRuntimeError.from(assessmentAwardConflict({
|
|
101618
102135
|
attemptId: attempt.result.sourcedId,
|
|
101619
102136
|
submissionId: input.submissionId,
|
|
101620
|
-
xpAwarded: input.xpAwarded
|
|
102137
|
+
xpAwarded: input.xpAwarded,
|
|
102138
|
+
...input.masteredUnits !== undefined ? { masteredUnits: input.masteredUnits } : {},
|
|
102139
|
+
...input.masteredUnitsAbsolute !== undefined ? { masteredUnitsAbsolute: input.masteredUnitsAbsolute } : {}
|
|
101621
102140
|
}));
|
|
101622
102141
|
}
|
|
101623
102142
|
if (disposition === "reject") {
|
|
101624
102143
|
throw AssessmentRuntimeError.from(assessmentAlreadySubmitted(attempt.result.sourcedId));
|
|
101625
102144
|
}
|
|
101626
|
-
const
|
|
101627
|
-
const
|
|
102145
|
+
const mastery = await this.evaluateMasteryAward(attempt, params.studentId, input);
|
|
102146
|
+
const awardRecord = buildAssessmentAwardRecord(input, mastery);
|
|
102147
|
+
const finalization = buildAssessmentResultAwardFinalization({
|
|
101628
102148
|
result: attempt.result,
|
|
101629
102149
|
metadata: attempt.metadata,
|
|
101630
|
-
|
|
102150
|
+
award: awardRecord,
|
|
101631
102151
|
appName: game2.appName,
|
|
101632
102152
|
timestamp: new Date().toISOString()
|
|
101633
102153
|
});
|
|
102154
|
+
const response = buildCompletedAssessmentSubmitResult({
|
|
102155
|
+
result: attempt.result,
|
|
102156
|
+
metadata: finalization.metadata
|
|
102157
|
+
});
|
|
101634
102158
|
const finalized = await this.requireClient().api.oneroster.assessmentResults.upsert(attempt.result.sourcedId, finalization.resultUpdate);
|
|
101635
102159
|
const finalizedForEmission = {
|
|
101636
102160
|
...finalized,
|
|
@@ -101649,25 +102173,78 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
101649
102173
|
"app.assessment.submission_id": input.submissionId,
|
|
101650
102174
|
"app.assessment.purpose": finalization.metadata.purpose,
|
|
101651
102175
|
"app.assessment.award_age_ms": this.awardAgeMs(attempt.result.scoreDate),
|
|
101652
|
-
"app.assessment.xp_awarded": input.xpAwarded
|
|
102176
|
+
"app.assessment.xp_awarded": input.xpAwarded,
|
|
102177
|
+
"app.assessment.mastered_units_applied": mastery.masteredUnitsApplied,
|
|
102178
|
+
"app.assessment.mastery_achieved": mastery.masteryAchieved,
|
|
102179
|
+
"app.assessment.mastery_revoked": mastery.masteryRevoked
|
|
101653
102180
|
});
|
|
102181
|
+
await this.settleMasteryAward(finalizedAttempt, mastery, input.submissionId, game2);
|
|
101654
102182
|
return {
|
|
101655
|
-
response
|
|
101656
|
-
...submitted,
|
|
101657
|
-
status: "completed",
|
|
101658
|
-
xpAwarded: input.xpAwarded
|
|
101659
|
-
},
|
|
102183
|
+
response,
|
|
101660
102184
|
completion: this.replayCompletion(finalizedAttempt, input.submissionId, params, game2)
|
|
101661
102185
|
};
|
|
101662
102186
|
});
|
|
101663
102187
|
if (award.completion) {
|
|
101664
102188
|
await this.emitCompletionBestEffort({
|
|
101665
102189
|
...award.completion,
|
|
101666
|
-
|
|
102190
|
+
award: award.response
|
|
101667
102191
|
});
|
|
101668
102192
|
}
|
|
101669
102193
|
return award.response;
|
|
101670
102194
|
}
|
|
102195
|
+
async evaluateMasteryAward(attempt, studentIdentifier, input) {
|
|
102196
|
+
if (!hasMasteryAwardRequest(input)) {
|
|
102197
|
+
return NO_MASTERY_AWARD;
|
|
102198
|
+
}
|
|
102199
|
+
const student = await this.requireClient().roster.resolveStudent(studentIdentifier);
|
|
102200
|
+
return this.requireClient().activity.evaluateMasteryAward({
|
|
102201
|
+
studentId: student.id,
|
|
102202
|
+
courseId: attempt.integration.courseId,
|
|
102203
|
+
...input.masteredUnits !== undefined ? { masteredUnits: input.masteredUnits } : {},
|
|
102204
|
+
...input.masteredUnitsAbsolute !== undefined ? { masteredUnitsAbsolute: input.masteredUnitsAbsolute } : {}
|
|
102205
|
+
});
|
|
102206
|
+
}
|
|
102207
|
+
async settleMasteryAward(attempt, mastery, submissionId, game2) {
|
|
102208
|
+
if (!mastery.masteryAchieved && !mastery.masteryRevoked) {
|
|
102209
|
+
return;
|
|
102210
|
+
}
|
|
102211
|
+
const integration = attempt.integration;
|
|
102212
|
+
const course = this.trackableCourse(integration, "assessment.mastery_settlement_invalid_course_metadata", { "app.assessment.attempt_id": attempt.result.sourcedId });
|
|
102213
|
+
if (!course) {
|
|
102214
|
+
setAttributes({ "app.assessment.mastery_completion_entry_missing": true });
|
|
102215
|
+
return;
|
|
102216
|
+
}
|
|
102217
|
+
try {
|
|
102218
|
+
const student = await this.requireClient().roster.resolveStudent(attempt.result.student.sourcedId);
|
|
102219
|
+
const completionHistoryEventId = `urn:uuid:${await deterministicUUID([
|
|
102220
|
+
"playcademy-assessment-course-completed-event-v1",
|
|
102221
|
+
attempt.result.sourcedId,
|
|
102222
|
+
submissionId
|
|
102223
|
+
].join("\x00"))}`;
|
|
102224
|
+
const settlement = await this.requireClient().activity.settleMasteryAward(mastery, {
|
|
102225
|
+
studentId: student.id,
|
|
102226
|
+
studentEmail: student.email,
|
|
102227
|
+
courseId: integration.courseId,
|
|
102228
|
+
gameId: integration.gameId,
|
|
102229
|
+
activityId: attempt.metadata.activityId,
|
|
102230
|
+
courseName: attempt.metadata.completion?.courseName ?? "Game Course",
|
|
102231
|
+
subject: course.subject,
|
|
102232
|
+
appName: game2.appName,
|
|
102233
|
+
sensorUrl: game2.sensorUrl,
|
|
102234
|
+
completionHistoryEventId
|
|
102235
|
+
});
|
|
102236
|
+
if (mastery.masteryAchieved && !settlement.completionEntryWritten) {
|
|
102237
|
+
setAttributes({ "app.assessment.mastery_completion_entry_missing": true });
|
|
102238
|
+
}
|
|
102239
|
+
} catch (error88) {
|
|
102240
|
+
setAttributes({ "app.assessment.mastery_completion_entry_missing": true });
|
|
102241
|
+
addEvent("assessment.mastery_settlement_failed", {
|
|
102242
|
+
"app.assessment.attempt_id": attempt.result.sourcedId,
|
|
102243
|
+
"exception.type": errorType(error88),
|
|
102244
|
+
"app.error.message": errorMessage(error88)
|
|
102245
|
+
});
|
|
102246
|
+
}
|
|
102247
|
+
}
|
|
101671
102248
|
async finalizeDiagnosticSubmission(input) {
|
|
101672
102249
|
const { attempt } = input;
|
|
101673
102250
|
const routing = await this.loadAttemptDiagnosticManifest(attempt.metadata, input.db);
|
|
@@ -102114,9 +102691,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
102114
102691
|
if (this.isSettled(resumed.result)) {
|
|
102115
102692
|
return this.snapshotForResult(resumed.result, resumed.metadata, context2.integration);
|
|
102116
102693
|
}
|
|
102117
|
-
const
|
|
102118
|
-
const currentSource = await this.hydrateReviewBankSelection(catalog, resumed.metadata.review.selections.map((selection) => selection.itemIdentifier));
|
|
102119
|
-
const source = this.pinnedReviewBankSource(currentSource, resumed.metadata);
|
|
102694
|
+
const source = await this.loadPinnedReviewBankSource(resumed.metadata);
|
|
102120
102695
|
const assessment = projectReviewAssessment(source.assessment, source.bank, resumed.metadata.review.selections);
|
|
102121
102696
|
await this.ensureReviewChildResults({
|
|
102122
102697
|
result: resumed.result,
|
|
@@ -102160,7 +102735,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
102160
102735
|
membershipItemIdentifiers: loaded.references.questions.map((question) => question.reference.identifier)
|
|
102161
102736
|
});
|
|
102162
102737
|
} catch (error88) {
|
|
102163
|
-
throw new ServiceUnavailableError(`Review bank ${identifier} has stale or invalid routing metadata. Use
|
|
102738
|
+
throw new ServiceUnavailableError(`Review bank ${identifier} has stale or invalid routing metadata. Use Sync Review Bank in assessment authoring.`, { reason: errorMessage(error88) });
|
|
102164
102739
|
}
|
|
102165
102740
|
if (!bank) {
|
|
102166
102741
|
throw new ServiceUnavailableError(`Review bank ${identifier} needs its authoring mapping updated before it can serve review questions.`);
|
|
@@ -102176,28 +102751,60 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
102176
102751
|
this.reviewBankCache.set(`${identifier}\x00${bank.sourceContentRevision}`, catalog);
|
|
102177
102752
|
return catalog;
|
|
102178
102753
|
}
|
|
102179
|
-
async
|
|
102180
|
-
const cacheKey2 = [
|
|
102181
|
-
catalog.test.identifier,
|
|
102182
|
-
catalog.bank.sourceContentRevision,
|
|
102183
|
-
...itemIdentifiers
|
|
102184
|
-
].join("\x00");
|
|
102754
|
+
async cachedReviewSelection(input) {
|
|
102755
|
+
const cacheKey2 = [input.identifier, input.contentRevision, ...input.itemIdentifiers].join("\x00");
|
|
102185
102756
|
const cached3 = this.reviewSelectionCache.get(cacheKey2);
|
|
102186
102757
|
if (cached3) {
|
|
102187
|
-
return
|
|
102758
|
+
return cached3;
|
|
102188
102759
|
}
|
|
102189
|
-
const questions = await
|
|
102190
|
-
const assessment = await buildPlayableAssessment(
|
|
102191
|
-
assessment.contentRevision =
|
|
102760
|
+
const { test, questions } = await input.hydrate();
|
|
102761
|
+
const assessment = await buildPlayableAssessment(test, questions);
|
|
102762
|
+
assessment.contentRevision = input.contentRevision;
|
|
102192
102763
|
this.reviewSelectionCache.set(cacheKey2, assessment);
|
|
102764
|
+
return assessment;
|
|
102765
|
+
}
|
|
102766
|
+
async hydrateReviewBankSelection(catalog, itemIdentifiers) {
|
|
102767
|
+
const assessment = await this.cachedReviewSelection({
|
|
102768
|
+
identifier: catalog.test.identifier,
|
|
102769
|
+
contentRevision: catalog.bank.sourceContentRevision,
|
|
102770
|
+
itemIdentifiers,
|
|
102771
|
+
hydrate: async () => ({
|
|
102772
|
+
test: catalog.test,
|
|
102773
|
+
questions: await hydrateQtiTestQuestionSelection(this.requireClient(), catalog.references, itemIdentifiers)
|
|
102774
|
+
})
|
|
102775
|
+
});
|
|
102193
102776
|
return { assessment, bank: catalog.bank };
|
|
102194
102777
|
}
|
|
102195
|
-
|
|
102778
|
+
async loadPinnedReviewBankSource(metadata2) {
|
|
102779
|
+
const itemIdentifiers = metadata2.review.selections.map((selection) => selection.itemIdentifier);
|
|
102780
|
+
let assessment;
|
|
102781
|
+
try {
|
|
102782
|
+
assessment = await this.cachedReviewSelection({
|
|
102783
|
+
identifier: metadata2.selectedTest.identifier,
|
|
102784
|
+
contentRevision: metadata2.selectedTest.contentRevision,
|
|
102785
|
+
itemIdentifiers,
|
|
102786
|
+
hydrate: async () => {
|
|
102787
|
+
const test = await this.requireClient().qtiApi.assessmentTests.get(metadata2.selectedTest.identifier);
|
|
102788
|
+
return {
|
|
102789
|
+
test,
|
|
102790
|
+
questions: await hydrateQtiItemSelection(this.requireClient(), test, itemIdentifiers)
|
|
102791
|
+
};
|
|
102792
|
+
}
|
|
102793
|
+
});
|
|
102794
|
+
} catch (error88) {
|
|
102795
|
+
if (isApiError(error88) && error88.statusCode === 404) {
|
|
102796
|
+
throw new AssessmentRuntimeError("SELECTED_TEST_VERSION_UNAVAILABLE", `A selected review item for ${metadata2.selectedTest.identifier} is no longer available.`, {
|
|
102797
|
+
identifier: metadata2.selectedTest.identifier,
|
|
102798
|
+
expectedRevision: metadata2.selectedTest.contentRevision
|
|
102799
|
+
});
|
|
102800
|
+
}
|
|
102801
|
+
throw error88;
|
|
102802
|
+
}
|
|
102196
102803
|
return {
|
|
102197
|
-
assessment
|
|
102804
|
+
assessment,
|
|
102198
102805
|
bank: {
|
|
102199
|
-
bankIdentifier:
|
|
102200
|
-
sourceContentRevision:
|
|
102806
|
+
bankIdentifier: assessment.identifier,
|
|
102807
|
+
sourceContentRevision: metadata2.selectedTest.contentRevision,
|
|
102201
102808
|
bankRevision: metadata2.review.bankRevision,
|
|
102202
102809
|
items: metadata2.review.selections.map((selection) => ({
|
|
102203
102810
|
itemIdentifier: selection.itemIdentifier,
|
|
@@ -102221,9 +102828,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
102221
102828
|
if (metadata2.purpose !== "review") {
|
|
102222
102829
|
return this.loadAssessment(metadata2.selectedTest.identifier, metadata2.selectedTest.contentRevision);
|
|
102223
102830
|
}
|
|
102224
|
-
const
|
|
102225
|
-
const currentSource = await this.hydrateReviewBankSelection(catalog, metadata2.review.selections.map((selection) => selection.itemIdentifier));
|
|
102226
|
-
const source = this.pinnedReviewBankSource(currentSource, metadata2);
|
|
102831
|
+
const source = await this.loadPinnedReviewBankSource(metadata2);
|
|
102227
102832
|
return projectReviewAssessment(source.assessment, source.bank, metadata2.review.selections);
|
|
102228
102833
|
}
|
|
102229
102834
|
isPlatformRoutedDiagnosticMetadata(metadata2) {
|
|
@@ -102709,7 +103314,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
102709
103314
|
async projectReviewItemResponse(params, projection) {
|
|
102710
103315
|
await this.putReviewChildResponse(projection.result, projection.metadata, projection.itemIdentifier);
|
|
102711
103316
|
try {
|
|
102712
|
-
await this.
|
|
103317
|
+
await crossAssessmentAttemptLockBarrier(this.deps.assessmentRuntimeLock, this.deps.db, params.attemptId);
|
|
102713
103318
|
const latest = await this.peekAttempt(params);
|
|
102714
103319
|
if (this.isSettled(latest.result) && latest.metadata.purpose === "review") {
|
|
102715
103320
|
await this.restoreCompletedReviewChildResponses(latest.result, latest.metadata, new Set([projection.itemIdentifier]));
|
|
@@ -102723,21 +103328,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
102723
103328
|
});
|
|
102724
103329
|
}
|
|
102725
103330
|
}
|
|
102726
|
-
async crossAttemptLockBarrier(attemptId) {
|
|
102727
|
-
for (let attempt = 1;attempt <= TimebackAssessmentRuntimeService.PROJECTION_BARRIER_MAX_ATTEMPTS; attempt += 1) {
|
|
102728
|
-
try {
|
|
102729
|
-
await this.deps.assessmentRuntimeLock(this.deps.db, [assessmentRuntimeAttemptLockKey(attemptId)], async () => {
|
|
102730
|
-
return;
|
|
102731
|
-
});
|
|
102732
|
-
return;
|
|
102733
|
-
} catch (error88) {
|
|
102734
|
-
if (!(error88 instanceof ServiceUnavailableError) || attempt === TimebackAssessmentRuntimeService.PROJECTION_BARRIER_MAX_ATTEMPTS) {
|
|
102735
|
-
throw error88;
|
|
102736
|
-
}
|
|
102737
|
-
await sleep(TimebackAssessmentRuntimeService.PROJECTION_BARRIER_RETRY_DELAY_MS);
|
|
102738
|
-
}
|
|
102739
|
-
}
|
|
102740
|
-
}
|
|
102741
103331
|
async restoreCompletedReviewChildResponses(result, metadata2, changedItemIdentifiers) {
|
|
102742
103332
|
const outcomes = metadata2.completion?.itemOutcomes;
|
|
102743
103333
|
const submissionId = metadata2.submissionId;
|
|
@@ -102883,7 +103473,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
102883
103473
|
attemptId: result.sourcedId,
|
|
102884
103474
|
responseVersion: metadata2.responseVersion,
|
|
102885
103475
|
status: "completed",
|
|
102886
|
-
result:
|
|
103476
|
+
result: buildCompletedAssessmentSubmitResult({ result, metadata: metadata2 })
|
|
102887
103477
|
};
|
|
102888
103478
|
}
|
|
102889
103479
|
const assessment = await this.loadAttemptAssessment(metadata2);
|
|
@@ -102900,58 +103490,37 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
102900
103490
|
return this.snapshot(result, metadata2, assessment, integration);
|
|
102901
103491
|
}
|
|
102902
103492
|
diagnosticSnapshot(result, metadata2, assessment, integration, state) {
|
|
102903
|
-
|
|
102904
|
-
return {
|
|
103493
|
+
return buildDiagnosticAssessmentSnapshot({
|
|
102905
103494
|
attemptId: result.sourcedId,
|
|
102906
|
-
|
|
102907
|
-
|
|
102908
|
-
|
|
102909
|
-
|
|
102910
|
-
|
|
102911
|
-
selection: {
|
|
102912
|
-
kind: "platform-routed-diagnostic",
|
|
102913
|
-
purpose: "diagnostic",
|
|
102914
|
-
definitionId: metadata2.diagnostic.definitionId,
|
|
102915
|
-
diagnosticKey: metadata2.diagnostic.diagnosticKey,
|
|
102916
|
-
routingRevision: metadata2.diagnostic.routingRevision
|
|
102917
|
-
},
|
|
102918
|
-
routing: this.diagnosticRoutingSnapshot(metadata2, state),
|
|
102919
|
-
completion: null
|
|
102920
|
-
};
|
|
103495
|
+
metadata: metadata2,
|
|
103496
|
+
assessment,
|
|
103497
|
+
state,
|
|
103498
|
+
activityData: this.activityData(metadata2, assessment, integration)
|
|
103499
|
+
});
|
|
102921
103500
|
}
|
|
102922
103501
|
diagnosticRoutingSnapshot(metadata2, state) {
|
|
102923
103502
|
return projectDiagnosticRoutingSnapshot(metadata2.diagnostic.routingRevision, state);
|
|
102924
103503
|
}
|
|
102925
103504
|
snapshot(result, metadata2, assessment, integration) {
|
|
102926
|
-
|
|
102927
|
-
const activityData = this.activityData(metadata2, assessment, integration);
|
|
102928
|
-
return {
|
|
103505
|
+
return buildConventionalAssessmentSnapshot({
|
|
102929
103506
|
attemptId: result.sourcedId,
|
|
102930
|
-
|
|
102931
|
-
|
|
102932
|
-
|
|
102933
|
-
|
|
102934
|
-
assessment: assessmentPresentationForAttempt(assessment, result.sourcedId),
|
|
102935
|
-
responses: metadata2.responses,
|
|
102936
|
-
itemSubmissions: metadata2.itemSubmissions,
|
|
102937
|
-
score: null,
|
|
102938
|
-
selection
|
|
102939
|
-
};
|
|
103507
|
+
metadata: metadata2,
|
|
103508
|
+
assessment,
|
|
103509
|
+
activityData: this.activityData(metadata2, assessment, integration)
|
|
103510
|
+
});
|
|
102940
103511
|
}
|
|
102941
103512
|
activityData(metadata2, assessment, integration) {
|
|
102942
103513
|
const course = this.trackableCourse(integration, "assessment.activity_tracking_invalid_course_metadata");
|
|
102943
|
-
|
|
102944
|
-
return;
|
|
102945
|
-
}
|
|
102946
|
-
return {
|
|
103514
|
+
return buildAssessmentActivityData({
|
|
102947
103515
|
activityId: metadata2.activityId,
|
|
102948
103516
|
activityName: assessment.title,
|
|
102949
|
-
|
|
102950
|
-
|
|
102951
|
-
};
|
|
103517
|
+
courseId: integration.courseId,
|
|
103518
|
+
course
|
|
103519
|
+
});
|
|
102952
103520
|
}
|
|
102953
103521
|
trackableCourse(integration, event, attributes2 = {}) {
|
|
102954
|
-
|
|
103522
|
+
const course = assessmentTrackableCourse(integration);
|
|
103523
|
+
if (!course) {
|
|
102955
103524
|
addEvent(event, {
|
|
102956
103525
|
...attributes2,
|
|
102957
103526
|
"app.timeback.integration_id": integration.id,
|
|
@@ -102960,27 +103529,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
102960
103529
|
});
|
|
102961
103530
|
return null;
|
|
102962
103531
|
}
|
|
102963
|
-
return
|
|
102964
|
-
}
|
|
102965
|
-
selectionContext(metadata2) {
|
|
102966
|
-
if (metadata2.purpose === "review") {
|
|
102967
|
-
return {
|
|
102968
|
-
kind: "standards-review",
|
|
102969
|
-
purpose: metadata2.purpose,
|
|
102970
|
-
standards: [...metadata2.review.standards],
|
|
102971
|
-
candidateItemsPerStandard: metadata2.review.candidateItemsPerStandard,
|
|
102972
|
-
selections: [...metadata2.review.selections],
|
|
102973
|
-
fulfillment: metadata2.review.fulfillment
|
|
102974
|
-
};
|
|
102975
|
-
}
|
|
102976
|
-
if (metadata2.purpose === "mastery") {
|
|
102977
|
-
return {
|
|
102978
|
-
kind: "standard-quiz",
|
|
102979
|
-
purpose: metadata2.purpose,
|
|
102980
|
-
standard: metadata2.mastery.standard
|
|
102981
|
-
};
|
|
102982
|
-
}
|
|
102983
|
-
return { kind: "fixed-test", purpose: metadata2.purpose };
|
|
103532
|
+
return course;
|
|
102984
103533
|
}
|
|
102985
103534
|
submittedResult(result, metadata2) {
|
|
102986
103535
|
const response = buildAssessmentSubmitResult({
|
|
@@ -102994,29 +103543,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
102994
103543
|
}
|
|
102995
103544
|
return response;
|
|
102996
103545
|
}
|
|
102997
|
-
completedResult(result, metadata2) {
|
|
102998
|
-
const xpAwarded = this.xpFromResult(result);
|
|
102999
|
-
if (xpAwarded === null) {
|
|
103000
|
-
throw new GoneError("This completed assessment is missing its recorded XP.", {
|
|
103001
|
-
attemptId: result.sourcedId
|
|
103002
|
-
});
|
|
103003
|
-
}
|
|
103004
|
-
const response = buildCompletedAssessmentSubmitResult({
|
|
103005
|
-
attemptId: result.sourcedId,
|
|
103006
|
-
metadata: metadata2,
|
|
103007
|
-
xpAwarded
|
|
103008
|
-
});
|
|
103009
|
-
if (!response) {
|
|
103010
|
-
throw new GoneError("This completed assessment has incomplete persisted results.", {
|
|
103011
|
-
attemptId: result.sourcedId
|
|
103012
|
-
});
|
|
103013
|
-
}
|
|
103014
|
-
return response;
|
|
103015
|
-
}
|
|
103016
|
-
xpFromResult(result) {
|
|
103017
|
-
const xp = result.metadata?.xp;
|
|
103018
|
-
return typeof xp === "number" && Number.isFinite(xp) && xp >= 0 ? xp : null;
|
|
103019
|
-
}
|
|
103020
103546
|
awardAgeMs(scoreDate) {
|
|
103021
103547
|
const submittedAt = Date.parse(scoreDate);
|
|
103022
103548
|
return Number.isFinite(submittedAt) ? Math.max(0, Date.now() - submittedAt) : -1;
|
|
@@ -103143,7 +103669,9 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
103143
103669
|
attemptNumber: attempt.metadata.attemptNumber,
|
|
103144
103670
|
totalQuestions: input.totalQuestions,
|
|
103145
103671
|
correctQuestions: input.correctQuestions,
|
|
103146
|
-
xpAwarded: input.xpAwarded,
|
|
103672
|
+
xpAwarded: input.award.xpAwarded,
|
|
103673
|
+
...input.award.masteredUnitsApplied !== 0 ? { masteredUnits: input.award.masteredUnitsApplied } : {},
|
|
103674
|
+
...input.award.pctCompleteApp !== undefined ? { pctCompleteApp: input.award.pctCompleteApp } : {},
|
|
103147
103675
|
submittedAt: attempt.result.scoreDate,
|
|
103148
103676
|
eventId,
|
|
103149
103677
|
...session2 ? { resumeId: session2.resumeId } : {}
|
|
@@ -103268,6 +103796,9 @@ function assertAssessmentImportCourseMatches(manifest, integration) {
|
|
|
103268
103796
|
throw new ValidationError(`This manifest is for ${manifest.subject} grade ${manifest.grade}, not ${integration.subject} grade ${integration.grade}.`);
|
|
103269
103797
|
}
|
|
103270
103798
|
}
|
|
103799
|
+
function assessmentImportsAffectReviewBank(targetStatus, entries) {
|
|
103800
|
+
return targetStatus === "live" && entries.some((entry) => entry.purpose === "mastery");
|
|
103801
|
+
}
|
|
103271
103802
|
function associationMetadataMatches(row, entry) {
|
|
103272
103803
|
return row.purpose === entry.purpose && (row.standardFramework ?? undefined) === entry.standard?.framework && (row.standardIdentifier ?? undefined) === entry.standard?.identifier;
|
|
103273
103804
|
}
|
|
@@ -103317,18 +103848,6 @@ function attachedAssessmentImportMessage(targetStatus, editable) {
|
|
|
103317
103848
|
}
|
|
103318
103849
|
return editable ? "Attached as a draft." : "Attached as a read-only draft owned by another app.";
|
|
103319
103850
|
}
|
|
103320
|
-
function liveReviewAssessmentImportFailures(targetStatus, candidates, existingRows) {
|
|
103321
|
-
if (targetStatus === "draft") {
|
|
103322
|
-
return new Map;
|
|
103323
|
-
}
|
|
103324
|
-
const reviews = candidates.filter((candidate) => candidate.purpose === "review");
|
|
103325
|
-
const hasExistingLiveReview = existingRows.some((row) => row.purpose === "review" && row.status === "live");
|
|
103326
|
-
if (reviews.length <= 1 && !hasExistingLiveReview) {
|
|
103327
|
-
return new Map;
|
|
103328
|
-
}
|
|
103329
|
-
const message = hasExistingLiveReview ? "Another review assessment is already live for this course." : "Only one review assessment can be imported live at a time.";
|
|
103330
|
-
return new Map(reviews.map((review) => [review.qtiTestIdentifier, message]));
|
|
103331
|
-
}
|
|
103332
103851
|
var init_timeback_assessment_import_util = __esm(() => {
|
|
103333
103852
|
init_errors2();
|
|
103334
103853
|
init_timeback_qti_authoring_util();
|
|
@@ -103400,7 +103919,9 @@ class TimebackAssessmentsService {
|
|
|
103400
103919
|
const rows = await this.deps.db.query.gameTimebackAssessmentTests.findMany({
|
|
103401
103920
|
where: eq(gameTimebackAssessmentTests.integrationId, integrationId)
|
|
103402
103921
|
});
|
|
103403
|
-
const
|
|
103922
|
+
const sinkIdentifier = reviewBankSinkIdentifier(integrationId);
|
|
103923
|
+
const visibleRows = rows.filter((row) => row.purpose !== "review" || row.qtiTestIdentifier === sinkIdentifier);
|
|
103924
|
+
const assessments2 = await runWithConcurrency(visibleRows, QTI_HYDRATION_CONCURRENCY, async (row) => {
|
|
103404
103925
|
try {
|
|
103405
103926
|
const test = await client2.qtiApi.assessmentTests.get(row.qtiTestIdentifier);
|
|
103406
103927
|
return {
|
|
@@ -103408,7 +103929,7 @@ class TimebackAssessmentsService {
|
|
|
103408
103929
|
title: test.title,
|
|
103409
103930
|
questionCount: countQtiTestItems(test),
|
|
103410
103931
|
available: true,
|
|
103411
|
-
editable: isQtiTestOwnedByGame(test, ownership.gameSlug)
|
|
103932
|
+
editable: row.purpose !== "review" && isQtiTestOwnedByGame(test, ownership.gameSlug)
|
|
103412
103933
|
};
|
|
103413
103934
|
} catch (error88) {
|
|
103414
103935
|
addEvent("assessment.qti_fetch_failed", {
|
|
@@ -103428,8 +103949,11 @@ class TimebackAssessmentsService {
|
|
|
103428
103949
|
return assessments2.toSorted((a, b) => a.title.localeCompare(b.title));
|
|
103429
103950
|
}
|
|
103430
103951
|
async createAssessment(integrationId, input) {
|
|
103431
|
-
if (input.purpose === "diagnostic") {
|
|
103432
|
-
throw new ValidationError("Adaptive diagnostics must be created by importing assessment JSON with a routing sidecar.");
|
|
103952
|
+
if (input.purpose === "diagnostic" || input.purpose === "review") {
|
|
103953
|
+
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.");
|
|
103954
|
+
}
|
|
103955
|
+
if (input.assessmentKey === reviewBankSinkIdentifier(integrationId)) {
|
|
103956
|
+
throw new ValidationError("This assessment key is reserved for the review bank.");
|
|
103433
103957
|
}
|
|
103434
103958
|
const client2 = this.requireClient();
|
|
103435
103959
|
const ownership = await this.requireQtiTestOwnershipContext(integrationId);
|
|
@@ -103505,24 +104029,41 @@ class TimebackAssessmentsService {
|
|
|
103505
104029
|
index: index2,
|
|
103506
104030
|
standard: this.requirePurposeStandard(assessment.purpose, assessment.standard)
|
|
103507
104031
|
}));
|
|
103508
|
-
const
|
|
103509
|
-
|
|
103510
|
-
|
|
103511
|
-
|
|
103512
|
-
const existingByIdentifier = new Map(existingRows.map((row) => [row.qtiTestIdentifier, row]));
|
|
104032
|
+
const sinkIdentifier = reviewBankSinkIdentifier(integrationId);
|
|
104033
|
+
if (entries.some((entry) => entry.assessmentKey === sinkIdentifier || entry.qtiTestIdentifier === sinkIdentifier || entry.purpose === "review")) {
|
|
104034
|
+
throw new ValidationError("The review bank is system-managed and cannot be attached manually.");
|
|
104035
|
+
}
|
|
103513
104036
|
const validated = await this.validateAssessmentImportEntries(client2, entries, manifest.targetStatus);
|
|
103514
104037
|
const results = [];
|
|
103515
|
-
const
|
|
103516
|
-
|
|
103517
|
-
existingByKey,
|
|
103518
|
-
existingByIdentifier,
|
|
103519
|
-
|
|
103520
|
-
|
|
103521
|
-
|
|
103522
|
-
|
|
103523
|
-
|
|
103524
|
-
|
|
103525
|
-
|
|
104038
|
+
const affectsReviewBank = assessmentImportsAffectReviewBank(manifest.targetStatus, entries);
|
|
104039
|
+
const planAndInsert = async (db2, existingRows) => {
|
|
104040
|
+
const existingByKey = new Map(existingRows.flatMap((row) => row.assessmentKey ? [[row.assessmentKey, row]] : []));
|
|
104041
|
+
const existingByIdentifier = new Map(existingRows.map((row) => [row.qtiTestIdentifier, row]));
|
|
104042
|
+
const pendingInserts = this.planAssessmentImports({
|
|
104043
|
+
validated,
|
|
104044
|
+
existingByKey,
|
|
104045
|
+
existingByIdentifier,
|
|
104046
|
+
gameSlug,
|
|
104047
|
+
results
|
|
104048
|
+
});
|
|
104049
|
+
await this.insertImportedAssessments({
|
|
104050
|
+
db: db2,
|
|
104051
|
+
integrationId,
|
|
104052
|
+
pending: pendingInserts,
|
|
104053
|
+
targetStatus: manifest.targetStatus,
|
|
104054
|
+
gameSlug,
|
|
104055
|
+
results,
|
|
104056
|
+
abortOnError: affectsReviewBank
|
|
104057
|
+
});
|
|
104058
|
+
};
|
|
104059
|
+
if (affectsReviewBank) {
|
|
104060
|
+
await this.withLockedIntegrationAssessmentRows(integrationId, async (tx, lockedAssociations) => planAndInsert(tx, lockedAssociations));
|
|
104061
|
+
} else {
|
|
104062
|
+
const existingRows = await this.deps.db.query.gameTimebackAssessmentTests.findMany({
|
|
104063
|
+
where: eq(gameTimebackAssessmentTests.integrationId, integrationId)
|
|
104064
|
+
});
|
|
104065
|
+
await planAndInsert(this.deps.db, existingRows);
|
|
104066
|
+
}
|
|
103526
104067
|
setAttribute("app.assessment.operation", "bulk_attach_existing");
|
|
103527
104068
|
setAttribute("app.assessment.bulk_attach.target_status", manifest.targetStatus);
|
|
103528
104069
|
setAttribute("app.assessment.bulk_attach.created", results.filter((result) => result.status === "created").length);
|
|
@@ -103535,11 +104076,6 @@ class TimebackAssessmentsService {
|
|
|
103535
104076
|
assertManagedAssessmentIdentity(loaded.test, entry.assessmentKey);
|
|
103536
104077
|
assertAssessmentHasQuestions(loaded.questions.questions);
|
|
103537
104078
|
assertPlayableAssessmentImportQuestions(targetStatus, loaded.questions.questions.map(({ question }) => question));
|
|
103538
|
-
if (entry.purpose === "review") {
|
|
103539
|
-
assertReviewAssessmentHasStandards(loaded.questions.questions.map(({ question }) => reviewRoutingStandardRefsFromMetadata(question.metadata, {
|
|
103540
|
-
customOnly: true
|
|
103541
|
-
}).length));
|
|
103542
|
-
}
|
|
103543
104079
|
return { entry, test: loaded.test };
|
|
103544
104080
|
} catch (error88) {
|
|
103545
104081
|
return { entry, error: error88 };
|
|
@@ -103585,25 +104121,13 @@ class TimebackAssessmentsService {
|
|
|
103585
104121
|
}
|
|
103586
104122
|
return pendingInserts;
|
|
103587
104123
|
}
|
|
103588
|
-
|
|
103589
|
-
for (const { entry, test } of pending) {
|
|
103590
|
-
const message = failures.get(entry.qtiTestIdentifier);
|
|
103591
|
-
if (message) {
|
|
103592
|
-
results[entry.index] = {
|
|
103593
|
-
...this.associationImportResultBase(entry, test, gameSlug),
|
|
103594
|
-
status: "failed",
|
|
103595
|
-
message
|
|
103596
|
-
};
|
|
103597
|
-
}
|
|
103598
|
-
}
|
|
103599
|
-
}
|
|
103600
|
-
async concurrentAssessmentImportDecision(integrationId, entry) {
|
|
104124
|
+
async concurrentAssessmentImportDecision(db2, integrationId, entry) {
|
|
103601
104125
|
try {
|
|
103602
104126
|
const [byKey, byIdentifier] = await Promise.all([
|
|
103603
|
-
|
|
104127
|
+
db2.query.gameTimebackAssessmentTests.findFirst({
|
|
103604
104128
|
where: and(eq(gameTimebackAssessmentTests.integrationId, integrationId), eq(gameTimebackAssessmentTests.assessmentKey, entry.assessmentKey))
|
|
103605
104129
|
}),
|
|
103606
|
-
|
|
104130
|
+
db2.query.gameTimebackAssessmentTests.findFirst({
|
|
103607
104131
|
where: and(eq(gameTimebackAssessmentTests.integrationId, integrationId), eq(gameTimebackAssessmentTests.qtiTestIdentifier, entry.qtiTestIdentifier))
|
|
103608
104132
|
})
|
|
103609
104133
|
]);
|
|
@@ -103615,39 +104139,53 @@ class TimebackAssessmentsService {
|
|
|
103615
104139
|
return null;
|
|
103616
104140
|
}
|
|
103617
104141
|
}
|
|
103618
|
-
async insertImportedAssessments(
|
|
103619
|
-
for (const { entry, test } of pending) {
|
|
103620
|
-
const base = this.associationImportResultBase(entry, test, gameSlug);
|
|
104142
|
+
async insertImportedAssessments(input) {
|
|
104143
|
+
for (const { entry, test } of input.pending) {
|
|
104144
|
+
const base = this.associationImportResultBase(entry, test, input.gameSlug);
|
|
103621
104145
|
try {
|
|
103622
|
-
const [row] = await
|
|
103623
|
-
integrationId,
|
|
104146
|
+
const [row] = await input.db.insert(gameTimebackAssessmentTests).values({
|
|
104147
|
+
integrationId: input.integrationId,
|
|
103624
104148
|
assessmentKey: entry.assessmentKey,
|
|
103625
104149
|
qtiTestIdentifier: entry.qtiTestIdentifier,
|
|
103626
104150
|
purpose: entry.purpose,
|
|
103627
|
-
status: targetStatus,
|
|
104151
|
+
status: input.targetStatus,
|
|
103628
104152
|
sortOrder: entry.sortOrder,
|
|
103629
104153
|
standardFramework: entry.standard?.framework,
|
|
103630
104154
|
standardIdentifier: entry.standard?.identifier
|
|
103631
|
-
}).returning();
|
|
104155
|
+
}).onConflictDoNothing().returning();
|
|
103632
104156
|
if (!row) {
|
|
103633
|
-
|
|
104157
|
+
const concurrent = await this.concurrentAssessmentImportDecision(input.db, input.integrationId, entry);
|
|
104158
|
+
if (concurrent?.decision.kind === "skip" || concurrent?.decision.kind === "fail") {
|
|
104159
|
+
input.results[entry.index] = {
|
|
104160
|
+
...base,
|
|
104161
|
+
status: concurrent.decision.status,
|
|
104162
|
+
...concurrent.byKey ? { association: this.associationImportSummary(concurrent.byKey) } : {},
|
|
104163
|
+
message: concurrent.decision.message
|
|
104164
|
+
};
|
|
104165
|
+
} else {
|
|
104166
|
+
throw new Error("Assessment association insert conflicted");
|
|
104167
|
+
}
|
|
104168
|
+
} else {
|
|
104169
|
+
input.results[entry.index] = {
|
|
104170
|
+
...base,
|
|
104171
|
+
status: "created",
|
|
104172
|
+
association: this.associationImportSummary(row),
|
|
104173
|
+
message: attachedAssessmentImportMessage(input.targetStatus, base.editable ?? false)
|
|
104174
|
+
};
|
|
103634
104175
|
}
|
|
103635
|
-
results[entry.index] = {
|
|
103636
|
-
...base,
|
|
103637
|
-
status: "created",
|
|
103638
|
-
association: this.associationImportSummary(row),
|
|
103639
|
-
message: attachedAssessmentImportMessage(targetStatus, base.editable ?? false)
|
|
103640
|
-
};
|
|
103641
104176
|
} catch (error88) {
|
|
103642
|
-
|
|
104177
|
+
if (input.abortOnError) {
|
|
104178
|
+
throw error88;
|
|
104179
|
+
}
|
|
104180
|
+
input.results[entry.index] = {
|
|
103643
104181
|
...base,
|
|
103644
104182
|
status: "failed",
|
|
103645
104183
|
message: `Association attach failed: ${errorMessage(error88)}`
|
|
103646
104184
|
};
|
|
103647
104185
|
if (isUniqueViolation(error88)) {
|
|
103648
|
-
const concurrent = await this.concurrentAssessmentImportDecision(integrationId, entry);
|
|
104186
|
+
const concurrent = await this.concurrentAssessmentImportDecision(input.db, input.integrationId, entry);
|
|
103649
104187
|
if (concurrent?.decision.kind === "skip" || concurrent?.decision.kind === "fail") {
|
|
103650
|
-
results[entry.index] = {
|
|
104188
|
+
input.results[entry.index] = {
|
|
103651
104189
|
...base,
|
|
103652
104190
|
status: concurrent.decision.status,
|
|
103653
104191
|
...concurrent.byKey ? { association: this.associationImportSummary(concurrent.byKey) } : {},
|
|
@@ -103661,6 +104199,12 @@ class TimebackAssessmentsService {
|
|
|
103661
104199
|
async updateAssessment(integrationId, qtiTestIdentifier, input) {
|
|
103662
104200
|
try {
|
|
103663
104201
|
return await this.withLockedAssessmentAssociations(integrationId, qtiTestIdentifier, async (row, tx, associations) => {
|
|
104202
|
+
if (row.purpose === "review" || qtiTestIdentifier === reviewBankSinkIdentifier(integrationId)) {
|
|
104203
|
+
throw new ValidationError("The review bank is system-managed and cannot be edited.");
|
|
104204
|
+
}
|
|
104205
|
+
if (input.purpose === "review") {
|
|
104206
|
+
throw new ValidationError("Assessment purpose cannot be changed to the system-managed review bank.");
|
|
104207
|
+
}
|
|
103664
104208
|
const nextPurpose = input.purpose ?? row.purpose;
|
|
103665
104209
|
assertPurposeChangeDraft(row, nextPurpose);
|
|
103666
104210
|
const requestedStandard = input.standard ? this.canonicalAssessmentStandard(input.standard) : undefined;
|
|
@@ -103679,19 +104223,10 @@ class TimebackAssessmentsService {
|
|
|
103679
104223
|
...associationUpdates,
|
|
103680
104224
|
...diagnosticChanges.updates
|
|
103681
104225
|
};
|
|
103682
|
-
const nextStatus = input.status ?? row.status;
|
|
103683
104226
|
const publishing = input.status !== undefined && isAssessmentPublicationTransition(row.status, input.status);
|
|
103684
|
-
const activatingReview = nextPurpose === "review" && nextStatus === "live" && (row.purpose !== "review" || row.status !== "live");
|
|
103685
104227
|
if (input.status !== undefined) {
|
|
103686
104228
|
validateAssessmentStatusTransition(row.status, input.status);
|
|
103687
104229
|
}
|
|
103688
|
-
if (!publishing && !activatingReview && nextPurpose === "review" && row.purpose !== "review") {
|
|
103689
|
-
const ownership = await this.requireQtiTestOwnershipContext(integrationId, tx);
|
|
103690
|
-
await this.rebuildReviewMapping(this.requireClient(), qtiTestIdentifier, {
|
|
103691
|
-
kind: "owned",
|
|
103692
|
-
gameSlug: ownership.gameSlug
|
|
103693
|
-
});
|
|
103694
|
-
}
|
|
103695
104230
|
if (input.title !== undefined) {
|
|
103696
104231
|
assertDraftAssessment(row);
|
|
103697
104232
|
assertAllAssessmentAssociationsDraft(associations);
|
|
@@ -103701,7 +104236,7 @@ class TimebackAssessmentsService {
|
|
|
103701
104236
|
assertQtiTestOwnedByGame(test, ownership.gameSlug);
|
|
103702
104237
|
await client2.qtiApi.assessmentTests.update(qtiTestIdentifier, buildQtiTestUpdateInput(test, input.title));
|
|
103703
104238
|
}
|
|
103704
|
-
if (publishing
|
|
104239
|
+
if (publishing) {
|
|
103705
104240
|
if (nextPurpose === "diagnostic" && !nextDiagnostic) {
|
|
103706
104241
|
throw new ValidationError("Platform-routed diagnostics require a diagnostic key and routing manifest before publication.");
|
|
103707
104242
|
}
|
|
@@ -103711,8 +104246,7 @@ class TimebackAssessmentsService {
|
|
|
103711
104246
|
updates.diagnosticKey = nextDiagnostic.diagnosticKey;
|
|
103712
104247
|
updates.diagnosticRoutingManifest = nextDiagnostic.routingManifest;
|
|
103713
104248
|
} else {
|
|
103714
|
-
|
|
103715
|
-
await this.validateAssessmentHasQuestions(loaded, reviewOwnership?.gameSlug);
|
|
104249
|
+
await this.validateAssessmentHasQuestions(loaded);
|
|
103716
104250
|
}
|
|
103717
104251
|
if (publishing) {
|
|
103718
104252
|
const publication = await this.publishManagedAssessment(row, nextPurpose, nextDiagnostic?.routingManifest ?? null, loaded);
|
|
@@ -103735,15 +104269,15 @@ class TimebackAssessmentsService {
|
|
|
103735
104269
|
if (databaseConstraintName(error88) === "game_timeback_assessment_tests_one_live_diagnostic_key_idx") {
|
|
103736
104270
|
throw new ValidationError("Only one live revision of a diagnostic key is allowed. Archive the current revision before publishing another.");
|
|
103737
104271
|
}
|
|
103738
|
-
if (databaseConstraintName(error88) === "game_timeback_assessment_tests_one_live_review_idx") {
|
|
103739
|
-
throw new ValidationError("Only one live review assessment is allowed. Archive the current review bank before publishing another.");
|
|
103740
|
-
}
|
|
103741
104272
|
}
|
|
103742
104273
|
throw error88;
|
|
103743
104274
|
}
|
|
103744
104275
|
}
|
|
103745
104276
|
async removeAssessment(integrationId, qtiTestIdentifier) {
|
|
103746
104277
|
return this.withLockedAssessmentAssociations(integrationId, qtiTestIdentifier, async (row, tx) => {
|
|
104278
|
+
if (qtiTestIdentifier === reviewBankSinkIdentifier(integrationId)) {
|
|
104279
|
+
throw new ValidationError("The review bank is permanent and cannot be removed or archived.");
|
|
104280
|
+
}
|
|
103747
104281
|
const plan = planAssessmentRemoval(row.status);
|
|
103748
104282
|
if (plan.kind === "delete") {
|
|
103749
104283
|
await tx.delete(gameTimebackAssessmentTests).where(eq(gameTimebackAssessmentTests.id, row.id));
|
|
@@ -103757,6 +104291,9 @@ class TimebackAssessmentsService {
|
|
|
103757
104291
|
});
|
|
103758
104292
|
}
|
|
103759
104293
|
async reorderAssessments(integrationId, purpose, testIdentifiers) {
|
|
104294
|
+
if (purpose === "review") {
|
|
104295
|
+
throw new ValidationError("The review bank is system-managed and cannot be reordered.");
|
|
104296
|
+
}
|
|
103760
104297
|
await this.requireIntegration(integrationId);
|
|
103761
104298
|
validateUniqueAssessmentIdentifiers(testIdentifiers);
|
|
103762
104299
|
const updatedAt = new Date;
|
|
@@ -103814,19 +104351,96 @@ class TimebackAssessmentsService {
|
|
|
103814
104351
|
});
|
|
103815
104352
|
return { ...result, questions };
|
|
103816
104353
|
}
|
|
103817
|
-
async
|
|
103818
|
-
|
|
103819
|
-
|
|
103820
|
-
|
|
104354
|
+
async synchronizeReviewBank(integrationId) {
|
|
104355
|
+
const client2 = this.requireClient();
|
|
104356
|
+
await this.requireIntegration(integrationId);
|
|
104357
|
+
const sinkIdentifier = reviewBankSinkIdentifier(integrationId);
|
|
104358
|
+
for (let attempt = 0;attempt < REVIEW_BANK_SYNC_ATTEMPTS; attempt++) {
|
|
104359
|
+
const associations = await this.deps.db.query.gameTimebackAssessmentTests.findMany({
|
|
104360
|
+
where: eq(gameTimebackAssessmentTests.integrationId, integrationId)
|
|
104361
|
+
});
|
|
104362
|
+
const masterySources = this.reviewBankMasterySources(associations);
|
|
104363
|
+
const contributorFingerprint = await reviewBankContributorFingerprint(masterySources);
|
|
104364
|
+
const existingSink = await this.findReviewBankSink(client2, integrationId);
|
|
104365
|
+
if (existingSink) {
|
|
104366
|
+
assertReviewBankSinkOwnership(existingSink, integrationId);
|
|
104367
|
+
}
|
|
104368
|
+
let unchangedResult = null;
|
|
104369
|
+
if (existingSink) {
|
|
104370
|
+
try {
|
|
104371
|
+
unchangedResult = await unchangedReviewBankSynchronizationResult({
|
|
104372
|
+
sink: existingSink,
|
|
104373
|
+
integrationId,
|
|
104374
|
+
contributorFingerprint,
|
|
104375
|
+
contributorCount: masterySources.length
|
|
104376
|
+
});
|
|
104377
|
+
} catch (error88) {
|
|
104378
|
+
addEvent("assessment.review_mapping_noop_validation_failed", {
|
|
104379
|
+
"app.assessment.qti_test_identifier": existingSink.identifier,
|
|
104380
|
+
"exception.type": errorType(error88),
|
|
104381
|
+
"app.error.message": errorMessage(error88)
|
|
104382
|
+
});
|
|
104383
|
+
}
|
|
103821
104384
|
}
|
|
103822
|
-
|
|
103823
|
-
|
|
103824
|
-
|
|
104385
|
+
let plan = null;
|
|
104386
|
+
let addedItemCount = 0;
|
|
104387
|
+
let removedItemCount = 0;
|
|
104388
|
+
let catalogUnchanged = unchangedResult !== null;
|
|
104389
|
+
if (!unchangedResult) {
|
|
104390
|
+
const contributors = await runWithConcurrency(masterySources, QTI_HYDRATION_CONCURRENCY, async (source) => {
|
|
104391
|
+
const test = await client2.qtiApi.assessmentTests.get(source.qtiTestIdentifier);
|
|
104392
|
+
assertManagedAssessmentIdentity(test, source.assessmentKey);
|
|
104393
|
+
return { ...source, test };
|
|
104394
|
+
});
|
|
104395
|
+
plan = await prepareReviewBankSynchronization({
|
|
104396
|
+
bankIdentifier: sinkIdentifier,
|
|
104397
|
+
contributors,
|
|
104398
|
+
contributorFingerprint
|
|
104399
|
+
});
|
|
104400
|
+
const diff = reviewBankSynchronizationDiff({ existingSink, plan });
|
|
104401
|
+
addedItemCount = diff.addedItemCount;
|
|
104402
|
+
removedItemCount = diff.removedItemCount;
|
|
104403
|
+
catalogUnchanged = diff.unchanged;
|
|
104404
|
+
}
|
|
104405
|
+
const committed = await this.withLockedIntegrationAssessmentRows(integrationId, async (tx, lockedAssociations) => {
|
|
104406
|
+
const lockedSources = this.reviewBankMasterySources(lockedAssociations);
|
|
104407
|
+
const lockedFingerprint = await reviewBankContributorFingerprint(lockedSources);
|
|
104408
|
+
if (lockedFingerprint !== contributorFingerprint) {
|
|
104409
|
+
return null;
|
|
104410
|
+
}
|
|
104411
|
+
const associationChanged = await this.installReviewBankAssociation({
|
|
104412
|
+
tx,
|
|
104413
|
+
integrationId,
|
|
104414
|
+
associations: lockedAssociations,
|
|
104415
|
+
sinkIdentifier,
|
|
104416
|
+
beforeInstall: async () => {
|
|
104417
|
+
if (!catalogUnchanged) {
|
|
104418
|
+
await this.writeReviewBankSink({
|
|
104419
|
+
client: client2,
|
|
104420
|
+
integrationId,
|
|
104421
|
+
existingSink,
|
|
104422
|
+
plan,
|
|
104423
|
+
metadata: reviewBankSinkMetadata(integrationId, plan)
|
|
104424
|
+
});
|
|
104425
|
+
}
|
|
104426
|
+
}
|
|
104427
|
+
});
|
|
104428
|
+
const unchanged = catalogUnchanged && !associationChanged;
|
|
104429
|
+
const result = unchangedResult ? { ...unchangedResult, unchanged } : {
|
|
104430
|
+
qtiTestIdentifier: sinkIdentifier,
|
|
104431
|
+
...plan.summary,
|
|
104432
|
+
addedItemCount,
|
|
104433
|
+
removedItemCount,
|
|
104434
|
+
unchanged,
|
|
104435
|
+
warnings: plan.warnings
|
|
104436
|
+
};
|
|
104437
|
+
return this.reviewMappingResult(result);
|
|
103825
104438
|
});
|
|
103826
|
-
|
|
103827
|
-
|
|
103828
|
-
|
|
103829
|
-
}
|
|
104439
|
+
if (committed) {
|
|
104440
|
+
return committed;
|
|
104441
|
+
}
|
|
104442
|
+
}
|
|
104443
|
+
throw new ValidationError("Mastery assessments changed while the review bank was synchronizing. Try again.");
|
|
103830
104444
|
}
|
|
103831
104445
|
async listQuestionLibrary(integrationId, params) {
|
|
103832
104446
|
const client2 = this.requireClient();
|
|
@@ -103846,15 +104460,18 @@ class TimebackAssessmentsService {
|
|
|
103846
104460
|
purpose,
|
|
103847
104461
|
standard: standardInput
|
|
103848
104462
|
} = input;
|
|
103849
|
-
if (purpose === "diagnostic") {
|
|
103850
|
-
throw new ValidationError("Adaptive diagnostics must be created by importing assessment JSON with a routing sidecar.");
|
|
104463
|
+
if (purpose === "diagnostic" || purpose === "review") {
|
|
104464
|
+
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.");
|
|
104465
|
+
}
|
|
104466
|
+
if (assessmentKey === reviewBankSinkIdentifier(integrationId) || targetTestIdentifier === reviewBankSinkIdentifier(integrationId)) {
|
|
104467
|
+
throw new ValidationError("This assessment identity is reserved for the review bank.");
|
|
103851
104468
|
}
|
|
103852
104469
|
const client2 = this.requireClient();
|
|
103853
104470
|
const ownership = await this.requireQtiTestOwnershipContext(integrationId);
|
|
103854
104471
|
const { integration } = ownership;
|
|
103855
104472
|
const standard = this.requirePurposeStandard(purpose, standardInput);
|
|
103856
104473
|
const source = await client2.qtiApi.assessmentTests.get(sourceTestIdentifier);
|
|
103857
|
-
const itemPlan = buildQtiAssessmentItemCopyPlan(source, targetTestIdentifier, (identifier) =>
|
|
104474
|
+
const itemPlan = buildQtiAssessmentItemCopyPlan(source, targetTestIdentifier, (identifier) => qtiItemHref(client2, identifier));
|
|
103858
104475
|
const itemCopies = await runWithConcurrency(itemPlan, QTI_HYDRATION_CONCURRENCY, async (plan) => {
|
|
103859
104476
|
const sourceItem = await client2.qtiApi.assessmentItems.get(plan.sourceIdentifier);
|
|
103860
104477
|
return {
|
|
@@ -103979,7 +104596,7 @@ class TimebackAssessmentsService {
|
|
|
103979
104596
|
};
|
|
103980
104597
|
}
|
|
103981
104598
|
const section = await this.qtiSectionItems(client2, qtiTestIdentifier, undefined, undefined, ownership.test);
|
|
103982
|
-
const href =
|
|
104599
|
+
const href = qtiItemHref(client2, itemIdentifier);
|
|
103983
104600
|
let item;
|
|
103984
104601
|
let itemCreationAttempted = false;
|
|
103985
104602
|
let referenceCreationAttempted = false;
|
|
@@ -104089,7 +104706,7 @@ class TimebackAssessmentsService {
|
|
|
104089
104706
|
const result = await section.items.reorder({
|
|
104090
104707
|
items: items.map((item) => ({
|
|
104091
104708
|
identifier: item.identifier,
|
|
104092
|
-
href: item.href ??
|
|
104709
|
+
href: item.href ?? qtiItemHref(client2, item.identifier),
|
|
104093
104710
|
sequence: item.sequence
|
|
104094
104711
|
}))
|
|
104095
104712
|
});
|
|
@@ -104103,6 +104720,121 @@ class TimebackAssessmentsService {
|
|
|
104103
104720
|
}
|
|
104104
104721
|
return this.deps.timeback;
|
|
104105
104722
|
}
|
|
104723
|
+
reviewBankMasterySources(associations) {
|
|
104724
|
+
return associations.filter((association) => association.purpose === "mastery" && association.status === "live").map((association) => {
|
|
104725
|
+
const standard = assessmentStandardForRow(association);
|
|
104726
|
+
if (!association.assessmentKey || !standard) {
|
|
104727
|
+
throw new ValidationError(`Live mastery assessment ${association.qtiTestIdentifier} requires a managed identity and standard`);
|
|
104728
|
+
}
|
|
104729
|
+
if (assessmentKeyFromManagedQtiIdentifier(association.qtiTestIdentifier) !== association.assessmentKey) {
|
|
104730
|
+
throw new ValidationError(`Live mastery assessment ${association.qtiTestIdentifier} is not a managed publication for ${association.assessmentKey}`);
|
|
104731
|
+
}
|
|
104732
|
+
return {
|
|
104733
|
+
assessmentKey: association.assessmentKey,
|
|
104734
|
+
qtiTestIdentifier: association.qtiTestIdentifier,
|
|
104735
|
+
standard
|
|
104736
|
+
};
|
|
104737
|
+
});
|
|
104738
|
+
}
|
|
104739
|
+
async writeReviewBankSink(input) {
|
|
104740
|
+
function sinkInput(title, includeIdentifier = false) {
|
|
104741
|
+
return buildReviewBankSinkInput({
|
|
104742
|
+
identifier: input.plan.bankIdentifier,
|
|
104743
|
+
title,
|
|
104744
|
+
metadata: input.metadata,
|
|
104745
|
+
itemIdentifiers: input.plan.itemIdentifiers,
|
|
104746
|
+
itemHref: (identifier) => qtiItemHref(input.client, identifier),
|
|
104747
|
+
includeIdentifier
|
|
104748
|
+
});
|
|
104749
|
+
}
|
|
104750
|
+
if (input.existingSink) {
|
|
104751
|
+
await input.client.qtiApi.assessmentTests.update(input.plan.bankIdentifier, sinkInput(input.existingSink.title));
|
|
104752
|
+
return;
|
|
104753
|
+
}
|
|
104754
|
+
try {
|
|
104755
|
+
const createInput = sinkInput(REVIEW_BANK_TITLE, true);
|
|
104756
|
+
await input.client.qtiApi.assessmentTests.create(createInput);
|
|
104757
|
+
} catch (error88) {
|
|
104758
|
+
if (!isApiError(error88) || error88.statusCode !== 409) {
|
|
104759
|
+
throw error88;
|
|
104760
|
+
}
|
|
104761
|
+
const concurrentlyCreated = await input.client.qtiApi.assessmentTests.get(input.plan.bankIdentifier);
|
|
104762
|
+
assertReviewBankSinkOwnership(concurrentlyCreated, input.integrationId);
|
|
104763
|
+
await input.client.qtiApi.assessmentTests.update(input.plan.bankIdentifier, sinkInput(concurrentlyCreated.title));
|
|
104764
|
+
}
|
|
104765
|
+
}
|
|
104766
|
+
async installReviewBankAssociation(input) {
|
|
104767
|
+
const singleton = input.associations.find((association) => association.qtiTestIdentifier === input.sinkIdentifier);
|
|
104768
|
+
if (singleton && singleton.purpose !== "review") {
|
|
104769
|
+
throw new ValidationError("The reserved review-bank identifier is associated with another assessment purpose.");
|
|
104770
|
+
}
|
|
104771
|
+
const legacyReviews = input.associations.filter((association) => association.purpose === "review" && association.qtiTestIdentifier !== input.sinkIdentifier);
|
|
104772
|
+
const reservedKeyOwner = input.associations.find((association) => association.assessmentKey === input.sinkIdentifier && association.qtiTestIdentifier !== input.sinkIdentifier);
|
|
104773
|
+
if (reservedKeyOwner && reservedKeyOwner.purpose !== "review") {
|
|
104774
|
+
throw new ValidationError("The reserved review-bank assessment key is already in use.");
|
|
104775
|
+
}
|
|
104776
|
+
const singletonCurrent = Boolean(singleton && singleton.assessmentKey === input.sinkIdentifier && singleton.status === "live" && singleton.sortOrder === null && singleton.standardFramework === null && singleton.standardIdentifier === null && singleton.diagnosticKey === null && singleton.diagnosticRoutingManifest === null);
|
|
104777
|
+
const legacyCurrent = legacyReviews.every((association) => association.status === "archived" && association.sortOrder === null && association.assessmentKey !== input.sinkIdentifier);
|
|
104778
|
+
const changed = !singletonCurrent || !legacyCurrent;
|
|
104779
|
+
await input.beforeInstall();
|
|
104780
|
+
for (const legacy of legacyReviews) {
|
|
104781
|
+
if (legacy.status !== "archived" || legacy.sortOrder !== null || legacy.assessmentKey === input.sinkIdentifier) {
|
|
104782
|
+
await input.tx.update(gameTimebackAssessmentTests).set({
|
|
104783
|
+
status: "archived",
|
|
104784
|
+
sortOrder: null,
|
|
104785
|
+
...legacy.assessmentKey === input.sinkIdentifier ? { assessmentKey: null } : {},
|
|
104786
|
+
updatedAt: new Date
|
|
104787
|
+
}).where(eq(gameTimebackAssessmentTests.id, legacy.id));
|
|
104788
|
+
}
|
|
104789
|
+
}
|
|
104790
|
+
if (singletonCurrent) {
|
|
104791
|
+
return changed;
|
|
104792
|
+
}
|
|
104793
|
+
if (singleton) {
|
|
104794
|
+
await input.tx.update(gameTimebackAssessmentTests).set({
|
|
104795
|
+
assessmentKey: input.sinkIdentifier,
|
|
104796
|
+
purpose: "review",
|
|
104797
|
+
status: "live",
|
|
104798
|
+
sortOrder: null,
|
|
104799
|
+
standardFramework: null,
|
|
104800
|
+
standardIdentifier: null,
|
|
104801
|
+
diagnosticKey: null,
|
|
104802
|
+
diagnosticRoutingManifest: null,
|
|
104803
|
+
updatedAt: new Date
|
|
104804
|
+
}).where(eq(gameTimebackAssessmentTests.id, singleton.id));
|
|
104805
|
+
return changed;
|
|
104806
|
+
}
|
|
104807
|
+
await input.tx.insert(gameTimebackAssessmentTests).values({
|
|
104808
|
+
integrationId: input.integrationId,
|
|
104809
|
+
assessmentKey: input.sinkIdentifier,
|
|
104810
|
+
qtiTestIdentifier: input.sinkIdentifier,
|
|
104811
|
+
purpose: "review",
|
|
104812
|
+
status: "live",
|
|
104813
|
+
sortOrder: null,
|
|
104814
|
+
standardFramework: null,
|
|
104815
|
+
standardIdentifier: null,
|
|
104816
|
+
diagnosticKey: null,
|
|
104817
|
+
diagnosticRoutingManifest: null
|
|
104818
|
+
});
|
|
104819
|
+
return changed;
|
|
104820
|
+
}
|
|
104821
|
+
reviewMappingResult(result) {
|
|
104822
|
+
setAttribute("app.assessment.operation", "update_review_mapping");
|
|
104823
|
+
setAttribute("app.assessment.review_mapping_item_count", result.itemCount);
|
|
104824
|
+
setAttribute("app.assessment.review_mapping_added_item_count", result.addedItemCount);
|
|
104825
|
+
setAttribute("app.assessment.review_mapping_removed_item_count", result.removedItemCount);
|
|
104826
|
+
return result;
|
|
104827
|
+
}
|
|
104828
|
+
async withLockedIntegrationAssessmentRows(integrationId, action) {
|
|
104829
|
+
return this.deps.db.transaction(async (tx) => {
|
|
104830
|
+
const [integration] = await tx.select({ id: gameTimebackIntegrations.id }).from(gameTimebackIntegrations).where(and(eq(gameTimebackIntegrations.id, integrationId), isActiveGameTimebackIntegrationStatus())).for("update");
|
|
104831
|
+
if (!integration) {
|
|
104832
|
+
throw new NotFoundError(`Integration not found: ${integrationId}`);
|
|
104833
|
+
}
|
|
104834
|
+
const rows = await tx.select().from(gameTimebackAssessmentTests).where(eq(gameTimebackAssessmentTests.integrationId, integrationId)).orderBy(gameTimebackAssessmentTests.id).for("update");
|
|
104835
|
+
return action(tx, rows);
|
|
104836
|
+
});
|
|
104837
|
+
}
|
|
104106
104838
|
async withLockedAssessmentAssociations(integrationId, qtiTestIdentifier, action) {
|
|
104107
104839
|
return this.deps.db.transaction(async (tx) => {
|
|
104108
104840
|
const rows = await tx.select().from(gameTimebackAssessmentTests).where(eq(gameTimebackAssessmentTests.qtiTestIdentifier, qtiTestIdentifier)).orderBy(gameTimebackAssessmentTests.id).for("update");
|
|
@@ -104187,8 +104919,16 @@ class TimebackAssessmentsService {
|
|
|
104187
104919
|
items: client2.qtiApi.assessmentTests.testParts(qtiTestIdentifier).sections(section.partIdentifier).items(section.sectionIdentifier)
|
|
104188
104920
|
};
|
|
104189
104921
|
}
|
|
104190
|
-
|
|
104191
|
-
|
|
104922
|
+
async findReviewBankSink(client2, integrationId) {
|
|
104923
|
+
const identifier = reviewBankSinkIdentifier(integrationId);
|
|
104924
|
+
try {
|
|
104925
|
+
return await client2.qtiApi.assessmentTests.get(identifier);
|
|
104926
|
+
} catch (error88) {
|
|
104927
|
+
if (isApiError(error88) && error88.statusCode === 404) {
|
|
104928
|
+
return null;
|
|
104929
|
+
}
|
|
104930
|
+
throw error88;
|
|
104931
|
+
}
|
|
104192
104932
|
}
|
|
104193
104933
|
async ensureImmutableQtiResource(get, create) {
|
|
104194
104934
|
try {
|
|
@@ -104229,7 +104969,7 @@ class TimebackAssessmentsService {
|
|
|
104229
104969
|
test: loaded.test,
|
|
104230
104970
|
questions: loaded.questions,
|
|
104231
104971
|
diagnosticRoutingManifest,
|
|
104232
|
-
itemHref: (identifier) =>
|
|
104972
|
+
itemHref: (identifier) => qtiItemHref(client2, identifier)
|
|
104233
104973
|
});
|
|
104234
104974
|
await this.ensureManagedQtiPublication(client2, plan);
|
|
104235
104975
|
return plan;
|
|
@@ -104311,15 +105051,11 @@ class TimebackAssessmentsService {
|
|
|
104311
105051
|
const plan = buildQtiLibraryListPlan(params);
|
|
104312
105052
|
return list(plan.params);
|
|
104313
105053
|
}
|
|
104314
|
-
async validateAssessmentHasQuestions(loaded
|
|
105054
|
+
async validateAssessmentHasQuestions(loaded) {
|
|
104315
105055
|
assertAssessmentHasQuestions(loaded.questions.questions);
|
|
104316
105056
|
for (const { question } of loaded.questions.questions) {
|
|
104317
105057
|
assertPlayableQtiQuestion(question);
|
|
104318
105058
|
}
|
|
104319
|
-
if (reviewGameSlug) {
|
|
104320
|
-
assertQtiTestOwnedByGame(loaded.test, reviewGameSlug);
|
|
104321
|
-
await this.writeReviewMapping(this.requireClient(), loaded.test, loaded.questions);
|
|
104322
|
-
}
|
|
104323
105059
|
}
|
|
104324
105060
|
async prepareDiagnosticAssociationChanges(row, nextPurpose, requested) {
|
|
104325
105061
|
if (row.purpose !== "diagnostic" && nextPurpose === "diagnostic" && !requested) {
|
|
@@ -104348,22 +105084,8 @@ class TimebackAssessmentsService {
|
|
|
104348
105084
|
}
|
|
104349
105085
|
};
|
|
104350
105086
|
}
|
|
104351
|
-
async rebuildReviewMapping(client2, qtiTestIdentifier, scope) {
|
|
104352
|
-
const [test, references] = await Promise.all([
|
|
104353
|
-
client2.qtiApi.assessmentTests.get(qtiTestIdentifier),
|
|
104354
|
-
client2.qtiApi.assessmentTests.getQuestions(qtiTestIdentifier)
|
|
104355
|
-
]);
|
|
104356
|
-
if (scope.kind === "owned") {
|
|
104357
|
-
assertQtiTestOwnedByGame(test, scope.gameSlug);
|
|
104358
|
-
}
|
|
104359
|
-
return this.writeReviewMapping(client2, test, await hydrateQtiTestQuestions(client2, references));
|
|
104360
|
-
}
|
|
104361
|
-
async writeReviewMapping(client2, test, questions) {
|
|
104362
|
-
const prepared = await prepareReviewMappingUpdate(test, questions);
|
|
104363
|
-
await client2.qtiApi.assessmentTests.update(test.identifier, prepared.update);
|
|
104364
|
-
return prepared.summary;
|
|
104365
|
-
}
|
|
104366
105087
|
}
|
|
105088
|
+
var REVIEW_BANK_TITLE = "Review Bank", REVIEW_BANK_SYNC_ATTEMPTS = 2;
|
|
104367
105089
|
var init_timeback_assessments_service = __esm(async () => {
|
|
104368
105090
|
init_drizzle_orm();
|
|
104369
105091
|
init_helpers_index();
|
|
@@ -164385,7 +165107,7 @@ function parseQtiLibraryParams(searchParams) {
|
|
|
164385
165107
|
limit
|
|
164386
165108
|
};
|
|
164387
165109
|
}
|
|
164388
|
-
var populateStudent, getUser, getUserEnrollments, getUserById, setupIntegration, getIntegrations, getRemovedIntegrations, createIntegration, updateIntegration, deactivateCourse, reactivateCourse, getIntegrationConfig, verifyIntegration, getConfig, deleteIntegrations, endActivity, heartbeat, advanceCourse, unenrollCourse, startRuntimeAssessment, getRuntimeAssessment, getLatestRuntimeAssessment, saveRuntimeAssessment, submitRuntimeAssessmentItem, submitRuntimeAssessment, finalizeRuntimeAssessment, exportRuntimeAssessmentFixture, getStudentXp, getStudentMastery, getStudentHighestGradeMastered, getRoster, getStudentOverview, getGameMetrics, getStudentActivity, getGradeLevelTestResults, getGradeLevelTestReview, getActivityDetail, listMetricDiscrepancies, verifyMetricDiscrepancy, grantXp, adjustTime, adjustMastery, reconcileMasteryForConfigChange, searchStudents, enrollStudent, unenrollStudent, reactivateEnrollment, listAssessments, createAssessment, attachExistingAssessments, updateAssessment,
|
|
165110
|
+
var populateStudent, getUser, getUserEnrollments, getUserById, setupIntegration, getIntegrations, getRemovedIntegrations, createIntegration, updateIntegration, deactivateCourse, reactivateCourse, getIntegrationConfig, verifyIntegration, getConfig, deleteIntegrations, endActivity, heartbeat, advanceCourse, unenrollCourse, startRuntimeAssessment, getRuntimeAssessment, getLatestRuntimeAssessment, saveRuntimeAssessment, submitRuntimeAssessmentItem, submitRuntimeAssessment, finalizeRuntimeAssessment, exportRuntimeAssessmentFixture, getStudentXp, getStudentMastery, getStudentHighestGradeMastered, getRoster, getStudentOverview, getGameMetrics, getStudentActivity, getGradeLevelTestResults, getGradeLevelTestReview, getActivityDetail, listMetricDiscrepancies, verifyMetricDiscrepancy, grantXp, adjustTime, adjustMastery, reconcileMasteryForConfigChange, searchStudents, enrollStudent, unenrollStudent, reactivateEnrollment, listAssessments, createAssessment, attachExistingAssessments, updateAssessment, synchronizeReviewBank, reorderAssessments, reorderQuestions, removeAssessment, listQuestions, listQuestionLibrary, listTestLibrary, copyAssessment, createQuestion, updateQuestion, removeQuestion, timeback2;
|
|
164389
165111
|
var init_timeback_controller = __esm(() => {
|
|
164390
165112
|
init_esm();
|
|
164391
165113
|
init_src();
|
|
@@ -164747,7 +165469,9 @@ var init_timeback_controller = __esm(() => {
|
|
|
164747
165469
|
attemptId,
|
|
164748
165470
|
input: {
|
|
164749
165471
|
submissionId: body2.submissionId,
|
|
164750
|
-
xpAwarded: body2.xpAwarded
|
|
165472
|
+
xpAwarded: body2.xpAwarded,
|
|
165473
|
+
...body2.masteredUnits !== undefined ? { masteredUnits: body2.masteredUnits } : {},
|
|
165474
|
+
...body2.masteredUnitsAbsolute !== undefined ? { masteredUnitsAbsolute: body2.masteredUnitsAbsolute } : {}
|
|
164751
165475
|
},
|
|
164752
165476
|
game: {
|
|
164753
165477
|
appName: body2.appName,
|
|
@@ -165081,13 +165805,13 @@ var init_timeback_controller = __esm(() => {
|
|
|
165081
165805
|
const body2 = await parseRequestBody(ctx.request, UpdateAssessmentRequestSchema);
|
|
165082
165806
|
return ctx.services.timebackAssessments.updateAssessment(integrationId, testIdentifier, body2);
|
|
165083
165807
|
});
|
|
165084
|
-
|
|
165085
|
-
const { gameId, courseId
|
|
165086
|
-
if (!gameId || !courseId
|
|
165087
|
-
throw ApiError.badRequest("Missing gameId
|
|
165808
|
+
synchronizeReviewBank = requireDeveloper(async (ctx) => {
|
|
165809
|
+
const { gameId, courseId } = ctx.params;
|
|
165810
|
+
if (!gameId || !courseId) {
|
|
165811
|
+
throw ApiError.badRequest("Missing gameId or courseId parameter");
|
|
165088
165812
|
}
|
|
165089
165813
|
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
165090
|
-
return ctx.services.timebackAssessments.
|
|
165814
|
+
return ctx.services.timebackAssessments.synchronizeReviewBank(integrationId);
|
|
165091
165815
|
});
|
|
165092
165816
|
reorderAssessments = requireDeveloper(async (ctx) => {
|
|
165093
165817
|
const { gameId, courseId } = ctx.params;
|
|
@@ -165238,7 +165962,7 @@ var init_timeback_controller = __esm(() => {
|
|
|
165238
165962
|
createAssessment,
|
|
165239
165963
|
attachExistingAssessments,
|
|
165240
165964
|
updateAssessment,
|
|
165241
|
-
|
|
165965
|
+
synchronizeReviewBank,
|
|
165242
165966
|
reorderAssessments,
|
|
165243
165967
|
removeAssessment,
|
|
165244
165968
|
reorderQuestions,
|