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