@playcademy/vite-plugin 1.3.1-beta.1 → 1.3.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/index.js +1134 -405
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -24317,7 +24317,7 @@ import path2 from "node:path";
|
|
|
24317
24317
|
// package.json
|
|
24318
24318
|
var package_default = {
|
|
24319
24319
|
name: "@playcademy/vite-plugin",
|
|
24320
|
-
version: "1.3.1-beta.
|
|
24320
|
+
version: "1.3.1-beta.2",
|
|
24321
24321
|
type: "module",
|
|
24322
24322
|
exports: {
|
|
24323
24323
|
".": {
|
|
@@ -25934,7 +25934,7 @@ var package_default2;
|
|
|
25934
25934
|
var init_package = __esm(() => {
|
|
25935
25935
|
package_default2 = {
|
|
25936
25936
|
name: "@playcademy/sandbox",
|
|
25937
|
-
version: "0.8.1-beta.
|
|
25937
|
+
version: "0.8.1-beta.2",
|
|
25938
25938
|
description: "Local development server for Playcademy game development",
|
|
25939
25939
|
type: "module",
|
|
25940
25940
|
exports: {
|
|
@@ -26324,6 +26324,9 @@ async function runWithConcurrency(items, concurrency, worker) {
|
|
|
26324
26324
|
}
|
|
26325
26325
|
return results;
|
|
26326
26326
|
}
|
|
26327
|
+
function reviewBankSinkIdentifier(integrationId) {
|
|
26328
|
+
return `playcademy-review-bank.${integrationId}`;
|
|
26329
|
+
}
|
|
26327
26330
|
function playcademySupportedQtiInteractionType(value) {
|
|
26328
26331
|
if (typeof value !== "string") {
|
|
26329
26332
|
return;
|
|
@@ -34411,9 +34414,12 @@ function classifyAssessmentSubmission(attempt, submissionId) {
|
|
|
34411
34414
|
}
|
|
34412
34415
|
return isAssessmentAttemptOpen(attempt) ? "submit" : "reject";
|
|
34413
34416
|
}
|
|
34417
|
+
function isSameAssessmentAward(recorded, input) {
|
|
34418
|
+
return recorded.xpAwarded === input.xpAwarded && recorded.masteredUnits === input.masteredUnits && recorded.masteredUnitsAbsolute === input.masteredUnitsAbsolute;
|
|
34419
|
+
}
|
|
34414
34420
|
function classifyAssessmentFinalization(attempt, input) {
|
|
34415
34421
|
if (isAssessmentAttemptCompleted(attempt)) {
|
|
34416
|
-
return attempt.submissionId === input.submissionId && attempt.
|
|
34422
|
+
return attempt.submissionId === input.submissionId && attempt.award !== undefined && isSameAssessmentAward(attempt.award, input) ? "replay" : "conflict";
|
|
34417
34423
|
}
|
|
34418
34424
|
if (!isAssessmentAttemptAwaitingAward(attempt) || attempt.submissionId !== input.submissionId) {
|
|
34419
34425
|
return "reject";
|
|
@@ -35894,7 +35900,7 @@ async function reviewBankRevision(input) {
|
|
|
35894
35900
|
})}`);
|
|
35895
35901
|
return `review-bank-v1:${bankRevisionUuid}`;
|
|
35896
35902
|
}
|
|
35897
|
-
async function
|
|
35903
|
+
async function buildReviewBankIndexFromReferences(input) {
|
|
35898
35904
|
return {
|
|
35899
35905
|
...input,
|
|
35900
35906
|
bankRevision: await reviewBankRevision(input)
|
|
@@ -35923,7 +35929,7 @@ async function buildReviewBankIndex(assessment, questions, options = {}) {
|
|
|
35923
35929
|
standards
|
|
35924
35930
|
};
|
|
35925
35931
|
});
|
|
35926
|
-
return
|
|
35932
|
+
return buildReviewBankIndexFromReferences({
|
|
35927
35933
|
bankIdentifier: assessment.identifier,
|
|
35928
35934
|
sourceContentRevision: assessment.contentRevision,
|
|
35929
35935
|
items
|
|
@@ -36028,7 +36034,7 @@ async function reviewBankIndexFromManifest(metadata2, input) {
|
|
|
36028
36034
|
}
|
|
36029
36035
|
}
|
|
36030
36036
|
}
|
|
36031
|
-
const index = await
|
|
36037
|
+
const index = await buildReviewBankIndexFromReferences({
|
|
36032
36038
|
bankIdentifier,
|
|
36033
36039
|
sourceContentRevision,
|
|
36034
36040
|
items: input.membershipItemIdentifiers.map((itemIdentifier) => ({
|
|
@@ -36383,6 +36389,18 @@ function isRoutedDiagnosticAttemptMetadata(value, itemSubmissions, responseVersi
|
|
|
36383
36389
|
function isReviewAttemptMetadata(value) {
|
|
36384
36390
|
return isRecord(value) && typeof value.requestFingerprint === "string" && typeof value.bankRevision === "string" && Array.isArray(value.standards) && value.standards.every(isAssessmentStandardRef) && Number.isInteger(value.candidateItemsPerStandard) && Array.isArray(value.selections) && value.selections.every((selection) => isRecord(selection) && isAssessmentStandardRef(selection.standard) && typeof selection.itemIdentifier === "string") && isReviewFulfillment(value.fulfillment);
|
|
36385
36391
|
}
|
|
36392
|
+
function isOptionalInteger(value) {
|
|
36393
|
+
return value === undefined || Number.isInteger(value);
|
|
36394
|
+
}
|
|
36395
|
+
function isMasteryWriteWarning(value) {
|
|
36396
|
+
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);
|
|
36397
|
+
}
|
|
36398
|
+
function isAssessmentAwardRecord(value) {
|
|
36399
|
+
if (value === undefined) {
|
|
36400
|
+
return true;
|
|
36401
|
+
}
|
|
36402
|
+
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));
|
|
36403
|
+
}
|
|
36386
36404
|
function isMasteryAttemptMetadata(value) {
|
|
36387
36405
|
return isRecord(value) && typeof value.requestFingerprint === "string" && isAssessmentStandardRef(value.standard);
|
|
36388
36406
|
}
|
|
@@ -36407,7 +36425,7 @@ function isPlaycademyAssessmentResultMetadataV1(value) {
|
|
|
36407
36425
|
const review = metadata2.review;
|
|
36408
36426
|
const diagnostic = metadata2.diagnostic;
|
|
36409
36427
|
const routedDiagnostic = metadata2.purpose === "diagnostic" && diagnostic !== undefined && Array.isArray(metadata2.itemSubmissions) && Number.isInteger(metadata2.responseVersion) && isRoutedDiagnosticAttemptMetadata(diagnostic, metadata2.itemSubmissions, metadata2.responseVersion);
|
|
36410
|
-
return metadata2.version === 1 && typeof metadata2.activityId === "string" && isAssessmentPurpose(metadata2.purpose) && typeof metadata2.courseId === "string" && typeof metadata2.integrationId === "string" && typeof metadata2.enrollmentId === "string" && Boolean(selectedTest) && (selectedTest?.assessmentKey === undefined || isNonEmptyString(selectedTest.assessmentKey)) && typeof selectedTest?.identifier === "string" && typeof selectedTest.contentRevision === "string" && Number.isInteger(metadata2.attemptNumber) && Number.isInteger(metadata2.responseVersion) && Boolean(metadata2.responses) && typeof metadata2.responses === "object" && Array.isArray(metadata2.itemSubmissions) && metadata2.itemSubmissions.every(isAssessmentItemSubmission) && typeof metadata2.startedAt === "string" && typeof metadata2.updatedAt === "string" && (metadata2.finalizedAt === undefined || typeof metadata2.finalizedAt === "string") && (metadata2.purpose === "mastery" ? isMasteryAttemptMetadata(mastery) : mastery === undefined) && (metadata2.purpose === "review" ? isReviewAttemptMetadata(review) : review === undefined) && (metadata2.purpose === "diagnostic" ? diagnostic === undefined || routedDiagnostic : diagnostic === undefined) && (metadata2.submissionId === undefined || typeof metadata2.submissionId === "string") && (metadata2.score === undefined || isAssessmentScore(metadata2.score)) && isCompletionMetadata(metadata2.completion, metadata2.purpose, routedDiagnostic);
|
|
36428
|
+
return metadata2.version === 1 && typeof metadata2.activityId === "string" && isAssessmentPurpose(metadata2.purpose) && typeof metadata2.courseId === "string" && typeof metadata2.integrationId === "string" && typeof metadata2.enrollmentId === "string" && Boolean(selectedTest) && (selectedTest?.assessmentKey === undefined || isNonEmptyString(selectedTest.assessmentKey)) && typeof selectedTest?.identifier === "string" && typeof selectedTest.contentRevision === "string" && Number.isInteger(metadata2.attemptNumber) && Number.isInteger(metadata2.responseVersion) && Boolean(metadata2.responses) && typeof metadata2.responses === "object" && Array.isArray(metadata2.itemSubmissions) && metadata2.itemSubmissions.every(isAssessmentItemSubmission) && typeof metadata2.startedAt === "string" && typeof metadata2.updatedAt === "string" && (metadata2.finalizedAt === undefined || typeof metadata2.finalizedAt === "string") && isAssessmentAwardRecord(metadata2.award) && (metadata2.purpose === "mastery" ? isMasteryAttemptMetadata(mastery) : mastery === undefined) && (metadata2.purpose === "review" ? isReviewAttemptMetadata(review) : review === undefined) && (metadata2.purpose === "diagnostic" ? diagnostic === undefined || routedDiagnostic : diagnostic === undefined) && (metadata2.submissionId === undefined || typeof metadata2.submissionId === "string") && (metadata2.score === undefined || isAssessmentScore(metadata2.score)) && isCompletionMetadata(metadata2.completion, metadata2.purpose, routedDiagnostic);
|
|
36411
36429
|
}
|
|
36412
36430
|
function isPlaycademyReviewAssessmentItemResultMetadataV1(value) {
|
|
36413
36431
|
if (!isRecord(value) || !isRecord(value.responses)) {
|
|
@@ -36927,7 +36945,12 @@ var init_assessment_runtime2 = __esm(() => {
|
|
|
36927
36945
|
});
|
|
36928
36946
|
FinalizeAssessmentBodySchema = exports_external.object({
|
|
36929
36947
|
submissionId: exports_external.string().trim().min(1),
|
|
36930
|
-
xpAwarded: exports_external.number().finite().nonnegative()
|
|
36948
|
+
xpAwarded: exports_external.number().finite().nonnegative(),
|
|
36949
|
+
masteredUnits: exports_external.number().finite().optional(),
|
|
36950
|
+
masteredUnitsAbsolute: exports_external.number().finite().int().optional()
|
|
36951
|
+
}).refine((body2) => body2.masteredUnits === undefined || body2.masteredUnitsAbsolute === undefined, {
|
|
36952
|
+
message: "Provide either masteredUnits or masteredUnitsAbsolute, not both",
|
|
36953
|
+
path: ["masteredUnits"]
|
|
36931
36954
|
});
|
|
36932
36955
|
StartRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.and(StartAssessmentBodySchema);
|
|
36933
36956
|
SaveRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.extend(SaveAssessmentBodySchema.shape);
|
|
@@ -36936,10 +36959,10 @@ var init_assessment_runtime2 = __esm(() => {
|
|
|
36936
36959
|
appName: exports_external.string().trim().min(1),
|
|
36937
36960
|
sensorUrl: exports_external.string().url()
|
|
36938
36961
|
});
|
|
36939
|
-
FinalizeRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.extend(
|
|
36962
|
+
FinalizeRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.extend({
|
|
36940
36963
|
appName: exports_external.string().trim().min(1),
|
|
36941
36964
|
sensorUrl: exports_external.string().url()
|
|
36942
|
-
});
|
|
36965
|
+
}).and(FinalizeAssessmentBodySchema);
|
|
36943
36966
|
});
|
|
36944
36967
|
var DOMAIN_CODE_BY_STATUS;
|
|
36945
36968
|
var AssessmentRuntimeError;
|
|
@@ -71642,6 +71665,15 @@ function requireMasteryStandard(input, context2) {
|
|
|
71642
71665
|
});
|
|
71643
71666
|
}
|
|
71644
71667
|
}
|
|
71668
|
+
function rejectSystemManagedReview(input, context2) {
|
|
71669
|
+
if (input.purpose === "review") {
|
|
71670
|
+
context2.addIssue({
|
|
71671
|
+
code: "custom",
|
|
71672
|
+
path: ["purpose"],
|
|
71673
|
+
message: "The review bank is system-managed and cannot be managed manually"
|
|
71674
|
+
});
|
|
71675
|
+
}
|
|
71676
|
+
}
|
|
71645
71677
|
var TimebackGradeSchema;
|
|
71646
71678
|
var TimebackSubjectSchema;
|
|
71647
71679
|
var CourseGoalsSchema;
|
|
@@ -71946,7 +71978,10 @@ var init_schemas4 = __esm(() => {
|
|
|
71946
71978
|
title: exports_external.string().min(1, "Assessment title is required"),
|
|
71947
71979
|
purpose: AssessmentPurposeSchema,
|
|
71948
71980
|
standard: AssessmentStandardRefSchema2.optional()
|
|
71949
|
-
}).superRefine(
|
|
71981
|
+
}).superRefine((input, context2) => {
|
|
71982
|
+
requireMasteryStandard(input, context2);
|
|
71983
|
+
rejectSystemManagedReview(input, context2);
|
|
71984
|
+
});
|
|
71950
71985
|
UpdateAssessmentRequestSchema = exports_external.object({
|
|
71951
71986
|
title: exports_external.string().trim().min(1, "Assessment title is required").optional(),
|
|
71952
71987
|
purpose: AssessmentPurposeSchema.optional(),
|
|
@@ -71955,13 +71990,16 @@ var init_schemas4 = __esm(() => {
|
|
|
71955
71990
|
status: AssessmentStatusSchema.optional()
|
|
71956
71991
|
}).refine((input) => input.title !== undefined || input.purpose !== undefined || input.standard !== undefined || input.diagnostic !== undefined || input.status !== undefined, {
|
|
71957
71992
|
message: "Title, purpose, standard, diagnostic, or status is required"
|
|
71958
|
-
});
|
|
71993
|
+
}).superRefine(rejectSystemManagedReview);
|
|
71959
71994
|
CopyAssessmentRequestSchema = exports_external.object({
|
|
71960
71995
|
assessmentKey: AssessmentKeySchema,
|
|
71961
71996
|
testIdentifier: exports_external.string().trim().min(1, "Assessment identifier is required"),
|
|
71962
71997
|
purpose: AssessmentPurposeSchema,
|
|
71963
71998
|
standard: AssessmentStandardRefSchema2.optional()
|
|
71964
|
-
}).superRefine(
|
|
71999
|
+
}).superRefine((input, context2) => {
|
|
72000
|
+
requireMasteryStandard(input, context2);
|
|
72001
|
+
rejectSystemManagedReview(input, context2);
|
|
72002
|
+
});
|
|
71965
72003
|
AssessmentAssociationImportEntrySchema = exports_external.object({
|
|
71966
72004
|
assessmentKey: AssessmentKeySchema,
|
|
71967
72005
|
qtiTestIdentifier: exports_external.string().trim().min(1, "Assessment identifier is required"),
|
|
@@ -71970,6 +72008,7 @@ var init_schemas4 = __esm(() => {
|
|
|
71970
72008
|
sortOrder: exports_external.number().int().nonnegative().nullable()
|
|
71971
72009
|
}).superRefine((input, context2) => {
|
|
71972
72010
|
requireMasteryStandard(input, context2);
|
|
72011
|
+
rejectSystemManagedReview(input, context2);
|
|
71973
72012
|
if (input.purpose !== "mastery" && input.standard !== undefined) {
|
|
71974
72013
|
context2.addIssue({
|
|
71975
72014
|
code: "custom",
|
|
@@ -72019,7 +72058,7 @@ var init_schemas4 = __esm(() => {
|
|
|
72019
72058
|
ReorderAssessmentsRequestSchema = exports_external.object({
|
|
72020
72059
|
purpose: AssessmentPurposeSchema,
|
|
72021
72060
|
testIdentifiers: exports_external.array(exports_external.string().trim().min(1, "Assessment identifier is required")).min(1, "At least one assessment is required")
|
|
72022
|
-
});
|
|
72061
|
+
}).superRefine(rejectSystemManagedReview);
|
|
72023
72062
|
ReorderQuestionsRequestSchema = exports_external.object({
|
|
72024
72063
|
items: exports_external.array(exports_external.object({
|
|
72025
72064
|
identifier: exports_external.string().min(1),
|
|
@@ -116646,6 +116685,7 @@ function buildAssessmentCompletionEvent(input) {
|
|
|
116646
116685
|
totalQuestions: input.totalQuestions,
|
|
116647
116686
|
correctQuestions: input.correctQuestions,
|
|
116648
116687
|
xpEarned: input.xpAwarded,
|
|
116688
|
+
...input.masteredUnits ? { masteredUnits: input.masteredUnits } : {},
|
|
116649
116689
|
attemptNumber: input.attemptNumber,
|
|
116650
116690
|
process: false,
|
|
116651
116691
|
subject: input.subject,
|
|
@@ -116654,7 +116694,10 @@ function buildAssessmentCompletionEvent(input) {
|
|
|
116654
116694
|
eventId: input.eventId,
|
|
116655
116695
|
eventTime: input.submittedAt,
|
|
116656
116696
|
runId: input.attemptId,
|
|
116657
|
-
generatedExtensions:
|
|
116697
|
+
generatedExtensions: {
|
|
116698
|
+
...metadata2,
|
|
116699
|
+
...input.pctCompleteApp !== undefined ? { pctCompleteApp: input.pctCompleteApp } : {}
|
|
116700
|
+
},
|
|
116658
116701
|
eventExtensions: {
|
|
116659
116702
|
playcademy: {
|
|
116660
116703
|
assessmentPurpose: input.purpose,
|
|
@@ -116708,6 +116751,85 @@ function deriveSourcedIds(courseId) {
|
|
|
116708
116751
|
componentResource: `${courseId}-cr`
|
|
116709
116752
|
};
|
|
116710
116753
|
}
|
|
116754
|
+
function hasMasteryAwardRequest(request) {
|
|
116755
|
+
return request.masteredUnits !== undefined || request.masteredUnitsAbsolute !== undefined;
|
|
116756
|
+
}
|
|
116757
|
+
|
|
116758
|
+
class ActivityMasteryAward {
|
|
116759
|
+
mastery;
|
|
116760
|
+
events;
|
|
116761
|
+
constructor(mastery, events) {
|
|
116762
|
+
this.mastery = mastery;
|
|
116763
|
+
this.events = events;
|
|
116764
|
+
}
|
|
116765
|
+
async evaluate(input) {
|
|
116766
|
+
const ids = deriveSourcedIds(input.courseId);
|
|
116767
|
+
const progress = await this.mastery.checkProgress({
|
|
116768
|
+
studentId: input.studentId,
|
|
116769
|
+
courseId: input.courseId,
|
|
116770
|
+
resourceId: ids.resource,
|
|
116771
|
+
masteredUnits: input.masteredUnits ?? 0,
|
|
116772
|
+
masteredUnitsAbsolute: input.masteredUnitsAbsolute
|
|
116773
|
+
});
|
|
116774
|
+
if (!progress) {
|
|
116775
|
+
return {
|
|
116776
|
+
masteredUnitsApplied: input.masteredUnits ?? 0,
|
|
116777
|
+
masteryAchieved: false,
|
|
116778
|
+
masteryRevoked: false
|
|
116779
|
+
};
|
|
116780
|
+
}
|
|
116781
|
+
return {
|
|
116782
|
+
masteredUnitsApplied: progress.effectiveDelta,
|
|
116783
|
+
pctCompleteApp: progress.pctCompleteApp,
|
|
116784
|
+
masteryAchieved: progress.masteryAchieved,
|
|
116785
|
+
masteryRevoked: progress.masteryRevoked,
|
|
116786
|
+
...progress.writeWarning ? { warnings: [progress.writeWarning] } : {}
|
|
116787
|
+
};
|
|
116788
|
+
}
|
|
116789
|
+
async settle(evaluation, input) {
|
|
116790
|
+
let completionEntryWritten = false;
|
|
116791
|
+
if (evaluation.masteryAchieved) {
|
|
116792
|
+
setAttributes({ "app.timeback.mastery_achieved": true });
|
|
116793
|
+
completionEntryWritten = await this.mastery.createCompletionEntry(input.studentId, input.courseId, input.classId, input.appName);
|
|
116794
|
+
await this.emitCourseCompletionHistoryEvent(input);
|
|
116795
|
+
}
|
|
116796
|
+
if (evaluation.masteryRevoked) {
|
|
116797
|
+
await this.mastery.revokeCompletionEntry(input.studentId, input.courseId, input.classId, input.appName);
|
|
116798
|
+
}
|
|
116799
|
+
return { completionEntryWritten };
|
|
116800
|
+
}
|
|
116801
|
+
async emitCourseCompletionHistoryEvent(input) {
|
|
116802
|
+
const ids = deriveSourcedIds(input.courseId);
|
|
116803
|
+
await this.events.emitActivityEvent({
|
|
116804
|
+
eventId: input.completionHistoryEventId,
|
|
116805
|
+
studentId: input.studentId,
|
|
116806
|
+
studentEmail: input.studentEmail,
|
|
116807
|
+
gameId: input.gameId,
|
|
116808
|
+
activityId: input.activityId,
|
|
116809
|
+
activityName: "Course completed",
|
|
116810
|
+
courseId: ids.course,
|
|
116811
|
+
courseName: input.courseName,
|
|
116812
|
+
subject: input.subject,
|
|
116813
|
+
appName: input.appName,
|
|
116814
|
+
sensorUrl: input.sensorUrl,
|
|
116815
|
+
process: false,
|
|
116816
|
+
includeAttempt: false,
|
|
116817
|
+
eventExtensions: {
|
|
116818
|
+
playcademy: {
|
|
116819
|
+
eventKind: "course-completed",
|
|
116820
|
+
source: "gameplay"
|
|
116821
|
+
}
|
|
116822
|
+
},
|
|
116823
|
+
generatedExtensions: {
|
|
116824
|
+
playcademy: {
|
|
116825
|
+
eventKind: "course-completed",
|
|
116826
|
+
source: "gameplay",
|
|
116827
|
+
activityId: input.activityId
|
|
116828
|
+
}
|
|
116829
|
+
}
|
|
116830
|
+
}).catch(catchEvent("timeback.caliper_completion_event_failed"));
|
|
116831
|
+
}
|
|
116832
|
+
}
|
|
116711
116833
|
function validateProgressData(progressData) {
|
|
116712
116834
|
if (!progressData.subject) {
|
|
116713
116835
|
throw new ConfigurationError("subject", "Subject is required for Caliper events. Provide it in progressData.subject");
|
|
@@ -116723,12 +116845,12 @@ function validateProgressData(progressData) {
|
|
|
116723
116845
|
class ActivityRecord {
|
|
116724
116846
|
core;
|
|
116725
116847
|
students;
|
|
116726
|
-
|
|
116848
|
+
masteryAward;
|
|
116727
116849
|
events;
|
|
116728
|
-
constructor(core3, students,
|
|
116850
|
+
constructor(core3, students, masteryAward, events) {
|
|
116729
116851
|
this.core = core3;
|
|
116730
116852
|
this.students = students;
|
|
116731
|
-
this.
|
|
116853
|
+
this.masteryAward = masteryAward;
|
|
116732
116854
|
this.events = events;
|
|
116733
116855
|
}
|
|
116734
116856
|
async record(courseId, studentIdentifier, progressData) {
|
|
@@ -116754,52 +116876,32 @@ class ActivityRecord {
|
|
|
116754
116876
|
throw error88;
|
|
116755
116877
|
}
|
|
116756
116878
|
const legacyLineItemId = `${ids.course}-${activityId}-assessment`;
|
|
116757
|
-
const [currentAttemptNumber,
|
|
116879
|
+
const [currentAttemptNumber, mastery] = await Promise.all([
|
|
116758
116880
|
this.resolveAttemptNumber(attemptNumber, studentId, caliperLineItemId, legacyLineItemId),
|
|
116759
|
-
this.
|
|
116881
|
+
this.masteryAward.evaluate({
|
|
116760
116882
|
studentId,
|
|
116761
116883
|
courseId,
|
|
116762
|
-
|
|
116763
|
-
masteredUnits: progressData.masteredUnits ?? 0,
|
|
116884
|
+
masteredUnits: progressData.masteredUnits,
|
|
116764
116885
|
masteredUnitsAbsolute: progressData.masteredUnitsAbsolute
|
|
116765
116886
|
})
|
|
116766
116887
|
]);
|
|
116767
116888
|
setAttributes({ "app.timeback.attempt_number": currentAttemptNumber });
|
|
116768
|
-
|
|
116769
|
-
const
|
|
116770
|
-
|
|
116771
|
-
|
|
116772
|
-
|
|
116773
|
-
|
|
116774
|
-
|
|
116775
|
-
|
|
116776
|
-
|
|
116777
|
-
|
|
116778
|
-
|
|
116779
|
-
|
|
116780
|
-
|
|
116781
|
-
|
|
116782
|
-
|
|
116783
|
-
|
|
116784
|
-
}
|
|
116785
|
-
if (masteryAchieved) {
|
|
116786
|
-
completionEntryWritten = await this.mastery.createCompletionEntry(studentId, courseId, progressData.classId, progressData.appName);
|
|
116787
|
-
await this.emitCourseCompletionHistoryEvent({
|
|
116788
|
-
studentId,
|
|
116789
|
-
studentEmail,
|
|
116790
|
-
gameId: progressData.gameId,
|
|
116791
|
-
activityId,
|
|
116792
|
-
courseId: ids.course,
|
|
116793
|
-
courseName,
|
|
116794
|
-
subject: progressData.subject,
|
|
116795
|
-
appName: progressData.appName,
|
|
116796
|
-
sensorUrl: progressData.sensorUrl,
|
|
116797
|
-
eventId: progressData.completionHistoryEventId
|
|
116798
|
-
});
|
|
116799
|
-
}
|
|
116800
|
-
if (masteryProgress?.masteryRevoked) {
|
|
116801
|
-
await this.mastery.revokeCompletionEntry(studentId, courseId, progressData.classId, progressData.appName);
|
|
116802
|
-
}
|
|
116889
|
+
const effectiveMasteredUnits = mastery.masteredUnitsApplied;
|
|
116890
|
+
const { pctCompleteApp, warnings } = mastery;
|
|
116891
|
+
const extensions = pctCompleteApp !== undefined ? { ...progressData.extensions, pctCompleteApp } : progressData.extensions;
|
|
116892
|
+
const { completionEntryWritten } = await this.masteryAward.settle(mastery, {
|
|
116893
|
+
studentId,
|
|
116894
|
+
studentEmail,
|
|
116895
|
+
courseId,
|
|
116896
|
+
classId: progressData.classId,
|
|
116897
|
+
gameId: progressData.gameId,
|
|
116898
|
+
activityId,
|
|
116899
|
+
courseName,
|
|
116900
|
+
subject: progressData.subject,
|
|
116901
|
+
appName: progressData.appName,
|
|
116902
|
+
sensorUrl: progressData.sensorUrl,
|
|
116903
|
+
completionHistoryEventId: progressData.completionHistoryEventId
|
|
116904
|
+
});
|
|
116803
116905
|
try {
|
|
116804
116906
|
await this.events.emitActivityEvent({
|
|
116805
116907
|
studentId,
|
|
@@ -116820,7 +116922,7 @@ class ActivityRecord {
|
|
|
116820
116922
|
appName: progressData.appName,
|
|
116821
116923
|
sensorUrl: progressData.sensorUrl,
|
|
116822
116924
|
eventId: progressData.eventId,
|
|
116823
|
-
extensions
|
|
116925
|
+
extensions,
|
|
116824
116926
|
...progressData.runId ? { runId: progressData.runId } : {}
|
|
116825
116927
|
}).catch((error88) => {
|
|
116826
116928
|
setAttributes({ "app.timeback.caliper_emit_failed": true });
|
|
@@ -116841,36 +116943,6 @@ class ActivityRecord {
|
|
|
116841
116943
|
...warnings ? { warnings } : {}
|
|
116842
116944
|
};
|
|
116843
116945
|
}
|
|
116844
|
-
async emitCourseCompletionHistoryEvent(data) {
|
|
116845
|
-
await this.events.emitActivityEvent({
|
|
116846
|
-
eventId: data.eventId,
|
|
116847
|
-
studentId: data.studentId,
|
|
116848
|
-
studentEmail: data.studentEmail,
|
|
116849
|
-
gameId: data.gameId,
|
|
116850
|
-
activityId: data.activityId,
|
|
116851
|
-
activityName: "Course completed",
|
|
116852
|
-
courseId: data.courseId,
|
|
116853
|
-
courseName: data.courseName,
|
|
116854
|
-
subject: data.subject,
|
|
116855
|
-
appName: data.appName,
|
|
116856
|
-
sensorUrl: data.sensorUrl,
|
|
116857
|
-
process: false,
|
|
116858
|
-
includeAttempt: false,
|
|
116859
|
-
eventExtensions: {
|
|
116860
|
-
playcademy: {
|
|
116861
|
-
eventKind: "course-completed",
|
|
116862
|
-
source: "gameplay"
|
|
116863
|
-
}
|
|
116864
|
-
},
|
|
116865
|
-
generatedExtensions: {
|
|
116866
|
-
playcademy: {
|
|
116867
|
-
eventKind: "course-completed",
|
|
116868
|
-
source: "gameplay",
|
|
116869
|
-
activityId: data.activityId
|
|
116870
|
-
}
|
|
116871
|
-
}
|
|
116872
|
-
}).catch(catchEvent("timeback.caliper_completion_event_failed"));
|
|
116873
|
-
}
|
|
116874
116946
|
async resolveAttemptNumber(providedAttemptNumber, studentId, caliperLineItemId, legacyLineItemId) {
|
|
116875
116947
|
if (providedAttemptNumber) {
|
|
116876
116948
|
setAttributes({ "app.timeback.attempt_source": "provided" });
|
|
@@ -117268,9 +117340,12 @@ class ActivityEvents {
|
|
|
117268
117340
|
}
|
|
117269
117341
|
}
|
|
117270
117342
|
function createActivityNamespace(core3, deps) {
|
|
117271
|
-
const
|
|
117343
|
+
const masteryAward = new ActivityMasteryAward(deps.mastery, deps.events);
|
|
117344
|
+
const record3 = new ActivityRecord(core3, deps.students, masteryAward, deps.events);
|
|
117272
117345
|
const session2 = new ActivitySession(deps.students, deps.events);
|
|
117273
117346
|
return {
|
|
117347
|
+
evaluateMasteryAward: (input) => masteryAward.evaluate(input),
|
|
117348
|
+
settleMasteryAward: (evaluation, input) => masteryAward.settle(evaluation, input),
|
|
117274
117349
|
record: (courseId, studentIdentifier, progressData) => record3.record(courseId, studentIdentifier, progressData),
|
|
117275
117350
|
session: (courseId, studentIdentifier, sessionData) => session2.record(courseId, studentIdentifier, sessionData),
|
|
117276
117351
|
listEvents: (params) => deps.events.listEvents(params),
|
|
@@ -118560,6 +118635,7 @@ var StudentNotFoundError;
|
|
|
118560
118635
|
var ConfigurationError;
|
|
118561
118636
|
var UUID_REGEX2;
|
|
118562
118637
|
var ONEROSTER_PATHS;
|
|
118638
|
+
var NO_MASTERY_AWARD;
|
|
118563
118639
|
var TIMEBACK_API_URLS;
|
|
118564
118640
|
var QTI_API_URL = "https://qti.alpha-1edtech.ai/api";
|
|
118565
118641
|
var TIMEBACK_AUTH_URLS;
|
|
@@ -118576,6 +118652,7 @@ var GRADE_VALUES3;
|
|
|
118576
118652
|
var MASTERY_WRITE_CAPPED_WARNING_CODE = "MASTERY_WRITE_CAPPED";
|
|
118577
118653
|
var EmailSchema;
|
|
118578
118654
|
var init_dist5 = __esm(async () => {
|
|
118655
|
+
init_spans();
|
|
118579
118656
|
init_spans();
|
|
118580
118657
|
init_src();
|
|
118581
118658
|
init_spans();
|
|
@@ -118636,6 +118713,11 @@ var init_dist5 = __esm(async () => {
|
|
|
118636
118713
|
courses: "/ims/oneroster/rostering/v1p2/courses",
|
|
118637
118714
|
componentResources: "/ims/oneroster/rostering/v1p2/courses/component-resources"
|
|
118638
118715
|
};
|
|
118716
|
+
NO_MASTERY_AWARD = {
|
|
118717
|
+
masteredUnitsApplied: 0,
|
|
118718
|
+
masteryAchieved: false,
|
|
118719
|
+
masteryRevoked: false
|
|
118720
|
+
};
|
|
118639
118721
|
TIMEBACK_API_URLS = {
|
|
118640
118722
|
production: "https://api.alpha-1edtech.ai",
|
|
118641
118723
|
staging: "https://api.staging.alpha-1edtech.com"
|
|
@@ -121390,6 +121472,62 @@ function stringField2(value) {
|
|
|
121390
121472
|
function firstStringField2(...values) {
|
|
121391
121473
|
return values.map(stringField2).find(Boolean) ?? "";
|
|
121392
121474
|
}
|
|
121475
|
+
function normalizedWhitespace2(value) {
|
|
121476
|
+
return value.normalize("NFKC").trim().replace(/\s+/g, " ");
|
|
121477
|
+
}
|
|
121478
|
+
function normalizedIdentityCase2(value) {
|
|
121479
|
+
return value.toLocaleUpperCase("en-US");
|
|
121480
|
+
}
|
|
121481
|
+
function frameworkAliasKey2(value) {
|
|
121482
|
+
return value.normalize("NFKC").toLocaleLowerCase("en-US").replace(/[^a-z0-9]+/g, "");
|
|
121483
|
+
}
|
|
121484
|
+
function isCommonCoreMathIdentifier2(identifier) {
|
|
121485
|
+
const canonical = normalizedIdentityCase2(normalizedWhitespace2(identifier));
|
|
121486
|
+
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);
|
|
121487
|
+
}
|
|
121488
|
+
function isCommonCoreElaIdentifier2(identifier) {
|
|
121489
|
+
const canonical = normalizedIdentityCase2(normalizedWhitespace2(identifier));
|
|
121490
|
+
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);
|
|
121491
|
+
}
|
|
121492
|
+
function canonicalFramework2(authoredFramework, identifier) {
|
|
121493
|
+
const aliasKey = frameworkAliasKey2(authoredFramework);
|
|
121494
|
+
if (COMMON_CORE_MATH_FRAMEWORK_ALIASES2.has(aliasKey) || COMMON_CORE_FRAMEWORK_ALIASES2.has(aliasKey) && isCommonCoreMathIdentifier2(identifier)) {
|
|
121495
|
+
return "CCSS.Math";
|
|
121496
|
+
}
|
|
121497
|
+
if (COMMON_CORE_ELA_FRAMEWORK_ALIASES2.has(aliasKey) || COMMON_CORE_FRAMEWORK_ALIASES2.has(aliasKey) && isCommonCoreElaIdentifier2(identifier)) {
|
|
121498
|
+
return "CCSS.ELA-Literacy";
|
|
121499
|
+
}
|
|
121500
|
+
if (COMMON_CORE_FRAMEWORK_ALIASES2.has(aliasKey)) {
|
|
121501
|
+
return "CCSS";
|
|
121502
|
+
}
|
|
121503
|
+
return normalizedIdentityCase2(authoredFramework);
|
|
121504
|
+
}
|
|
121505
|
+
function canonicalIdentifier2(framework, authoredIdentifier) {
|
|
121506
|
+
const identifier = normalizedIdentityCase2(normalizedWhitespace2(authoredIdentifier));
|
|
121507
|
+
if (framework === "CCSS.Math") {
|
|
121508
|
+
return identifier.replace(/^CCSS\.MATH\.CONTENT\./, "").replace(/^CCSS\.MATH\.PRACTICE\./, "").replace(/^CCSS\.MATH\./, "");
|
|
121509
|
+
}
|
|
121510
|
+
if (framework === "CCSS.ELA-Literacy") {
|
|
121511
|
+
return identifier.replace(/^CCSS\.ELA-LITERACY\./, "");
|
|
121512
|
+
}
|
|
121513
|
+
return identifier;
|
|
121514
|
+
}
|
|
121515
|
+
function canonicalAssessmentStandardRef2(standard) {
|
|
121516
|
+
const authoredFramework = normalizedWhitespace2(standard.framework);
|
|
121517
|
+
const framework = canonicalFramework2(authoredFramework, standard.identifier);
|
|
121518
|
+
const identifier = canonicalIdentifier2(framework, standard.identifier);
|
|
121519
|
+
return {
|
|
121520
|
+
framework,
|
|
121521
|
+
identifier
|
|
121522
|
+
};
|
|
121523
|
+
}
|
|
121524
|
+
function assessmentStandardRefKey2(standard) {
|
|
121525
|
+
const canonical = canonicalAssessmentStandardRef2(standard);
|
|
121526
|
+
return JSON.stringify([
|
|
121527
|
+
normalizedIdentityCase2(canonical.framework),
|
|
121528
|
+
normalizedIdentityCase2(canonical.identifier)
|
|
121529
|
+
]);
|
|
121530
|
+
}
|
|
121393
121531
|
function dedupeStandards2(standards) {
|
|
121394
121532
|
const deduped = new Map;
|
|
121395
121533
|
for (const standard of standards) {
|
|
@@ -124161,6 +124299,22 @@ var init_locks = __esm(() => {
|
|
|
124161
124299
|
}
|
|
124162
124300
|
};
|
|
124163
124301
|
});
|
|
124302
|
+
async function crossAssessmentAttemptLockBarrier(lock, db2, attemptId) {
|
|
124303
|
+
const maxAttempts = 5;
|
|
124304
|
+
for (let attempt = 1;attempt <= maxAttempts; attempt += 1) {
|
|
124305
|
+
try {
|
|
124306
|
+
await lock(db2, [assessmentRuntimeAttemptLockKey(attemptId)], async () => {
|
|
124307
|
+
return;
|
|
124308
|
+
});
|
|
124309
|
+
return;
|
|
124310
|
+
} catch (error88) {
|
|
124311
|
+
if (!(error88 instanceof ServiceUnavailableError) || attempt === maxAttempts) {
|
|
124312
|
+
throw error88;
|
|
124313
|
+
}
|
|
124314
|
+
await sleep(25);
|
|
124315
|
+
}
|
|
124316
|
+
}
|
|
124317
|
+
}
|
|
124164
124318
|
function normalizeKeys(keys) {
|
|
124165
124319
|
return [...new Set(keys)].toSorted();
|
|
124166
124320
|
}
|
|
@@ -125176,6 +125330,97 @@ var init_timeback_qti_authoring_util = __esm(() => {
|
|
|
125176
125330
|
buildMatchQuestionXml
|
|
125177
125331
|
];
|
|
125178
125332
|
});
|
|
125333
|
+
function assessmentPreparationContentionError(expectedResponseVersion, responseVersion, kind) {
|
|
125334
|
+
if (expectedResponseVersion === responseVersion) {
|
|
125335
|
+
return new ServiceUnavailableError(`The ${kind} response state changed repeatedly while this item was being prepared. Try again shortly.`, {
|
|
125336
|
+
retryable: true,
|
|
125337
|
+
reason: "PREPARATION_CONTENTION",
|
|
125338
|
+
expectedResponseVersion,
|
|
125339
|
+
responseVersion
|
|
125340
|
+
});
|
|
125341
|
+
}
|
|
125342
|
+
return AssessmentRuntimeError.from(assessmentResponseVersionConflict(expectedResponseVersion, responseVersion));
|
|
125343
|
+
}
|
|
125344
|
+
function assessmentTrackableCourse(integration) {
|
|
125345
|
+
if (!isTimebackGrade2(integration.grade) || !isTimebackSubject(integration.subject)) {
|
|
125346
|
+
return null;
|
|
125347
|
+
}
|
|
125348
|
+
return { grade: integration.grade, subject: integration.subject };
|
|
125349
|
+
}
|
|
125350
|
+
function buildAssessmentActivityData(input) {
|
|
125351
|
+
if (!input.course) {
|
|
125352
|
+
return;
|
|
125353
|
+
}
|
|
125354
|
+
return {
|
|
125355
|
+
activityId: input.activityId,
|
|
125356
|
+
activityName: input.activityName,
|
|
125357
|
+
...input.course,
|
|
125358
|
+
courseId: input.courseId
|
|
125359
|
+
};
|
|
125360
|
+
}
|
|
125361
|
+
function buildDiagnosticAssessmentSnapshot(input) {
|
|
125362
|
+
return {
|
|
125363
|
+
attemptId: input.attemptId,
|
|
125364
|
+
responseVersion: input.metadata.responseVersion,
|
|
125365
|
+
status: "in_progress",
|
|
125366
|
+
flow: "platform-routed-item-submit",
|
|
125367
|
+
...input.activityData ? { activityData: input.activityData } : {},
|
|
125368
|
+
assessment: assessmentPresentationForAttempt(input.assessment, input.attemptId),
|
|
125369
|
+
selection: {
|
|
125370
|
+
kind: "platform-routed-diagnostic",
|
|
125371
|
+
purpose: "diagnostic",
|
|
125372
|
+
definitionId: input.metadata.diagnostic.definitionId,
|
|
125373
|
+
diagnosticKey: input.metadata.diagnostic.diagnosticKey,
|
|
125374
|
+
routingRevision: input.metadata.diagnostic.routingRevision
|
|
125375
|
+
},
|
|
125376
|
+
routing: projectDiagnosticRoutingSnapshot(input.metadata.diagnostic.routingRevision, input.state),
|
|
125377
|
+
completion: null
|
|
125378
|
+
};
|
|
125379
|
+
}
|
|
125380
|
+
function buildConventionalAssessmentSnapshot(input) {
|
|
125381
|
+
const metadata2 = input.metadata;
|
|
125382
|
+
let selection;
|
|
125383
|
+
if (metadata2.purpose === "review") {
|
|
125384
|
+
selection = {
|
|
125385
|
+
kind: "standards-review",
|
|
125386
|
+
purpose: metadata2.purpose,
|
|
125387
|
+
standards: [...metadata2.review.standards],
|
|
125388
|
+
candidateItemsPerStandard: metadata2.review.candidateItemsPerStandard,
|
|
125389
|
+
selections: [...metadata2.review.selections],
|
|
125390
|
+
fulfillment: metadata2.review.fulfillment
|
|
125391
|
+
};
|
|
125392
|
+
} else if (metadata2.purpose === "mastery") {
|
|
125393
|
+
selection = {
|
|
125394
|
+
kind: "standard-quiz",
|
|
125395
|
+
purpose: metadata2.purpose,
|
|
125396
|
+
standard: metadata2.mastery.standard
|
|
125397
|
+
};
|
|
125398
|
+
} else {
|
|
125399
|
+
selection = { kind: "fixed-test", purpose: metadata2.purpose };
|
|
125400
|
+
}
|
|
125401
|
+
return {
|
|
125402
|
+
attemptId: input.attemptId,
|
|
125403
|
+
responseVersion: metadata2.responseVersion,
|
|
125404
|
+
status: "in_progress",
|
|
125405
|
+
flow: assessmentFlowForPurpose(metadata2.purpose),
|
|
125406
|
+
...input.activityData ? { activityData: input.activityData } : {},
|
|
125407
|
+
assessment: assessmentPresentationForAttempt(input.assessment, input.attemptId),
|
|
125408
|
+
responses: metadata2.responses,
|
|
125409
|
+
itemSubmissions: metadata2.itemSubmissions,
|
|
125410
|
+
score: null,
|
|
125411
|
+
selection
|
|
125412
|
+
};
|
|
125413
|
+
}
|
|
125414
|
+
function buildAssessmentAwardRecord(input, mastery) {
|
|
125415
|
+
return {
|
|
125416
|
+
xpAwarded: input.xpAwarded,
|
|
125417
|
+
...input.masteredUnits !== undefined ? { masteredUnits: input.masteredUnits } : {},
|
|
125418
|
+
...input.masteredUnitsAbsolute !== undefined ? { masteredUnitsAbsolute: input.masteredUnitsAbsolute } : {},
|
|
125419
|
+
masteredUnitsApplied: mastery.masteredUnitsApplied,
|
|
125420
|
+
...mastery.pctCompleteApp !== undefined ? { pctCompleteApp: mastery.pctCompleteApp } : {},
|
|
125421
|
+
...mastery.warnings ? { warnings: mastery.warnings } : {}
|
|
125422
|
+
};
|
|
125423
|
+
}
|
|
125179
125424
|
function stageAssessmentAttemptSupersession(attempt) {
|
|
125180
125425
|
Object.assign(attempt.result, ASSESSMENT_ATTEMPT_SUPERSEDED);
|
|
125181
125426
|
Object.assign(attempt.selection, ASSESSMENT_ATTEMPT_SUPERSEDED);
|
|
@@ -125386,13 +125631,35 @@ function buildAssessmentSubmitResult(input) {
|
|
|
125386
125631
|
}
|
|
125387
125632
|
return { ...base, purpose: input.metadata.purpose };
|
|
125388
125633
|
}
|
|
125634
|
+
function assessmentAwardFromResult(result, metadata2) {
|
|
125635
|
+
if (metadata2.award) {
|
|
125636
|
+
return metadata2.award;
|
|
125637
|
+
}
|
|
125638
|
+
const xp = result.metadata?.xp;
|
|
125639
|
+
return typeof xp === "number" && Number.isFinite(xp) && xp >= 0 ? { xpAwarded: xp, masteredUnitsApplied: 0 } : null;
|
|
125640
|
+
}
|
|
125389
125641
|
function buildCompletedAssessmentSubmitResult(input) {
|
|
125390
|
-
const
|
|
125391
|
-
|
|
125642
|
+
const attemptId = input.result.sourcedId;
|
|
125643
|
+
const award = assessmentAwardFromResult(input.result, input.metadata);
|
|
125644
|
+
if (award === null) {
|
|
125645
|
+
throw new GoneError("This completed assessment is missing its recorded XP.", {
|
|
125646
|
+
attemptId
|
|
125647
|
+
});
|
|
125648
|
+
}
|
|
125649
|
+
const submitted = buildAssessmentSubmitResult({ attemptId, metadata: input.metadata });
|
|
125650
|
+
if (!submitted) {
|
|
125651
|
+
throw new GoneError("This completed assessment has incomplete persisted results.", {
|
|
125652
|
+
attemptId
|
|
125653
|
+
});
|
|
125654
|
+
}
|
|
125655
|
+
return {
|
|
125392
125656
|
...submitted,
|
|
125393
125657
|
status: "completed",
|
|
125394
|
-
xpAwarded:
|
|
125395
|
-
|
|
125658
|
+
xpAwarded: award.xpAwarded,
|
|
125659
|
+
masteredUnitsApplied: award.masteredUnitsApplied,
|
|
125660
|
+
...award.pctCompleteApp !== undefined ? { pctCompleteApp: award.pctCompleteApp } : {},
|
|
125661
|
+
...award.warnings ? { warnings: award.warnings } : {}
|
|
125662
|
+
};
|
|
125396
125663
|
}
|
|
125397
125664
|
function assessmentAttemptId(input) {
|
|
125398
125665
|
return deterministicUUID([
|
|
@@ -125719,6 +125986,22 @@ function assessmentFixtureScoringKeys(assessment, questions) {
|
|
|
125719
125986
|
...Object.keys(responseAreas).length > 0 ? { responseAreas } : {}
|
|
125720
125987
|
};
|
|
125721
125988
|
}
|
|
125989
|
+
function prepareDiagnosticAssessmentResponses(assessment, current, input) {
|
|
125990
|
+
const update2 = { [input.itemIdentifier]: input.responses };
|
|
125991
|
+
validateAssessmentResponseUpdate(assessment, update2);
|
|
125992
|
+
const responses = applyAssessmentResponseUpdate(current, update2);
|
|
125993
|
+
const item = assessment.items.find((candidate) => candidate.identifier === input.itemIdentifier);
|
|
125994
|
+
const itemResponses = responses[input.itemIdentifier];
|
|
125995
|
+
const missingResponse = item?.interactions.find((interaction) => itemResponses?.[interaction.responseIdentifier] === undefined);
|
|
125996
|
+
if (!item || !itemResponses || missingResponse) {
|
|
125997
|
+
throw new AssessmentRuntimeError("INVALID_RESPONSE", `Diagnostic item ${input.itemIdentifier} requires a response for every interaction.`, {
|
|
125998
|
+
itemIdentifier: input.itemIdentifier,
|
|
125999
|
+
...missingResponse ? { responseIdentifier: missingResponse.responseIdentifier } : {}
|
|
126000
|
+
});
|
|
126001
|
+
}
|
|
126002
|
+
validateAssessmentResponses(assessment, { [input.itemIdentifier]: itemResponses });
|
|
126003
|
+
return responses;
|
|
126004
|
+
}
|
|
125722
126005
|
function validateAssessmentResponseUpdate(assessment, update2) {
|
|
125723
126006
|
const message = assessmentResponseUpdateValidationMessage(assessment, update2);
|
|
125724
126007
|
if (message) {
|
|
@@ -125805,7 +126088,7 @@ function buildAssessmentResultSubmission(input) {
|
|
|
125805
126088
|
}
|
|
125806
126089
|
};
|
|
125807
126090
|
}
|
|
125808
|
-
function
|
|
126091
|
+
function buildAssessmentResultAwardFinalization(input) {
|
|
125809
126092
|
const completion = input.metadata.completion;
|
|
125810
126093
|
if (!completion || completion.totalQuestions === undefined) {
|
|
125811
126094
|
throw new Error("A scored assessment must retain its completion facts before finalization");
|
|
@@ -125813,8 +126096,10 @@ function buildAssessmentResultXpFinalization(input) {
|
|
|
125813
126096
|
const metadata2 = {
|
|
125814
126097
|
...input.metadata,
|
|
125815
126098
|
finalizedAt: input.timestamp,
|
|
125816
|
-
updatedAt: input.timestamp
|
|
126099
|
+
updatedAt: input.timestamp,
|
|
126100
|
+
award: input.award
|
|
125817
126101
|
};
|
|
126102
|
+
const masteryRequested = input.award.masteredUnits !== undefined || input.award.masteredUnitsAbsolute !== undefined;
|
|
125818
126103
|
return {
|
|
125819
126104
|
metadata: metadata2,
|
|
125820
126105
|
resultUpdate: {
|
|
@@ -125826,7 +126111,8 @@ function buildAssessmentResultXpFinalization(input) {
|
|
|
125826
126111
|
...ASSESSMENT_ATTEMPT_COMPLETED,
|
|
125827
126112
|
metadata: {
|
|
125828
126113
|
...mergeAssessmentRuntimeMetadata(input.result.metadata, metadata2),
|
|
125829
|
-
xp: input.xpAwarded,
|
|
126114
|
+
xp: input.award.xpAwarded,
|
|
126115
|
+
...masteryRequested ? { masteredUnits: input.award.masteredUnitsApplied } : {},
|
|
125830
126116
|
totalQuestions: completion.totalQuestions,
|
|
125831
126117
|
correctQuestions: completion.correctQuestions,
|
|
125832
126118
|
appName: input.appName
|
|
@@ -125839,12 +126125,217 @@ var init_timeback_assessment_runtime_util = __esm(() => {
|
|
|
125839
126125
|
init_src();
|
|
125840
126126
|
init_assessment_runtime2();
|
|
125841
126127
|
init_qti();
|
|
126128
|
+
init_types2();
|
|
125842
126129
|
init_utils6();
|
|
125843
126130
|
init_timeback4();
|
|
125844
126131
|
init_uuid();
|
|
125845
126132
|
init_errors2();
|
|
125846
126133
|
PLAYABLE_SHAPE_SET = new Set(PLAYABLE_ASSESSMENT_SHAPES);
|
|
125847
126134
|
});
|
|
126135
|
+
function reviewBankTestItemIdentifiers(test) {
|
|
126136
|
+
return qtiTestParts(test).flatMap((part) => part["qti-assessment-section"].flatMap((section) => (section["qti-assessment-item-ref"] ?? []).map((reference) => reference.identifier)));
|
|
126137
|
+
}
|
|
126138
|
+
function canonicalSyncContributors(contributors) {
|
|
126139
|
+
return contributors.map((contributor) => {
|
|
126140
|
+
const canonical = canonicalContributorIdentity(contributor);
|
|
126141
|
+
return {
|
|
126142
|
+
...contributor,
|
|
126143
|
+
...canonical
|
|
126144
|
+
};
|
|
126145
|
+
}).toSorted((left, right) => compareCodeUnits(left.standardKey, right.standardKey) || compareCodeUnits(left.qtiTestIdentifier, right.qtiTestIdentifier) || compareCodeUnits(left.assessmentKey, right.assessmentKey));
|
|
126146
|
+
}
|
|
126147
|
+
function canonicalContributorIdentity(contributor) {
|
|
126148
|
+
const standard = canonicalReviewStandardRef(contributor.standard);
|
|
126149
|
+
if (!standard) {
|
|
126150
|
+
throw new ValidationError(`Mastery assessment ${contributor.qtiTestIdentifier} has an invalid standard`);
|
|
126151
|
+
}
|
|
126152
|
+
return {
|
|
126153
|
+
qtiTestIdentifier: contributor.qtiTestIdentifier,
|
|
126154
|
+
standard,
|
|
126155
|
+
standardKey: assessmentStandardRefKey2(standard)
|
|
126156
|
+
};
|
|
126157
|
+
}
|
|
126158
|
+
async function reviewBankContributorFingerprint(contributors) {
|
|
126159
|
+
const uniqueTuples = new Map;
|
|
126160
|
+
for (const contributor of contributors) {
|
|
126161
|
+
const { qtiTestIdentifier, standardKey } = canonicalContributorIdentity(contributor);
|
|
126162
|
+
const tuple3 = { qtiTestIdentifier, standardKey };
|
|
126163
|
+
uniqueTuples.set(canonicalJson2(tuple3), tuple3);
|
|
126164
|
+
}
|
|
126165
|
+
const digest = await sha256Hex(canonicalJson2([...uniqueTuples.values()].toSorted((left, right) => compareCodeUnits(left.standardKey, right.standardKey) || compareCodeUnits(left.qtiTestIdentifier, right.qtiTestIdentifier))));
|
|
126166
|
+
return `review-bank-contributors-v1:${digest}`;
|
|
126167
|
+
}
|
|
126168
|
+
function conflictWarning(input) {
|
|
126169
|
+
const retained = `${input.retainedStandard.framework} · ${input.retainedStandard.identifier}`;
|
|
126170
|
+
const skipped = `${input.skippedStandard.framework} · ${input.skippedStandard.identifier}`;
|
|
126171
|
+
return {
|
|
126172
|
+
code: "QUESTION_STANDARD_CONFLICT",
|
|
126173
|
+
...input,
|
|
126174
|
+
message: `Question ${input.itemIdentifier} is referenced by multiple mastery standards; retained ${retained} and skipped ${skipped} from ${input.skippedQtiTestIdentifier}.`
|
|
126175
|
+
};
|
|
126176
|
+
}
|
|
126177
|
+
async function prepareReviewBankSynchronization(input) {
|
|
126178
|
+
const contributors = canonicalSyncContributors(input.contributors);
|
|
126179
|
+
const selectedByItem = new Map;
|
|
126180
|
+
const warningKeys = new Set;
|
|
126181
|
+
const warnings = [];
|
|
126182
|
+
for (const contributor of contributors) {
|
|
126183
|
+
const itemIdentifiers2 = [
|
|
126184
|
+
...new Set(reviewBankTestItemIdentifiers(contributor.test))
|
|
126185
|
+
].toSorted();
|
|
126186
|
+
for (const itemIdentifier of itemIdentifiers2) {
|
|
126187
|
+
const selected = selectedByItem.get(itemIdentifier);
|
|
126188
|
+
if (!selected) {
|
|
126189
|
+
selectedByItem.set(itemIdentifier, {
|
|
126190
|
+
standard: contributor.standard,
|
|
126191
|
+
standardKey: contributor.standardKey
|
|
126192
|
+
});
|
|
126193
|
+
} else if (selected.standardKey !== contributor.standardKey) {
|
|
126194
|
+
const warningKey = `${itemIdentifier}\x00${contributor.standardKey}`;
|
|
126195
|
+
if (!warningKeys.has(warningKey)) {
|
|
126196
|
+
warningKeys.add(warningKey);
|
|
126197
|
+
warnings.push(conflictWarning({
|
|
126198
|
+
itemIdentifier,
|
|
126199
|
+
retainedStandard: selected.standard,
|
|
126200
|
+
skippedStandard: contributor.standard,
|
|
126201
|
+
skippedQtiTestIdentifier: contributor.qtiTestIdentifier
|
|
126202
|
+
}));
|
|
126203
|
+
}
|
|
126204
|
+
}
|
|
126205
|
+
}
|
|
126206
|
+
}
|
|
126207
|
+
const itemIdentifiers = [...selectedByItem.keys()].toSorted();
|
|
126208
|
+
const sourceContentRevision = input.contributorFingerprint ?? await reviewBankContributorFingerprint(contributors);
|
|
126209
|
+
const bank = await buildReviewBankIndexFromReferences({
|
|
126210
|
+
bankIdentifier: input.bankIdentifier,
|
|
126211
|
+
sourceContentRevision,
|
|
126212
|
+
items: itemIdentifiers.map((itemIdentifier) => ({
|
|
126213
|
+
itemIdentifier,
|
|
126214
|
+
standards: [selectedByItem.get(itemIdentifier).standard]
|
|
126215
|
+
}))
|
|
126216
|
+
});
|
|
126217
|
+
const manifest = await buildReviewBankManifest(bank);
|
|
126218
|
+
return {
|
|
126219
|
+
bankIdentifier: input.bankIdentifier,
|
|
126220
|
+
contributorFingerprint: sourceContentRevision,
|
|
126221
|
+
itemIdentifiers,
|
|
126222
|
+
manifest,
|
|
126223
|
+
warnings,
|
|
126224
|
+
summary: {
|
|
126225
|
+
itemCount: itemIdentifiers.length,
|
|
126226
|
+
standardCount: Object.keys(manifest.itemsByStandard).length,
|
|
126227
|
+
contributorCount: contributors.length,
|
|
126228
|
+
sourceFingerprint: manifest.sourceFingerprint
|
|
126229
|
+
}
|
|
126230
|
+
};
|
|
126231
|
+
}
|
|
126232
|
+
function reviewBankSinkMetadata(integrationId, plan) {
|
|
126233
|
+
return {
|
|
126234
|
+
ownerSystem: PLAYCADEMY_QTI_OWNER_SYSTEM,
|
|
126235
|
+
integrationId,
|
|
126236
|
+
[PLAYCADEMY_REVIEW_BANK_SINK_METADATA_KEY]: {
|
|
126237
|
+
version: PLAYCADEMY_REVIEW_BANK_SINK_VERSION,
|
|
126238
|
+
integrationId,
|
|
126239
|
+
contributorFingerprint: plan.contributorFingerprint,
|
|
126240
|
+
warnings: plan.warnings
|
|
126241
|
+
},
|
|
126242
|
+
[PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY]: plan.manifest
|
|
126243
|
+
};
|
|
126244
|
+
}
|
|
126245
|
+
function reviewBankSinkMarker(test) {
|
|
126246
|
+
const parsed = ReviewBankSinkMarkerSchema.safeParse(test.metadata?.[PLAYCADEMY_REVIEW_BANK_SINK_METADATA_KEY]);
|
|
126247
|
+
return parsed.success ? parsed.data : null;
|
|
126248
|
+
}
|
|
126249
|
+
function reviewBankSinkState(test, integrationId) {
|
|
126250
|
+
assertReviewBankSinkOwnership(test, integrationId);
|
|
126251
|
+
const marker = reviewBankSinkMarker(test);
|
|
126252
|
+
return {
|
|
126253
|
+
contributorFingerprint: marker?.contributorFingerprint ?? null,
|
|
126254
|
+
warnings: marker?.warnings ?? []
|
|
126255
|
+
};
|
|
126256
|
+
}
|
|
126257
|
+
async function unchangedReviewBankSynchronizationResult(input) {
|
|
126258
|
+
const sinkState = reviewBankSinkState(input.sink, input.integrationId);
|
|
126259
|
+
if (sinkState.contributorFingerprint !== input.contributorFingerprint) {
|
|
126260
|
+
return null;
|
|
126261
|
+
}
|
|
126262
|
+
const membershipItemIdentifiers = reviewBankTestItemIdentifiers(input.sink);
|
|
126263
|
+
const bank = await reviewBankIndexFromManifest(input.sink.metadata, {
|
|
126264
|
+
bankIdentifier: input.sink.identifier,
|
|
126265
|
+
membershipItemIdentifiers
|
|
126266
|
+
});
|
|
126267
|
+
const manifest = input.sink.metadata?.[PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY];
|
|
126268
|
+
if (!bank || typeof manifest?.sourceFingerprint !== "string") {
|
|
126269
|
+
return null;
|
|
126270
|
+
}
|
|
126271
|
+
const standardCount = new Set(bank.items.flatMap((item) => item.standards.map(assessmentStandardRefKey2))).size;
|
|
126272
|
+
return {
|
|
126273
|
+
qtiTestIdentifier: input.sink.identifier,
|
|
126274
|
+
itemCount: bank.items.length,
|
|
126275
|
+
standardCount,
|
|
126276
|
+
contributorCount: input.contributorCount,
|
|
126277
|
+
sourceFingerprint: manifest.sourceFingerprint,
|
|
126278
|
+
addedItemCount: 0,
|
|
126279
|
+
removedItemCount: 0,
|
|
126280
|
+
unchanged: true,
|
|
126281
|
+
warnings: sinkState.warnings
|
|
126282
|
+
};
|
|
126283
|
+
}
|
|
126284
|
+
function reviewBankSynchronizationDiff(input) {
|
|
126285
|
+
let previousMembershipReadable = true;
|
|
126286
|
+
let previousItemIdentifiers = [];
|
|
126287
|
+
if (input.existingSink) {
|
|
126288
|
+
try {
|
|
126289
|
+
previousItemIdentifiers = reviewBankTestItemIdentifiers(input.existingSink);
|
|
126290
|
+
} catch {
|
|
126291
|
+
previousMembershipReadable = false;
|
|
126292
|
+
}
|
|
126293
|
+
}
|
|
126294
|
+
const previousItems = new Set(previousItemIdentifiers);
|
|
126295
|
+
const desiredItems = new Set(input.plan.itemIdentifiers);
|
|
126296
|
+
const addedItemCount = input.plan.itemIdentifiers.filter((identifier) => !previousItems.has(identifier)).length;
|
|
126297
|
+
const removedItemCount = [...previousItems].filter((identifier) => !desiredItems.has(identifier)).length;
|
|
126298
|
+
const currentManifest = input.existingSink?.metadata?.[PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY];
|
|
126299
|
+
const currentContributorFingerprint = input.existingSink ? reviewBankSinkMarker(input.existingSink)?.contributorFingerprint ?? null : null;
|
|
126300
|
+
const canonicalMembership = previousItemIdentifiers.length === input.plan.itemIdentifiers.length && previousItemIdentifiers.every((identifier, index2) => identifier === input.plan.itemIdentifiers[index2]);
|
|
126301
|
+
const unchanged = input.existingSink !== null && previousMembershipReadable && canonicalMembership && canonicalJson2(currentManifest) === canonicalJson2(input.plan.manifest) && currentContributorFingerprint === input.plan.contributorFingerprint;
|
|
126302
|
+
return { addedItemCount, removedItemCount, unchanged };
|
|
126303
|
+
}
|
|
126304
|
+
function assertReviewBankSinkOwnership(test, integrationId) {
|
|
126305
|
+
const marker = reviewBankSinkMarker(test);
|
|
126306
|
+
if (marker?.integrationId !== integrationId || test.metadata?.ownerSystem !== PLAYCADEMY_QTI_OWNER_SYSTEM || test.identifier !== reviewBankSinkIdentifier(integrationId)) {
|
|
126307
|
+
throw new ValidationError(`QTI assessment ${test.identifier} is not the review-bank sink for this integration`);
|
|
126308
|
+
}
|
|
126309
|
+
}
|
|
126310
|
+
function buildReviewBankSinkInput(input) {
|
|
126311
|
+
return {
|
|
126312
|
+
...input.includeIdentifier ? { identifier: input.identifier } : {},
|
|
126313
|
+
title: input.title,
|
|
126314
|
+
metadata: input.metadata,
|
|
126315
|
+
"qti-test-part": [
|
|
126316
|
+
{
|
|
126317
|
+
identifier: `${input.identifier}-part1`,
|
|
126318
|
+
navigationMode: "linear",
|
|
126319
|
+
submissionMode: "individual",
|
|
126320
|
+
"qti-assessment-section": [
|
|
126321
|
+
{
|
|
126322
|
+
identifier: `${input.identifier}-section1`,
|
|
126323
|
+
title: "Questions",
|
|
126324
|
+
visible: true,
|
|
126325
|
+
required: true,
|
|
126326
|
+
fixed: false,
|
|
126327
|
+
sequence: 1,
|
|
126328
|
+
"qti-assessment-item-ref": input.itemIdentifiers.map((itemIdentifier, index2) => ({
|
|
126329
|
+
identifier: itemIdentifier,
|
|
126330
|
+
href: input.itemHref(itemIdentifier),
|
|
126331
|
+
sequence: index2 + 1
|
|
126332
|
+
}))
|
|
126333
|
+
}
|
|
126334
|
+
]
|
|
126335
|
+
}
|
|
126336
|
+
]
|
|
126337
|
+
};
|
|
126338
|
+
}
|
|
125848
126339
|
function reviewMappingIssueSummary(label, identifiers) {
|
|
125849
126340
|
if (identifiers.length === 0) {
|
|
125850
126341
|
return null;
|
|
@@ -125893,11 +126384,50 @@ async function prepareReviewMappingUpdate(test, questions) {
|
|
|
125893
126384
|
}
|
|
125894
126385
|
};
|
|
125895
126386
|
}
|
|
126387
|
+
var PLAYCADEMY_REVIEW_BANK_SINK_METADATA_KEY = "playcademyReviewBankSink";
|
|
126388
|
+
var PLAYCADEMY_REVIEW_BANK_SINK_VERSION = 1;
|
|
126389
|
+
var StoredSinkWarningSchema;
|
|
126390
|
+
var ReviewBankSinkMarkerSchema;
|
|
125896
126391
|
var init_timeback_review_mapping_util = __esm(() => {
|
|
126392
|
+
init_esm();
|
|
125897
126393
|
init_assessment_runtime2();
|
|
126394
|
+
init_qti();
|
|
126395
|
+
init_timeback3();
|
|
125898
126396
|
init_errors2();
|
|
125899
126397
|
init_timeback_assessment_runtime_util();
|
|
125900
126398
|
init_timeback_qti_authoring_util();
|
|
126399
|
+
StoredSinkWarningSchema = exports_external.object({
|
|
126400
|
+
code: exports_external.literal("QUESTION_STANDARD_CONFLICT"),
|
|
126401
|
+
itemIdentifier: exports_external.string(),
|
|
126402
|
+
retainedStandard: exports_external.object({ framework: exports_external.string(), identifier: exports_external.string() }),
|
|
126403
|
+
skippedStandard: exports_external.object({ framework: exports_external.string(), identifier: exports_external.string() }),
|
|
126404
|
+
skippedQtiTestIdentifier: exports_external.string()
|
|
126405
|
+
});
|
|
126406
|
+
ReviewBankSinkMarkerSchema = exports_external.object({
|
|
126407
|
+
version: exports_external.literal(PLAYCADEMY_REVIEW_BANK_SINK_VERSION),
|
|
126408
|
+
integrationId: exports_external.string(),
|
|
126409
|
+
contributorFingerprint: exports_external.string().nullable().catch(null),
|
|
126410
|
+
warnings: exports_external.array(StoredSinkWarningSchema).nullable().catch(null)
|
|
126411
|
+
}).transform((marker) => {
|
|
126412
|
+
if (!marker.contributorFingerprint || !marker.warnings) {
|
|
126413
|
+
return { ...marker, contributorFingerprint: null, warnings: [] };
|
|
126414
|
+
}
|
|
126415
|
+
const warnings = [];
|
|
126416
|
+
for (const warning of marker.warnings) {
|
|
126417
|
+
const retained = canonicalReviewStandardRef(warning.retainedStandard);
|
|
126418
|
+
const skipped = canonicalReviewStandardRef(warning.skippedStandard);
|
|
126419
|
+
if (!retained || !skipped) {
|
|
126420
|
+
return { ...marker, contributorFingerprint: null, warnings: [] };
|
|
126421
|
+
}
|
|
126422
|
+
warnings.push(conflictWarning({
|
|
126423
|
+
itemIdentifier: warning.itemIdentifier,
|
|
126424
|
+
retainedStandard: retained,
|
|
126425
|
+
skippedStandard: skipped,
|
|
126426
|
+
skippedQtiTestIdentifier: warning.skippedQtiTestIdentifier
|
|
126427
|
+
}));
|
|
126428
|
+
}
|
|
126429
|
+
return { ...marker, warnings };
|
|
126430
|
+
});
|
|
125901
126431
|
});
|
|
125902
126432
|
function assessmentKeyFromManagedQtiIdentifier(identifier) {
|
|
125903
126433
|
const assessmentKey = /^playcademy-test\.(.+)\.[a-f0-9]{64}$/.exec(identifier)?.[1];
|
|
@@ -126201,11 +126731,6 @@ function assertAssessmentHasQuestions(questions) {
|
|
|
126201
126731
|
throw new ValidationError("An assessment must contain at least one question to publish");
|
|
126202
126732
|
}
|
|
126203
126733
|
}
|
|
126204
|
-
function assertReviewAssessmentHasStandards(standardCounts) {
|
|
126205
|
-
if (standardCounts.some((count) => count !== 1)) {
|
|
126206
|
-
throw new ValidationError("Every question in a review assessment must have exactly one standards alignment");
|
|
126207
|
-
}
|
|
126208
|
-
}
|
|
126209
126734
|
function planAssessmentRemoval(status) {
|
|
126210
126735
|
if (status === "draft") {
|
|
126211
126736
|
return { kind: "delete", action: "discarded", operation: "discard_draft" };
|
|
@@ -126266,6 +126791,9 @@ function orderLiveAssessmentRows(liveRows, purpose, testIdentifiers) {
|
|
|
126266
126791
|
var init_timeback_assessment_rules_util = __esm(() => {
|
|
126267
126792
|
init_errors2();
|
|
126268
126793
|
});
|
|
126794
|
+
function qtiItemHref(client2, itemIdentifier) {
|
|
126795
|
+
return `${client2.getQtiBaseUrl()}/assessment-items/${itemIdentifier}`;
|
|
126796
|
+
}
|
|
126269
126797
|
async function hydrateQtiTestQuestions(client2, references) {
|
|
126270
126798
|
const questions = await runWithConcurrency(references.questions, QTI_HYDRATION_CONCURRENCY, async (reference) => ({
|
|
126271
126799
|
...reference,
|
|
@@ -126288,6 +126816,28 @@ async function hydrateQtiTestQuestionSelection(client2, references, itemIdentifi
|
|
|
126288
126816
|
questions: selectedReferences
|
|
126289
126817
|
});
|
|
126290
126818
|
}
|
|
126819
|
+
async function hydrateQtiItemSelection(client2, test, itemIdentifiers) {
|
|
126820
|
+
const testPart = qtiTestParts(test)[0];
|
|
126821
|
+
const section = testPart?.["qti-assessment-section"][0];
|
|
126822
|
+
if (!testPart || !section) {
|
|
126823
|
+
throw new Error(`QTI assessment ${test.identifier} has no section to reference items from`);
|
|
126824
|
+
}
|
|
126825
|
+
const questions = await runWithConcurrency(itemIdentifiers, QTI_HYDRATION_CONCURRENCY, async (itemIdentifier) => ({
|
|
126826
|
+
reference: {
|
|
126827
|
+
identifier: itemIdentifier,
|
|
126828
|
+
href: qtiItemHref(client2, itemIdentifier),
|
|
126829
|
+
testPart: testPart.identifier,
|
|
126830
|
+
section: section.identifier
|
|
126831
|
+
},
|
|
126832
|
+
question: await client2.qtiApi.assessmentItems.get(itemIdentifier)
|
|
126833
|
+
}));
|
|
126834
|
+
return {
|
|
126835
|
+
assessmentTest: test.identifier,
|
|
126836
|
+
title: test.title,
|
|
126837
|
+
totalQuestions: questions.length,
|
|
126838
|
+
questions
|
|
126839
|
+
};
|
|
126840
|
+
}
|
|
126291
126841
|
async function loadQtiTestReferences(client2, identifier) {
|
|
126292
126842
|
const [test, references] = await Promise.all([
|
|
126293
126843
|
client2.qtiApi.assessmentTests.get(identifier),
|
|
@@ -126303,7 +126853,9 @@ async function loadHydratedQtiTest(client2, identifier) {
|
|
|
126303
126853
|
};
|
|
126304
126854
|
}
|
|
126305
126855
|
var QTI_HYDRATION_CONCURRENCY = 8;
|
|
126306
|
-
var init_timeback_qti_hydration_util = () => {
|
|
126856
|
+
var init_timeback_qti_hydration_util = __esm(() => {
|
|
126857
|
+
init_timeback_qti_authoring_util();
|
|
126858
|
+
});
|
|
126307
126859
|
var TimebackAssessmentRuntimeService;
|
|
126308
126860
|
var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
126309
126861
|
init_drizzle_orm();
|
|
@@ -126331,8 +126883,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
126331
126883
|
static EXPORT_CONCURRENCY = 4;
|
|
126332
126884
|
static ITEM_SUBMISSION_MAX_PREPARATION_ATTEMPTS = 3;
|
|
126333
126885
|
static ASSESSMENT_VERSION_CONFIRMATION_DELAY_MS = 25;
|
|
126334
|
-
static PROJECTION_BARRIER_MAX_ATTEMPTS = 5;
|
|
126335
|
-
static PROJECTION_BARRIER_RETRY_DELAY_MS = 25;
|
|
126336
126886
|
static SCORING_CONCURRENCY = 4;
|
|
126337
126887
|
static STALE_AWARD_AGE_MS = 900000;
|
|
126338
126888
|
deps;
|
|
@@ -126875,15 +127425,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
126875
127425
|
this.assertInProgress(attempt.result);
|
|
126876
127426
|
if (!preview || preview.responseVersion !== attempt.metadata.responseVersion || preview.submissionId !== input.submissionId || preview.itemIdentifier !== input.itemIdentifier) {
|
|
126877
127427
|
if (preparationAttempts >= TimebackAssessmentRuntimeService2.ITEM_SUBMISSION_MAX_PREPARATION_ATTEMPTS) {
|
|
126878
|
-
|
|
126879
|
-
throw new ServiceUnavailableError("The assessment response state changed repeatedly while this item was being prepared. Try again shortly.", {
|
|
126880
|
-
retryable: true,
|
|
126881
|
-
reason: "PREPARATION_CONTENTION",
|
|
126882
|
-
expectedResponseVersion: input.expectedResponseVersion,
|
|
126883
|
-
responseVersion: attempt.metadata.responseVersion
|
|
126884
|
-
});
|
|
126885
|
-
}
|
|
126886
|
-
throw AssessmentRuntimeError.from(assessmentResponseVersionConflict(input.expectedResponseVersion, attempt.metadata.responseVersion));
|
|
127428
|
+
throw assessmentPreparationContentionError(input.expectedResponseVersion, attempt.metadata.responseVersion, "assessment");
|
|
126887
127429
|
}
|
|
126888
127430
|
return { action: "prepare" };
|
|
126889
127431
|
}
|
|
@@ -127006,15 +127548,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
127006
127548
|
this.assertInProgress(attempt.result);
|
|
127007
127549
|
if (!preview || preview.responseVersion !== metadata2.responseVersion || preview.submissionId !== input.submissionId || preview.routingNodeKey !== input.routingNodeKey || preview.itemIdentifier !== input.itemIdentifier) {
|
|
127008
127550
|
if (preparationAttempts >= TimebackAssessmentRuntimeService2.ITEM_SUBMISSION_MAX_PREPARATION_ATTEMPTS) {
|
|
127009
|
-
|
|
127010
|
-
throw new ServiceUnavailableError("The diagnostic response state changed repeatedly while this item was being prepared. Try again shortly.", {
|
|
127011
|
-
retryable: true,
|
|
127012
|
-
reason: "PREPARATION_CONTENTION",
|
|
127013
|
-
expectedResponseVersion: input.expectedResponseVersion,
|
|
127014
|
-
responseVersion: metadata2.responseVersion
|
|
127015
|
-
});
|
|
127016
|
-
}
|
|
127017
|
-
throw AssessmentRuntimeError.from(assessmentResponseVersionConflict(input.expectedResponseVersion, metadata2.responseVersion));
|
|
127551
|
+
throw assessmentPreparationContentionError(input.expectedResponseVersion, metadata2.responseVersion, "diagnostic");
|
|
127018
127552
|
}
|
|
127019
127553
|
return { action: "prepare" };
|
|
127020
127554
|
}
|
|
@@ -127029,7 +127563,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
127029
127563
|
itemIdentifier: input.itemIdentifier
|
|
127030
127564
|
}));
|
|
127031
127565
|
}
|
|
127032
|
-
const responses =
|
|
127566
|
+
const responses = prepareDiagnosticAssessmentResponses(preview.assessment, metadata2.responses, input);
|
|
127033
127567
|
const scoring = preview.scoring;
|
|
127034
127568
|
if (!scoring || typeof scoring.isCorrect !== "boolean") {
|
|
127035
127569
|
throw AssessmentRuntimeError.from(assessmentFlowViolation(`Diagnostic item ${input.itemIdentifier} did not produce determinate binary grading.`, { itemIdentifier: input.itemIdentifier }));
|
|
@@ -127102,22 +127636,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
127102
127636
|
answered: submission.answered
|
|
127103
127637
|
};
|
|
127104
127638
|
}
|
|
127105
|
-
prepareDiagnosticResponses(assessment, current, input) {
|
|
127106
|
-
const update2 = { [input.itemIdentifier]: input.responses };
|
|
127107
|
-
validateAssessmentResponseUpdate(assessment, update2);
|
|
127108
|
-
const responses = applyAssessmentResponseUpdate(current, update2);
|
|
127109
|
-
const item = assessment.items.find((candidate) => candidate.identifier === input.itemIdentifier);
|
|
127110
|
-
const itemResponses = responses[input.itemIdentifier];
|
|
127111
|
-
const missingResponse = item?.interactions.find((interaction) => itemResponses?.[interaction.responseIdentifier] === undefined);
|
|
127112
|
-
if (!item || !itemResponses || missingResponse) {
|
|
127113
|
-
throw new AssessmentRuntimeError("INVALID_RESPONSE", `Diagnostic item ${input.itemIdentifier} requires a response for every interaction.`, {
|
|
127114
|
-
itemIdentifier: input.itemIdentifier,
|
|
127115
|
-
...missingResponse ? { responseIdentifier: missingResponse.responseIdentifier } : {}
|
|
127116
|
-
});
|
|
127117
|
-
}
|
|
127118
|
-
validateAssessmentResponses(assessment, { [input.itemIdentifier]: itemResponses });
|
|
127119
|
-
return responses;
|
|
127120
|
-
}
|
|
127121
127639
|
async prepareDiagnosticItemSubmission(params, input) {
|
|
127122
127640
|
try {
|
|
127123
127641
|
return await this.scoreDiagnosticItemSubmission(params, input);
|
|
@@ -127151,7 +127669,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
127151
127669
|
const expected = loaded.state.next;
|
|
127152
127670
|
let scoring = null;
|
|
127153
127671
|
if (loaded.state.status === "in-progress" && expected?.nodeKey === input.routingNodeKey && expected.itemIdentifier === input.itemIdentifier && attempt.metadata.responseVersion === input.expectedResponseVersion) {
|
|
127154
|
-
const responses =
|
|
127672
|
+
const responses = prepareDiagnosticAssessmentResponses(assessment, attempt.metadata.responses, input);
|
|
127155
127673
|
scoring = await this.scoreItem(assessment, responses, input.itemIdentifier);
|
|
127156
127674
|
}
|
|
127157
127675
|
return {
|
|
@@ -127422,11 +127940,14 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
127422
127940
|
inProgress: attempt.result.inProgress ?? "",
|
|
127423
127941
|
scoreStatus: attempt.result.scoreStatus,
|
|
127424
127942
|
submissionId: attempt.metadata.submissionId,
|
|
127425
|
-
|
|
127943
|
+
award: assessmentAwardFromResult(attempt.result, attempt.metadata) ?? undefined
|
|
127426
127944
|
}, input);
|
|
127427
127945
|
if (disposition === "replay") {
|
|
127428
127946
|
return {
|
|
127429
|
-
response:
|
|
127947
|
+
response: buildCompletedAssessmentSubmitResult({
|
|
127948
|
+
result: attempt.result,
|
|
127949
|
+
metadata: attempt.metadata
|
|
127950
|
+
}),
|
|
127430
127951
|
completion: this.replayCompletion(attempt, input.submissionId, params, game2)
|
|
127431
127952
|
};
|
|
127432
127953
|
}
|
|
@@ -127434,20 +127955,27 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
127434
127955
|
throw AssessmentRuntimeError.from(assessmentAwardConflict({
|
|
127435
127956
|
attemptId: attempt.result.sourcedId,
|
|
127436
127957
|
submissionId: input.submissionId,
|
|
127437
|
-
xpAwarded: input.xpAwarded
|
|
127958
|
+
xpAwarded: input.xpAwarded,
|
|
127959
|
+
...input.masteredUnits !== undefined ? { masteredUnits: input.masteredUnits } : {},
|
|
127960
|
+
...input.masteredUnitsAbsolute !== undefined ? { masteredUnitsAbsolute: input.masteredUnitsAbsolute } : {}
|
|
127438
127961
|
}));
|
|
127439
127962
|
}
|
|
127440
127963
|
if (disposition === "reject") {
|
|
127441
127964
|
throw AssessmentRuntimeError.from(assessmentAlreadySubmitted(attempt.result.sourcedId));
|
|
127442
127965
|
}
|
|
127443
|
-
const
|
|
127444
|
-
const
|
|
127966
|
+
const mastery = await this.evaluateMasteryAward(attempt, params.studentId, input);
|
|
127967
|
+
const awardRecord = buildAssessmentAwardRecord(input, mastery);
|
|
127968
|
+
const finalization = buildAssessmentResultAwardFinalization({
|
|
127445
127969
|
result: attempt.result,
|
|
127446
127970
|
metadata: attempt.metadata,
|
|
127447
|
-
|
|
127971
|
+
award: awardRecord,
|
|
127448
127972
|
appName: game2.appName,
|
|
127449
127973
|
timestamp: new Date().toISOString()
|
|
127450
127974
|
});
|
|
127975
|
+
const response = buildCompletedAssessmentSubmitResult({
|
|
127976
|
+
result: attempt.result,
|
|
127977
|
+
metadata: finalization.metadata
|
|
127978
|
+
});
|
|
127451
127979
|
const finalized = await this.requireClient().api.oneroster.assessmentResults.upsert(attempt.result.sourcedId, finalization.resultUpdate);
|
|
127452
127980
|
const finalizedForEmission = {
|
|
127453
127981
|
...finalized,
|
|
@@ -127466,25 +127994,78 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
127466
127994
|
"app.assessment.submission_id": input.submissionId,
|
|
127467
127995
|
"app.assessment.purpose": finalization.metadata.purpose,
|
|
127468
127996
|
"app.assessment.award_age_ms": this.awardAgeMs(attempt.result.scoreDate),
|
|
127469
|
-
"app.assessment.xp_awarded": input.xpAwarded
|
|
127997
|
+
"app.assessment.xp_awarded": input.xpAwarded,
|
|
127998
|
+
"app.assessment.mastered_units_applied": mastery.masteredUnitsApplied,
|
|
127999
|
+
"app.assessment.mastery_achieved": mastery.masteryAchieved,
|
|
128000
|
+
"app.assessment.mastery_revoked": mastery.masteryRevoked
|
|
127470
128001
|
});
|
|
128002
|
+
await this.settleMasteryAward(finalizedAttempt, mastery, input.submissionId, game2);
|
|
127471
128003
|
return {
|
|
127472
|
-
response
|
|
127473
|
-
...submitted,
|
|
127474
|
-
status: "completed",
|
|
127475
|
-
xpAwarded: input.xpAwarded
|
|
127476
|
-
},
|
|
128004
|
+
response,
|
|
127477
128005
|
completion: this.replayCompletion(finalizedAttempt, input.submissionId, params, game2)
|
|
127478
128006
|
};
|
|
127479
128007
|
});
|
|
127480
128008
|
if (award.completion) {
|
|
127481
128009
|
await this.emitCompletionBestEffort({
|
|
127482
128010
|
...award.completion,
|
|
127483
|
-
|
|
128011
|
+
award: award.response
|
|
127484
128012
|
});
|
|
127485
128013
|
}
|
|
127486
128014
|
return award.response;
|
|
127487
128015
|
}
|
|
128016
|
+
async evaluateMasteryAward(attempt, studentIdentifier, input) {
|
|
128017
|
+
if (!hasMasteryAwardRequest(input)) {
|
|
128018
|
+
return NO_MASTERY_AWARD;
|
|
128019
|
+
}
|
|
128020
|
+
const student = await this.requireClient().roster.resolveStudent(studentIdentifier);
|
|
128021
|
+
return this.requireClient().activity.evaluateMasteryAward({
|
|
128022
|
+
studentId: student.id,
|
|
128023
|
+
courseId: attempt.integration.courseId,
|
|
128024
|
+
...input.masteredUnits !== undefined ? { masteredUnits: input.masteredUnits } : {},
|
|
128025
|
+
...input.masteredUnitsAbsolute !== undefined ? { masteredUnitsAbsolute: input.masteredUnitsAbsolute } : {}
|
|
128026
|
+
});
|
|
128027
|
+
}
|
|
128028
|
+
async settleMasteryAward(attempt, mastery, submissionId, game2) {
|
|
128029
|
+
if (!mastery.masteryAchieved && !mastery.masteryRevoked) {
|
|
128030
|
+
return;
|
|
128031
|
+
}
|
|
128032
|
+
const integration = attempt.integration;
|
|
128033
|
+
const course = this.trackableCourse(integration, "assessment.mastery_settlement_invalid_course_metadata", { "app.assessment.attempt_id": attempt.result.sourcedId });
|
|
128034
|
+
if (!course) {
|
|
128035
|
+
setAttributes({ "app.assessment.mastery_completion_entry_missing": true });
|
|
128036
|
+
return;
|
|
128037
|
+
}
|
|
128038
|
+
try {
|
|
128039
|
+
const student = await this.requireClient().roster.resolveStudent(attempt.result.student.sourcedId);
|
|
128040
|
+
const completionHistoryEventId = `urn:uuid:${await deterministicUUID([
|
|
128041
|
+
"playcademy-assessment-course-completed-event-v1",
|
|
128042
|
+
attempt.result.sourcedId,
|
|
128043
|
+
submissionId
|
|
128044
|
+
].join("\x00"))}`;
|
|
128045
|
+
const settlement = await this.requireClient().activity.settleMasteryAward(mastery, {
|
|
128046
|
+
studentId: student.id,
|
|
128047
|
+
studentEmail: student.email,
|
|
128048
|
+
courseId: integration.courseId,
|
|
128049
|
+
gameId: integration.gameId,
|
|
128050
|
+
activityId: attempt.metadata.activityId,
|
|
128051
|
+
courseName: attempt.metadata.completion?.courseName ?? "Game Course",
|
|
128052
|
+
subject: course.subject,
|
|
128053
|
+
appName: game2.appName,
|
|
128054
|
+
sensorUrl: game2.sensorUrl,
|
|
128055
|
+
completionHistoryEventId
|
|
128056
|
+
});
|
|
128057
|
+
if (mastery.masteryAchieved && !settlement.completionEntryWritten) {
|
|
128058
|
+
setAttributes({ "app.assessment.mastery_completion_entry_missing": true });
|
|
128059
|
+
}
|
|
128060
|
+
} catch (error88) {
|
|
128061
|
+
setAttributes({ "app.assessment.mastery_completion_entry_missing": true });
|
|
128062
|
+
addEvent("assessment.mastery_settlement_failed", {
|
|
128063
|
+
"app.assessment.attempt_id": attempt.result.sourcedId,
|
|
128064
|
+
"exception.type": errorType(error88),
|
|
128065
|
+
"app.error.message": errorMessage2(error88)
|
|
128066
|
+
});
|
|
128067
|
+
}
|
|
128068
|
+
}
|
|
127488
128069
|
async finalizeDiagnosticSubmission(input) {
|
|
127489
128070
|
const { attempt } = input;
|
|
127490
128071
|
const routing = await this.loadAttemptDiagnosticManifest(attempt.metadata, input.db);
|
|
@@ -127931,9 +128512,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
127931
128512
|
if (this.isSettled(resumed.result)) {
|
|
127932
128513
|
return this.snapshotForResult(resumed.result, resumed.metadata, context2.integration);
|
|
127933
128514
|
}
|
|
127934
|
-
const
|
|
127935
|
-
const currentSource = await this.hydrateReviewBankSelection(catalog, resumed.metadata.review.selections.map((selection) => selection.itemIdentifier));
|
|
127936
|
-
const source = this.pinnedReviewBankSource(currentSource, resumed.metadata);
|
|
128515
|
+
const source = await this.loadPinnedReviewBankSource(resumed.metadata);
|
|
127937
128516
|
const assessment = projectReviewAssessment(source.assessment, source.bank, resumed.metadata.review.selections);
|
|
127938
128517
|
await this.ensureReviewChildResults({
|
|
127939
128518
|
result: resumed.result,
|
|
@@ -127977,7 +128556,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
127977
128556
|
membershipItemIdentifiers: loaded.references.questions.map((question) => question.reference.identifier)
|
|
127978
128557
|
});
|
|
127979
128558
|
} catch (error88) {
|
|
127980
|
-
throw new ServiceUnavailableError(`Review bank ${identifier} has stale or invalid routing metadata. Use
|
|
128559
|
+
throw new ServiceUnavailableError(`Review bank ${identifier} has stale or invalid routing metadata. Use Sync Review Bank in assessment authoring.`, { reason: errorMessage2(error88) });
|
|
127981
128560
|
}
|
|
127982
128561
|
if (!bank) {
|
|
127983
128562
|
throw new ServiceUnavailableError(`Review bank ${identifier} needs its authoring mapping updated before it can serve review questions.`);
|
|
@@ -127993,28 +128572,60 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
127993
128572
|
this.reviewBankCache.set(`${identifier}\x00${bank.sourceContentRevision}`, catalog);
|
|
127994
128573
|
return catalog;
|
|
127995
128574
|
}
|
|
127996
|
-
async
|
|
127997
|
-
const cacheKey2 = [
|
|
127998
|
-
catalog.test.identifier,
|
|
127999
|
-
catalog.bank.sourceContentRevision,
|
|
128000
|
-
...itemIdentifiers
|
|
128001
|
-
].join("\x00");
|
|
128575
|
+
async cachedReviewSelection(input) {
|
|
128576
|
+
const cacheKey2 = [input.identifier, input.contentRevision, ...input.itemIdentifiers].join("\x00");
|
|
128002
128577
|
const cached3 = this.reviewSelectionCache.get(cacheKey2);
|
|
128003
128578
|
if (cached3) {
|
|
128004
|
-
return
|
|
128579
|
+
return cached3;
|
|
128005
128580
|
}
|
|
128006
|
-
const questions = await
|
|
128007
|
-
const assessment = await buildPlayableAssessment(
|
|
128008
|
-
assessment.contentRevision =
|
|
128581
|
+
const { test, questions } = await input.hydrate();
|
|
128582
|
+
const assessment = await buildPlayableAssessment(test, questions);
|
|
128583
|
+
assessment.contentRevision = input.contentRevision;
|
|
128009
128584
|
this.reviewSelectionCache.set(cacheKey2, assessment);
|
|
128585
|
+
return assessment;
|
|
128586
|
+
}
|
|
128587
|
+
async hydrateReviewBankSelection(catalog, itemIdentifiers) {
|
|
128588
|
+
const assessment = await this.cachedReviewSelection({
|
|
128589
|
+
identifier: catalog.test.identifier,
|
|
128590
|
+
contentRevision: catalog.bank.sourceContentRevision,
|
|
128591
|
+
itemIdentifiers,
|
|
128592
|
+
hydrate: async () => ({
|
|
128593
|
+
test: catalog.test,
|
|
128594
|
+
questions: await hydrateQtiTestQuestionSelection(this.requireClient(), catalog.references, itemIdentifiers)
|
|
128595
|
+
})
|
|
128596
|
+
});
|
|
128010
128597
|
return { assessment, bank: catalog.bank };
|
|
128011
128598
|
}
|
|
128012
|
-
|
|
128599
|
+
async loadPinnedReviewBankSource(metadata2) {
|
|
128600
|
+
const itemIdentifiers = metadata2.review.selections.map((selection) => selection.itemIdentifier);
|
|
128601
|
+
let assessment;
|
|
128602
|
+
try {
|
|
128603
|
+
assessment = await this.cachedReviewSelection({
|
|
128604
|
+
identifier: metadata2.selectedTest.identifier,
|
|
128605
|
+
contentRevision: metadata2.selectedTest.contentRevision,
|
|
128606
|
+
itemIdentifiers,
|
|
128607
|
+
hydrate: async () => {
|
|
128608
|
+
const test = await this.requireClient().qtiApi.assessmentTests.get(metadata2.selectedTest.identifier);
|
|
128609
|
+
return {
|
|
128610
|
+
test,
|
|
128611
|
+
questions: await hydrateQtiItemSelection(this.requireClient(), test, itemIdentifiers)
|
|
128612
|
+
};
|
|
128613
|
+
}
|
|
128614
|
+
});
|
|
128615
|
+
} catch (error88) {
|
|
128616
|
+
if (isApiError(error88) && error88.statusCode === 404) {
|
|
128617
|
+
throw new AssessmentRuntimeError("SELECTED_TEST_VERSION_UNAVAILABLE", `A selected review item for ${metadata2.selectedTest.identifier} is no longer available.`, {
|
|
128618
|
+
identifier: metadata2.selectedTest.identifier,
|
|
128619
|
+
expectedRevision: metadata2.selectedTest.contentRevision
|
|
128620
|
+
});
|
|
128621
|
+
}
|
|
128622
|
+
throw error88;
|
|
128623
|
+
}
|
|
128013
128624
|
return {
|
|
128014
|
-
assessment
|
|
128625
|
+
assessment,
|
|
128015
128626
|
bank: {
|
|
128016
|
-
bankIdentifier:
|
|
128017
|
-
sourceContentRevision:
|
|
128627
|
+
bankIdentifier: assessment.identifier,
|
|
128628
|
+
sourceContentRevision: metadata2.selectedTest.contentRevision,
|
|
128018
128629
|
bankRevision: metadata2.review.bankRevision,
|
|
128019
128630
|
items: metadata2.review.selections.map((selection) => ({
|
|
128020
128631
|
itemIdentifier: selection.itemIdentifier,
|
|
@@ -128038,9 +128649,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
128038
128649
|
if (metadata2.purpose !== "review") {
|
|
128039
128650
|
return this.loadAssessment(metadata2.selectedTest.identifier, metadata2.selectedTest.contentRevision);
|
|
128040
128651
|
}
|
|
128041
|
-
const
|
|
128042
|
-
const currentSource = await this.hydrateReviewBankSelection(catalog, metadata2.review.selections.map((selection) => selection.itemIdentifier));
|
|
128043
|
-
const source = this.pinnedReviewBankSource(currentSource, metadata2);
|
|
128652
|
+
const source = await this.loadPinnedReviewBankSource(metadata2);
|
|
128044
128653
|
return projectReviewAssessment(source.assessment, source.bank, metadata2.review.selections);
|
|
128045
128654
|
}
|
|
128046
128655
|
isPlatformRoutedDiagnosticMetadata(metadata2) {
|
|
@@ -128526,7 +129135,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
128526
129135
|
async projectReviewItemResponse(params, projection) {
|
|
128527
129136
|
await this.putReviewChildResponse(projection.result, projection.metadata, projection.itemIdentifier);
|
|
128528
129137
|
try {
|
|
128529
|
-
await this.
|
|
129138
|
+
await crossAssessmentAttemptLockBarrier(this.deps.assessmentRuntimeLock, this.deps.db, params.attemptId);
|
|
128530
129139
|
const latest = await this.peekAttempt(params);
|
|
128531
129140
|
if (this.isSettled(latest.result) && latest.metadata.purpose === "review") {
|
|
128532
129141
|
await this.restoreCompletedReviewChildResponses(latest.result, latest.metadata, new Set([projection.itemIdentifier]));
|
|
@@ -128540,21 +129149,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
128540
129149
|
});
|
|
128541
129150
|
}
|
|
128542
129151
|
}
|
|
128543
|
-
async crossAttemptLockBarrier(attemptId) {
|
|
128544
|
-
for (let attempt = 1;attempt <= TimebackAssessmentRuntimeService2.PROJECTION_BARRIER_MAX_ATTEMPTS; attempt += 1) {
|
|
128545
|
-
try {
|
|
128546
|
-
await this.deps.assessmentRuntimeLock(this.deps.db, [assessmentRuntimeAttemptLockKey(attemptId)], async () => {
|
|
128547
|
-
return;
|
|
128548
|
-
});
|
|
128549
|
-
return;
|
|
128550
|
-
} catch (error88) {
|
|
128551
|
-
if (!(error88 instanceof ServiceUnavailableError) || attempt === TimebackAssessmentRuntimeService2.PROJECTION_BARRIER_MAX_ATTEMPTS) {
|
|
128552
|
-
throw error88;
|
|
128553
|
-
}
|
|
128554
|
-
await sleep(TimebackAssessmentRuntimeService2.PROJECTION_BARRIER_RETRY_DELAY_MS);
|
|
128555
|
-
}
|
|
128556
|
-
}
|
|
128557
|
-
}
|
|
128558
129152
|
async restoreCompletedReviewChildResponses(result, metadata2, changedItemIdentifiers) {
|
|
128559
129153
|
const outcomes = metadata2.completion?.itemOutcomes;
|
|
128560
129154
|
const submissionId = metadata2.submissionId;
|
|
@@ -128700,7 +129294,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
128700
129294
|
attemptId: result.sourcedId,
|
|
128701
129295
|
responseVersion: metadata2.responseVersion,
|
|
128702
129296
|
status: "completed",
|
|
128703
|
-
result:
|
|
129297
|
+
result: buildCompletedAssessmentSubmitResult({ result, metadata: metadata2 })
|
|
128704
129298
|
};
|
|
128705
129299
|
}
|
|
128706
129300
|
const assessment = await this.loadAttemptAssessment(metadata2);
|
|
@@ -128717,58 +129311,37 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
128717
129311
|
return this.snapshot(result, metadata2, assessment, integration);
|
|
128718
129312
|
}
|
|
128719
129313
|
diagnosticSnapshot(result, metadata2, assessment, integration, state) {
|
|
128720
|
-
|
|
128721
|
-
return {
|
|
129314
|
+
return buildDiagnosticAssessmentSnapshot({
|
|
128722
129315
|
attemptId: result.sourcedId,
|
|
128723
|
-
|
|
128724
|
-
|
|
128725
|
-
|
|
128726
|
-
|
|
128727
|
-
|
|
128728
|
-
selection: {
|
|
128729
|
-
kind: "platform-routed-diagnostic",
|
|
128730
|
-
purpose: "diagnostic",
|
|
128731
|
-
definitionId: metadata2.diagnostic.definitionId,
|
|
128732
|
-
diagnosticKey: metadata2.diagnostic.diagnosticKey,
|
|
128733
|
-
routingRevision: metadata2.diagnostic.routingRevision
|
|
128734
|
-
},
|
|
128735
|
-
routing: this.diagnosticRoutingSnapshot(metadata2, state),
|
|
128736
|
-
completion: null
|
|
128737
|
-
};
|
|
129316
|
+
metadata: metadata2,
|
|
129317
|
+
assessment,
|
|
129318
|
+
state,
|
|
129319
|
+
activityData: this.activityData(metadata2, assessment, integration)
|
|
129320
|
+
});
|
|
128738
129321
|
}
|
|
128739
129322
|
diagnosticRoutingSnapshot(metadata2, state) {
|
|
128740
129323
|
return projectDiagnosticRoutingSnapshot(metadata2.diagnostic.routingRevision, state);
|
|
128741
129324
|
}
|
|
128742
129325
|
snapshot(result, metadata2, assessment, integration) {
|
|
128743
|
-
|
|
128744
|
-
const activityData = this.activityData(metadata2, assessment, integration);
|
|
128745
|
-
return {
|
|
129326
|
+
return buildConventionalAssessmentSnapshot({
|
|
128746
129327
|
attemptId: result.sourcedId,
|
|
128747
|
-
|
|
128748
|
-
|
|
128749
|
-
|
|
128750
|
-
|
|
128751
|
-
assessment: assessmentPresentationForAttempt(assessment, result.sourcedId),
|
|
128752
|
-
responses: metadata2.responses,
|
|
128753
|
-
itemSubmissions: metadata2.itemSubmissions,
|
|
128754
|
-
score: null,
|
|
128755
|
-
selection
|
|
128756
|
-
};
|
|
129328
|
+
metadata: metadata2,
|
|
129329
|
+
assessment,
|
|
129330
|
+
activityData: this.activityData(metadata2, assessment, integration)
|
|
129331
|
+
});
|
|
128757
129332
|
}
|
|
128758
129333
|
activityData(metadata2, assessment, integration) {
|
|
128759
129334
|
const course = this.trackableCourse(integration, "assessment.activity_tracking_invalid_course_metadata");
|
|
128760
|
-
|
|
128761
|
-
return;
|
|
128762
|
-
}
|
|
128763
|
-
return {
|
|
129335
|
+
return buildAssessmentActivityData({
|
|
128764
129336
|
activityId: metadata2.activityId,
|
|
128765
129337
|
activityName: assessment.title,
|
|
128766
|
-
|
|
128767
|
-
|
|
128768
|
-
};
|
|
129338
|
+
courseId: integration.courseId,
|
|
129339
|
+
course
|
|
129340
|
+
});
|
|
128769
129341
|
}
|
|
128770
129342
|
trackableCourse(integration, event, attributes2 = {}) {
|
|
128771
|
-
|
|
129343
|
+
const course = assessmentTrackableCourse(integration);
|
|
129344
|
+
if (!course) {
|
|
128772
129345
|
addEvent(event, {
|
|
128773
129346
|
...attributes2,
|
|
128774
129347
|
"app.timeback.integration_id": integration.id,
|
|
@@ -128777,27 +129350,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
128777
129350
|
});
|
|
128778
129351
|
return null;
|
|
128779
129352
|
}
|
|
128780
|
-
return
|
|
128781
|
-
}
|
|
128782
|
-
selectionContext(metadata2) {
|
|
128783
|
-
if (metadata2.purpose === "review") {
|
|
128784
|
-
return {
|
|
128785
|
-
kind: "standards-review",
|
|
128786
|
-
purpose: metadata2.purpose,
|
|
128787
|
-
standards: [...metadata2.review.standards],
|
|
128788
|
-
candidateItemsPerStandard: metadata2.review.candidateItemsPerStandard,
|
|
128789
|
-
selections: [...metadata2.review.selections],
|
|
128790
|
-
fulfillment: metadata2.review.fulfillment
|
|
128791
|
-
};
|
|
128792
|
-
}
|
|
128793
|
-
if (metadata2.purpose === "mastery") {
|
|
128794
|
-
return {
|
|
128795
|
-
kind: "standard-quiz",
|
|
128796
|
-
purpose: metadata2.purpose,
|
|
128797
|
-
standard: metadata2.mastery.standard
|
|
128798
|
-
};
|
|
128799
|
-
}
|
|
128800
|
-
return { kind: "fixed-test", purpose: metadata2.purpose };
|
|
129353
|
+
return course;
|
|
128801
129354
|
}
|
|
128802
129355
|
submittedResult(result, metadata2) {
|
|
128803
129356
|
const response = buildAssessmentSubmitResult({
|
|
@@ -128811,29 +129364,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
128811
129364
|
}
|
|
128812
129365
|
return response;
|
|
128813
129366
|
}
|
|
128814
|
-
completedResult(result, metadata2) {
|
|
128815
|
-
const xpAwarded = this.xpFromResult(result);
|
|
128816
|
-
if (xpAwarded === null) {
|
|
128817
|
-
throw new GoneError("This completed assessment is missing its recorded XP.", {
|
|
128818
|
-
attemptId: result.sourcedId
|
|
128819
|
-
});
|
|
128820
|
-
}
|
|
128821
|
-
const response = buildCompletedAssessmentSubmitResult({
|
|
128822
|
-
attemptId: result.sourcedId,
|
|
128823
|
-
metadata: metadata2,
|
|
128824
|
-
xpAwarded
|
|
128825
|
-
});
|
|
128826
|
-
if (!response) {
|
|
128827
|
-
throw new GoneError("This completed assessment has incomplete persisted results.", {
|
|
128828
|
-
attemptId: result.sourcedId
|
|
128829
|
-
});
|
|
128830
|
-
}
|
|
128831
|
-
return response;
|
|
128832
|
-
}
|
|
128833
|
-
xpFromResult(result) {
|
|
128834
|
-
const xp = result.metadata?.xp;
|
|
128835
|
-
return typeof xp === "number" && Number.isFinite(xp) && xp >= 0 ? xp : null;
|
|
128836
|
-
}
|
|
128837
129367
|
awardAgeMs(scoreDate) {
|
|
128838
129368
|
const submittedAt = Date.parse(scoreDate);
|
|
128839
129369
|
return Number.isFinite(submittedAt) ? Math.max(0, Date.now() - submittedAt) : -1;
|
|
@@ -128960,7 +129490,9 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
128960
129490
|
attemptNumber: attempt.metadata.attemptNumber,
|
|
128961
129491
|
totalQuestions: input.totalQuestions,
|
|
128962
129492
|
correctQuestions: input.correctQuestions,
|
|
128963
|
-
xpAwarded: input.xpAwarded,
|
|
129493
|
+
xpAwarded: input.award.xpAwarded,
|
|
129494
|
+
...input.award.masteredUnitsApplied !== 0 ? { masteredUnits: input.award.masteredUnitsApplied } : {},
|
|
129495
|
+
...input.award.pctCompleteApp !== undefined ? { pctCompleteApp: input.award.pctCompleteApp } : {},
|
|
128964
129496
|
submittedAt: attempt.result.scoreDate,
|
|
128965
129497
|
eventId,
|
|
128966
129498
|
...session2 ? { resumeId: session2.resumeId } : {}
|
|
@@ -129081,6 +129613,9 @@ function assertAssessmentImportCourseMatches(manifest, integration) {
|
|
|
129081
129613
|
throw new ValidationError(`This manifest is for ${manifest.subject} grade ${manifest.grade}, not ${integration.subject} grade ${integration.grade}.`);
|
|
129082
129614
|
}
|
|
129083
129615
|
}
|
|
129616
|
+
function assessmentImportsAffectReviewBank(targetStatus, entries) {
|
|
129617
|
+
return targetStatus === "live" && entries.some((entry2) => entry2.purpose === "mastery");
|
|
129618
|
+
}
|
|
129084
129619
|
function associationMetadataMatches(row, entry2) {
|
|
129085
129620
|
return row.purpose === entry2.purpose && (row.standardFramework ?? undefined) === entry2.standard?.framework && (row.standardIdentifier ?? undefined) === entry2.standard?.identifier;
|
|
129086
129621
|
}
|
|
@@ -129130,18 +129665,6 @@ function attachedAssessmentImportMessage(targetStatus, editable) {
|
|
|
129130
129665
|
}
|
|
129131
129666
|
return editable ? "Attached as a draft." : "Attached as a read-only draft owned by another app.";
|
|
129132
129667
|
}
|
|
129133
|
-
function liveReviewAssessmentImportFailures(targetStatus, candidates, existingRows) {
|
|
129134
|
-
if (targetStatus === "draft") {
|
|
129135
|
-
return new Map;
|
|
129136
|
-
}
|
|
129137
|
-
const reviews = candidates.filter((candidate) => candidate.purpose === "review");
|
|
129138
|
-
const hasExistingLiveReview = existingRows.some((row) => row.purpose === "review" && row.status === "live");
|
|
129139
|
-
if (reviews.length <= 1 && !hasExistingLiveReview) {
|
|
129140
|
-
return new Map;
|
|
129141
|
-
}
|
|
129142
|
-
const message = hasExistingLiveReview ? "Another review assessment is already live for this course." : "Only one review assessment can be imported live at a time.";
|
|
129143
|
-
return new Map(reviews.map((review) => [review.qtiTestIdentifier, message]));
|
|
129144
|
-
}
|
|
129145
129668
|
var init_timeback_assessment_import_util = __esm(() => {
|
|
129146
129669
|
init_errors2();
|
|
129147
129670
|
init_timeback_qti_authoring_util();
|
|
@@ -129210,7 +129733,9 @@ class TimebackAssessmentsService {
|
|
|
129210
129733
|
const rows = await this.deps.db.query.gameTimebackAssessmentTests.findMany({
|
|
129211
129734
|
where: eq(gameTimebackAssessmentTests.integrationId, integrationId)
|
|
129212
129735
|
});
|
|
129213
|
-
const
|
|
129736
|
+
const sinkIdentifier = reviewBankSinkIdentifier(integrationId);
|
|
129737
|
+
const visibleRows = rows.filter((row) => row.purpose !== "review" || row.qtiTestIdentifier === sinkIdentifier);
|
|
129738
|
+
const assessments2 = await runWithConcurrency(visibleRows, QTI_HYDRATION_CONCURRENCY, async (row) => {
|
|
129214
129739
|
try {
|
|
129215
129740
|
const test = await client2.qtiApi.assessmentTests.get(row.qtiTestIdentifier);
|
|
129216
129741
|
return {
|
|
@@ -129218,7 +129743,7 @@ class TimebackAssessmentsService {
|
|
|
129218
129743
|
title: test.title,
|
|
129219
129744
|
questionCount: countQtiTestItems(test),
|
|
129220
129745
|
available: true,
|
|
129221
|
-
editable: isQtiTestOwnedByGame(test, ownership.gameSlug)
|
|
129746
|
+
editable: row.purpose !== "review" && isQtiTestOwnedByGame(test, ownership.gameSlug)
|
|
129222
129747
|
};
|
|
129223
129748
|
} catch (error88) {
|
|
129224
129749
|
addEvent("assessment.qti_fetch_failed", {
|
|
@@ -129238,8 +129763,11 @@ class TimebackAssessmentsService {
|
|
|
129238
129763
|
return assessments2.toSorted((a, b) => a.title.localeCompare(b.title));
|
|
129239
129764
|
}
|
|
129240
129765
|
async createAssessment(integrationId, input) {
|
|
129241
|
-
if (input.purpose === "diagnostic") {
|
|
129242
|
-
throw new ValidationError("Adaptive diagnostics must be created by importing assessment JSON with a routing sidecar.");
|
|
129766
|
+
if (input.purpose === "diagnostic" || input.purpose === "review") {
|
|
129767
|
+
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.");
|
|
129768
|
+
}
|
|
129769
|
+
if (input.assessmentKey === reviewBankSinkIdentifier(integrationId)) {
|
|
129770
|
+
throw new ValidationError("This assessment key is reserved for the review bank.");
|
|
129243
129771
|
}
|
|
129244
129772
|
const client2 = this.requireClient();
|
|
129245
129773
|
const ownership = await this.requireQtiTestOwnershipContext(integrationId);
|
|
@@ -129315,24 +129843,41 @@ class TimebackAssessmentsService {
|
|
|
129315
129843
|
index: index2,
|
|
129316
129844
|
standard: this.requirePurposeStandard(assessment.purpose, assessment.standard)
|
|
129317
129845
|
}));
|
|
129318
|
-
const
|
|
129319
|
-
|
|
129320
|
-
|
|
129321
|
-
|
|
129322
|
-
const existingByIdentifier = new Map(existingRows.map((row) => [row.qtiTestIdentifier, row]));
|
|
129846
|
+
const sinkIdentifier = reviewBankSinkIdentifier(integrationId);
|
|
129847
|
+
if (entries.some((entry2) => entry2.assessmentKey === sinkIdentifier || entry2.qtiTestIdentifier === sinkIdentifier || entry2.purpose === "review")) {
|
|
129848
|
+
throw new ValidationError("The review bank is system-managed and cannot be attached manually.");
|
|
129849
|
+
}
|
|
129323
129850
|
const validated = await this.validateAssessmentImportEntries(client2, entries, manifest.targetStatus);
|
|
129324
129851
|
const results = [];
|
|
129325
|
-
const
|
|
129326
|
-
|
|
129327
|
-
existingByKey,
|
|
129328
|
-
existingByIdentifier,
|
|
129329
|
-
|
|
129330
|
-
|
|
129331
|
-
|
|
129332
|
-
|
|
129333
|
-
|
|
129334
|
-
|
|
129335
|
-
|
|
129852
|
+
const affectsReviewBank = assessmentImportsAffectReviewBank(manifest.targetStatus, entries);
|
|
129853
|
+
const planAndInsert = async (db2, existingRows) => {
|
|
129854
|
+
const existingByKey = new Map(existingRows.flatMap((row) => row.assessmentKey ? [[row.assessmentKey, row]] : []));
|
|
129855
|
+
const existingByIdentifier = new Map(existingRows.map((row) => [row.qtiTestIdentifier, row]));
|
|
129856
|
+
const pendingInserts = this.planAssessmentImports({
|
|
129857
|
+
validated,
|
|
129858
|
+
existingByKey,
|
|
129859
|
+
existingByIdentifier,
|
|
129860
|
+
gameSlug,
|
|
129861
|
+
results
|
|
129862
|
+
});
|
|
129863
|
+
await this.insertImportedAssessments({
|
|
129864
|
+
db: db2,
|
|
129865
|
+
integrationId,
|
|
129866
|
+
pending: pendingInserts,
|
|
129867
|
+
targetStatus: manifest.targetStatus,
|
|
129868
|
+
gameSlug,
|
|
129869
|
+
results,
|
|
129870
|
+
abortOnError: affectsReviewBank
|
|
129871
|
+
});
|
|
129872
|
+
};
|
|
129873
|
+
if (affectsReviewBank) {
|
|
129874
|
+
await this.withLockedIntegrationAssessmentRows(integrationId, async (tx, lockedAssociations) => planAndInsert(tx, lockedAssociations));
|
|
129875
|
+
} else {
|
|
129876
|
+
const existingRows = await this.deps.db.query.gameTimebackAssessmentTests.findMany({
|
|
129877
|
+
where: eq(gameTimebackAssessmentTests.integrationId, integrationId)
|
|
129878
|
+
});
|
|
129879
|
+
await planAndInsert(this.deps.db, existingRows);
|
|
129880
|
+
}
|
|
129336
129881
|
setAttribute("app.assessment.operation", "bulk_attach_existing");
|
|
129337
129882
|
setAttribute("app.assessment.bulk_attach.target_status", manifest.targetStatus);
|
|
129338
129883
|
setAttribute("app.assessment.bulk_attach.created", results.filter((result) => result.status === "created").length);
|
|
@@ -129345,11 +129890,6 @@ class TimebackAssessmentsService {
|
|
|
129345
129890
|
assertManagedAssessmentIdentity(loaded.test, entry2.assessmentKey);
|
|
129346
129891
|
assertAssessmentHasQuestions(loaded.questions.questions);
|
|
129347
129892
|
assertPlayableAssessmentImportQuestions(targetStatus, loaded.questions.questions.map(({ question }) => question));
|
|
129348
|
-
if (entry2.purpose === "review") {
|
|
129349
|
-
assertReviewAssessmentHasStandards(loaded.questions.questions.map(({ question }) => reviewRoutingStandardRefsFromMetadata(question.metadata, {
|
|
129350
|
-
customOnly: true
|
|
129351
|
-
}).length));
|
|
129352
|
-
}
|
|
129353
129893
|
return { entry: entry2, test: loaded.test };
|
|
129354
129894
|
} catch (error88) {
|
|
129355
129895
|
return { entry: entry2, error: error88 };
|
|
@@ -129395,25 +129935,13 @@ class TimebackAssessmentsService {
|
|
|
129395
129935
|
}
|
|
129396
129936
|
return pendingInserts;
|
|
129397
129937
|
}
|
|
129398
|
-
|
|
129399
|
-
for (const { entry: entry2, test } of pending) {
|
|
129400
|
-
const message = failures.get(entry2.qtiTestIdentifier);
|
|
129401
|
-
if (message) {
|
|
129402
|
-
results[entry2.index] = {
|
|
129403
|
-
...this.associationImportResultBase(entry2, test, gameSlug),
|
|
129404
|
-
status: "failed",
|
|
129405
|
-
message
|
|
129406
|
-
};
|
|
129407
|
-
}
|
|
129408
|
-
}
|
|
129409
|
-
}
|
|
129410
|
-
async concurrentAssessmentImportDecision(integrationId, entry2) {
|
|
129938
|
+
async concurrentAssessmentImportDecision(db2, integrationId, entry2) {
|
|
129411
129939
|
try {
|
|
129412
129940
|
const [byKey, byIdentifier] = await Promise.all([
|
|
129413
|
-
|
|
129941
|
+
db2.query.gameTimebackAssessmentTests.findFirst({
|
|
129414
129942
|
where: and(eq(gameTimebackAssessmentTests.integrationId, integrationId), eq(gameTimebackAssessmentTests.assessmentKey, entry2.assessmentKey))
|
|
129415
129943
|
}),
|
|
129416
|
-
|
|
129944
|
+
db2.query.gameTimebackAssessmentTests.findFirst({
|
|
129417
129945
|
where: and(eq(gameTimebackAssessmentTests.integrationId, integrationId), eq(gameTimebackAssessmentTests.qtiTestIdentifier, entry2.qtiTestIdentifier))
|
|
129418
129946
|
})
|
|
129419
129947
|
]);
|
|
@@ -129425,39 +129953,53 @@ class TimebackAssessmentsService {
|
|
|
129425
129953
|
return null;
|
|
129426
129954
|
}
|
|
129427
129955
|
}
|
|
129428
|
-
async insertImportedAssessments(
|
|
129429
|
-
for (const { entry: entry2, test } of pending) {
|
|
129430
|
-
const base = this.associationImportResultBase(entry2, test, gameSlug);
|
|
129956
|
+
async insertImportedAssessments(input) {
|
|
129957
|
+
for (const { entry: entry2, test } of input.pending) {
|
|
129958
|
+
const base = this.associationImportResultBase(entry2, test, input.gameSlug);
|
|
129431
129959
|
try {
|
|
129432
|
-
const [row] = await
|
|
129433
|
-
integrationId,
|
|
129960
|
+
const [row] = await input.db.insert(gameTimebackAssessmentTests).values({
|
|
129961
|
+
integrationId: input.integrationId,
|
|
129434
129962
|
assessmentKey: entry2.assessmentKey,
|
|
129435
129963
|
qtiTestIdentifier: entry2.qtiTestIdentifier,
|
|
129436
129964
|
purpose: entry2.purpose,
|
|
129437
|
-
status: targetStatus,
|
|
129965
|
+
status: input.targetStatus,
|
|
129438
129966
|
sortOrder: entry2.sortOrder,
|
|
129439
129967
|
standardFramework: entry2.standard?.framework,
|
|
129440
129968
|
standardIdentifier: entry2.standard?.identifier
|
|
129441
|
-
}).returning();
|
|
129969
|
+
}).onConflictDoNothing().returning();
|
|
129442
129970
|
if (!row) {
|
|
129443
|
-
|
|
129971
|
+
const concurrent = await this.concurrentAssessmentImportDecision(input.db, input.integrationId, entry2);
|
|
129972
|
+
if (concurrent?.decision.kind === "skip" || concurrent?.decision.kind === "fail") {
|
|
129973
|
+
input.results[entry2.index] = {
|
|
129974
|
+
...base,
|
|
129975
|
+
status: concurrent.decision.status,
|
|
129976
|
+
...concurrent.byKey ? { association: this.associationImportSummary(concurrent.byKey) } : {},
|
|
129977
|
+
message: concurrent.decision.message
|
|
129978
|
+
};
|
|
129979
|
+
} else {
|
|
129980
|
+
throw new Error("Assessment association insert conflicted");
|
|
129981
|
+
}
|
|
129982
|
+
} else {
|
|
129983
|
+
input.results[entry2.index] = {
|
|
129984
|
+
...base,
|
|
129985
|
+
status: "created",
|
|
129986
|
+
association: this.associationImportSummary(row),
|
|
129987
|
+
message: attachedAssessmentImportMessage(input.targetStatus, base.editable ?? false)
|
|
129988
|
+
};
|
|
129444
129989
|
}
|
|
129445
|
-
results[entry2.index] = {
|
|
129446
|
-
...base,
|
|
129447
|
-
status: "created",
|
|
129448
|
-
association: this.associationImportSummary(row),
|
|
129449
|
-
message: attachedAssessmentImportMessage(targetStatus, base.editable ?? false)
|
|
129450
|
-
};
|
|
129451
129990
|
} catch (error88) {
|
|
129452
|
-
|
|
129991
|
+
if (input.abortOnError) {
|
|
129992
|
+
throw error88;
|
|
129993
|
+
}
|
|
129994
|
+
input.results[entry2.index] = {
|
|
129453
129995
|
...base,
|
|
129454
129996
|
status: "failed",
|
|
129455
129997
|
message: `Association attach failed: ${errorMessage2(error88)}`
|
|
129456
129998
|
};
|
|
129457
129999
|
if (isUniqueViolation(error88)) {
|
|
129458
|
-
const concurrent = await this.concurrentAssessmentImportDecision(integrationId, entry2);
|
|
130000
|
+
const concurrent = await this.concurrentAssessmentImportDecision(input.db, input.integrationId, entry2);
|
|
129459
130001
|
if (concurrent?.decision.kind === "skip" || concurrent?.decision.kind === "fail") {
|
|
129460
|
-
results[entry2.index] = {
|
|
130002
|
+
input.results[entry2.index] = {
|
|
129461
130003
|
...base,
|
|
129462
130004
|
status: concurrent.decision.status,
|
|
129463
130005
|
...concurrent.byKey ? { association: this.associationImportSummary(concurrent.byKey) } : {},
|
|
@@ -129471,6 +130013,12 @@ class TimebackAssessmentsService {
|
|
|
129471
130013
|
async updateAssessment(integrationId, qtiTestIdentifier, input) {
|
|
129472
130014
|
try {
|
|
129473
130015
|
return await this.withLockedAssessmentAssociations(integrationId, qtiTestIdentifier, async (row, tx, associations) => {
|
|
130016
|
+
if (row.purpose === "review" || qtiTestIdentifier === reviewBankSinkIdentifier(integrationId)) {
|
|
130017
|
+
throw new ValidationError("The review bank is system-managed and cannot be edited.");
|
|
130018
|
+
}
|
|
130019
|
+
if (input.purpose === "review") {
|
|
130020
|
+
throw new ValidationError("Assessment purpose cannot be changed to the system-managed review bank.");
|
|
130021
|
+
}
|
|
129474
130022
|
const nextPurpose = input.purpose ?? row.purpose;
|
|
129475
130023
|
assertPurposeChangeDraft(row, nextPurpose);
|
|
129476
130024
|
const requestedStandard = input.standard ? this.canonicalAssessmentStandard(input.standard) : undefined;
|
|
@@ -129489,19 +130037,10 @@ class TimebackAssessmentsService {
|
|
|
129489
130037
|
...associationUpdates,
|
|
129490
130038
|
...diagnosticChanges.updates
|
|
129491
130039
|
};
|
|
129492
|
-
const nextStatus = input.status ?? row.status;
|
|
129493
130040
|
const publishing = input.status !== undefined && isAssessmentPublicationTransition(row.status, input.status);
|
|
129494
|
-
const activatingReview = nextPurpose === "review" && nextStatus === "live" && (row.purpose !== "review" || row.status !== "live");
|
|
129495
130041
|
if (input.status !== undefined) {
|
|
129496
130042
|
validateAssessmentStatusTransition(row.status, input.status);
|
|
129497
130043
|
}
|
|
129498
|
-
if (!publishing && !activatingReview && nextPurpose === "review" && row.purpose !== "review") {
|
|
129499
|
-
const ownership = await this.requireQtiTestOwnershipContext(integrationId, tx);
|
|
129500
|
-
await this.rebuildReviewMapping(this.requireClient(), qtiTestIdentifier, {
|
|
129501
|
-
kind: "owned",
|
|
129502
|
-
gameSlug: ownership.gameSlug
|
|
129503
|
-
});
|
|
129504
|
-
}
|
|
129505
130044
|
if (input.title !== undefined) {
|
|
129506
130045
|
assertDraftAssessment(row);
|
|
129507
130046
|
assertAllAssessmentAssociationsDraft(associations);
|
|
@@ -129511,7 +130050,7 @@ class TimebackAssessmentsService {
|
|
|
129511
130050
|
assertQtiTestOwnedByGame(test, ownership.gameSlug);
|
|
129512
130051
|
await client2.qtiApi.assessmentTests.update(qtiTestIdentifier, buildQtiTestUpdateInput(test, input.title));
|
|
129513
130052
|
}
|
|
129514
|
-
if (publishing
|
|
130053
|
+
if (publishing) {
|
|
129515
130054
|
if (nextPurpose === "diagnostic" && !nextDiagnostic) {
|
|
129516
130055
|
throw new ValidationError("Platform-routed diagnostics require a diagnostic key and routing manifest before publication.");
|
|
129517
130056
|
}
|
|
@@ -129521,8 +130060,7 @@ class TimebackAssessmentsService {
|
|
|
129521
130060
|
updates.diagnosticKey = nextDiagnostic.diagnosticKey;
|
|
129522
130061
|
updates.diagnosticRoutingManifest = nextDiagnostic.routingManifest;
|
|
129523
130062
|
} else {
|
|
129524
|
-
|
|
129525
|
-
await this.validateAssessmentHasQuestions(loaded, reviewOwnership?.gameSlug);
|
|
130063
|
+
await this.validateAssessmentHasQuestions(loaded);
|
|
129526
130064
|
}
|
|
129527
130065
|
if (publishing) {
|
|
129528
130066
|
const publication = await this.publishManagedAssessment(row, nextPurpose, nextDiagnostic?.routingManifest ?? null, loaded);
|
|
@@ -129545,15 +130083,15 @@ class TimebackAssessmentsService {
|
|
|
129545
130083
|
if (databaseConstraintName(error88) === "game_timeback_assessment_tests_one_live_diagnostic_key_idx") {
|
|
129546
130084
|
throw new ValidationError("Only one live revision of a diagnostic key is allowed. Archive the current revision before publishing another.");
|
|
129547
130085
|
}
|
|
129548
|
-
if (databaseConstraintName(error88) === "game_timeback_assessment_tests_one_live_review_idx") {
|
|
129549
|
-
throw new ValidationError("Only one live review assessment is allowed. Archive the current review bank before publishing another.");
|
|
129550
|
-
}
|
|
129551
130086
|
}
|
|
129552
130087
|
throw error88;
|
|
129553
130088
|
}
|
|
129554
130089
|
}
|
|
129555
130090
|
async removeAssessment(integrationId, qtiTestIdentifier) {
|
|
129556
130091
|
return this.withLockedAssessmentAssociations(integrationId, qtiTestIdentifier, async (row, tx) => {
|
|
130092
|
+
if (qtiTestIdentifier === reviewBankSinkIdentifier(integrationId)) {
|
|
130093
|
+
throw new ValidationError("The review bank is permanent and cannot be removed or archived.");
|
|
130094
|
+
}
|
|
129557
130095
|
const plan = planAssessmentRemoval(row.status);
|
|
129558
130096
|
if (plan.kind === "delete") {
|
|
129559
130097
|
await tx.delete(gameTimebackAssessmentTests).where(eq(gameTimebackAssessmentTests.id, row.id));
|
|
@@ -129567,6 +130105,9 @@ class TimebackAssessmentsService {
|
|
|
129567
130105
|
});
|
|
129568
130106
|
}
|
|
129569
130107
|
async reorderAssessments(integrationId, purpose, testIdentifiers) {
|
|
130108
|
+
if (purpose === "review") {
|
|
130109
|
+
throw new ValidationError("The review bank is system-managed and cannot be reordered.");
|
|
130110
|
+
}
|
|
129570
130111
|
await this.requireIntegration(integrationId);
|
|
129571
130112
|
validateUniqueAssessmentIdentifiers(testIdentifiers);
|
|
129572
130113
|
const updatedAt = new Date;
|
|
@@ -129624,19 +130165,96 @@ class TimebackAssessmentsService {
|
|
|
129624
130165
|
});
|
|
129625
130166
|
return { ...result, questions };
|
|
129626
130167
|
}
|
|
129627
|
-
async
|
|
129628
|
-
|
|
129629
|
-
|
|
129630
|
-
|
|
130168
|
+
async synchronizeReviewBank(integrationId) {
|
|
130169
|
+
const client2 = this.requireClient();
|
|
130170
|
+
await this.requireIntegration(integrationId);
|
|
130171
|
+
const sinkIdentifier = reviewBankSinkIdentifier(integrationId);
|
|
130172
|
+
for (let attempt = 0;attempt < REVIEW_BANK_SYNC_ATTEMPTS; attempt++) {
|
|
130173
|
+
const associations = await this.deps.db.query.gameTimebackAssessmentTests.findMany({
|
|
130174
|
+
where: eq(gameTimebackAssessmentTests.integrationId, integrationId)
|
|
130175
|
+
});
|
|
130176
|
+
const masterySources = this.reviewBankMasterySources(associations);
|
|
130177
|
+
const contributorFingerprint = await reviewBankContributorFingerprint(masterySources);
|
|
130178
|
+
const existingSink = await this.findReviewBankSink(client2, integrationId);
|
|
130179
|
+
if (existingSink) {
|
|
130180
|
+
assertReviewBankSinkOwnership(existingSink, integrationId);
|
|
130181
|
+
}
|
|
130182
|
+
let unchangedResult = null;
|
|
130183
|
+
if (existingSink) {
|
|
130184
|
+
try {
|
|
130185
|
+
unchangedResult = await unchangedReviewBankSynchronizationResult({
|
|
130186
|
+
sink: existingSink,
|
|
130187
|
+
integrationId,
|
|
130188
|
+
contributorFingerprint,
|
|
130189
|
+
contributorCount: masterySources.length
|
|
130190
|
+
});
|
|
130191
|
+
} catch (error88) {
|
|
130192
|
+
addEvent("assessment.review_mapping_noop_validation_failed", {
|
|
130193
|
+
"app.assessment.qti_test_identifier": existingSink.identifier,
|
|
130194
|
+
"exception.type": errorType(error88),
|
|
130195
|
+
"app.error.message": errorMessage2(error88)
|
|
130196
|
+
});
|
|
130197
|
+
}
|
|
129631
130198
|
}
|
|
129632
|
-
|
|
129633
|
-
|
|
129634
|
-
|
|
130199
|
+
let plan = null;
|
|
130200
|
+
let addedItemCount = 0;
|
|
130201
|
+
let removedItemCount = 0;
|
|
130202
|
+
let catalogUnchanged = unchangedResult !== null;
|
|
130203
|
+
if (!unchangedResult) {
|
|
130204
|
+
const contributors = await runWithConcurrency(masterySources, QTI_HYDRATION_CONCURRENCY, async (source) => {
|
|
130205
|
+
const test = await client2.qtiApi.assessmentTests.get(source.qtiTestIdentifier);
|
|
130206
|
+
assertManagedAssessmentIdentity(test, source.assessmentKey);
|
|
130207
|
+
return { ...source, test };
|
|
130208
|
+
});
|
|
130209
|
+
plan = await prepareReviewBankSynchronization({
|
|
130210
|
+
bankIdentifier: sinkIdentifier,
|
|
130211
|
+
contributors,
|
|
130212
|
+
contributorFingerprint
|
|
130213
|
+
});
|
|
130214
|
+
const diff = reviewBankSynchronizationDiff({ existingSink, plan });
|
|
130215
|
+
addedItemCount = diff.addedItemCount;
|
|
130216
|
+
removedItemCount = diff.removedItemCount;
|
|
130217
|
+
catalogUnchanged = diff.unchanged;
|
|
130218
|
+
}
|
|
130219
|
+
const committed = await this.withLockedIntegrationAssessmentRows(integrationId, async (tx, lockedAssociations) => {
|
|
130220
|
+
const lockedSources = this.reviewBankMasterySources(lockedAssociations);
|
|
130221
|
+
const lockedFingerprint = await reviewBankContributorFingerprint(lockedSources);
|
|
130222
|
+
if (lockedFingerprint !== contributorFingerprint) {
|
|
130223
|
+
return null;
|
|
130224
|
+
}
|
|
130225
|
+
const associationChanged = await this.installReviewBankAssociation({
|
|
130226
|
+
tx,
|
|
130227
|
+
integrationId,
|
|
130228
|
+
associations: lockedAssociations,
|
|
130229
|
+
sinkIdentifier,
|
|
130230
|
+
beforeInstall: async () => {
|
|
130231
|
+
if (!catalogUnchanged) {
|
|
130232
|
+
await this.writeReviewBankSink({
|
|
130233
|
+
client: client2,
|
|
130234
|
+
integrationId,
|
|
130235
|
+
existingSink,
|
|
130236
|
+
plan,
|
|
130237
|
+
metadata: reviewBankSinkMetadata(integrationId, plan)
|
|
130238
|
+
});
|
|
130239
|
+
}
|
|
130240
|
+
}
|
|
130241
|
+
});
|
|
130242
|
+
const unchanged = catalogUnchanged && !associationChanged;
|
|
130243
|
+
const result = unchangedResult ? { ...unchangedResult, unchanged } : {
|
|
130244
|
+
qtiTestIdentifier: sinkIdentifier,
|
|
130245
|
+
...plan.summary,
|
|
130246
|
+
addedItemCount,
|
|
130247
|
+
removedItemCount,
|
|
130248
|
+
unchanged,
|
|
130249
|
+
warnings: plan.warnings
|
|
130250
|
+
};
|
|
130251
|
+
return this.reviewMappingResult(result);
|
|
129635
130252
|
});
|
|
129636
|
-
|
|
129637
|
-
|
|
129638
|
-
|
|
129639
|
-
}
|
|
130253
|
+
if (committed) {
|
|
130254
|
+
return committed;
|
|
130255
|
+
}
|
|
130256
|
+
}
|
|
130257
|
+
throw new ValidationError("Mastery assessments changed while the review bank was synchronizing. Try again.");
|
|
129640
130258
|
}
|
|
129641
130259
|
async listQuestionLibrary(integrationId, params) {
|
|
129642
130260
|
const client2 = this.requireClient();
|
|
@@ -129656,15 +130274,18 @@ class TimebackAssessmentsService {
|
|
|
129656
130274
|
purpose,
|
|
129657
130275
|
standard: standardInput
|
|
129658
130276
|
} = input;
|
|
129659
|
-
if (purpose === "diagnostic") {
|
|
129660
|
-
throw new ValidationError("Adaptive diagnostics must be created by importing assessment JSON with a routing sidecar.");
|
|
130277
|
+
if (purpose === "diagnostic" || purpose === "review") {
|
|
130278
|
+
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.");
|
|
130279
|
+
}
|
|
130280
|
+
if (assessmentKey === reviewBankSinkIdentifier(integrationId) || targetTestIdentifier === reviewBankSinkIdentifier(integrationId)) {
|
|
130281
|
+
throw new ValidationError("This assessment identity is reserved for the review bank.");
|
|
129661
130282
|
}
|
|
129662
130283
|
const client2 = this.requireClient();
|
|
129663
130284
|
const ownership = await this.requireQtiTestOwnershipContext(integrationId);
|
|
129664
130285
|
const { integration } = ownership;
|
|
129665
130286
|
const standard = this.requirePurposeStandard(purpose, standardInput);
|
|
129666
130287
|
const source = await client2.qtiApi.assessmentTests.get(sourceTestIdentifier);
|
|
129667
|
-
const itemPlan = buildQtiAssessmentItemCopyPlan(source, targetTestIdentifier, (identifier) =>
|
|
130288
|
+
const itemPlan = buildQtiAssessmentItemCopyPlan(source, targetTestIdentifier, (identifier) => qtiItemHref(client2, identifier));
|
|
129668
130289
|
const itemCopies = await runWithConcurrency(itemPlan, QTI_HYDRATION_CONCURRENCY, async (plan) => {
|
|
129669
130290
|
const sourceItem = await client2.qtiApi.assessmentItems.get(plan.sourceIdentifier);
|
|
129670
130291
|
return {
|
|
@@ -129789,7 +130410,7 @@ class TimebackAssessmentsService {
|
|
|
129789
130410
|
};
|
|
129790
130411
|
}
|
|
129791
130412
|
const section = await this.qtiSectionItems(client2, qtiTestIdentifier, undefined, undefined, ownership.test);
|
|
129792
|
-
const href =
|
|
130413
|
+
const href = qtiItemHref(client2, itemIdentifier);
|
|
129793
130414
|
let item;
|
|
129794
130415
|
let itemCreationAttempted = false;
|
|
129795
130416
|
let referenceCreationAttempted = false;
|
|
@@ -129899,7 +130520,7 @@ class TimebackAssessmentsService {
|
|
|
129899
130520
|
const result = await section.items.reorder({
|
|
129900
130521
|
items: items.map((item) => ({
|
|
129901
130522
|
identifier: item.identifier,
|
|
129902
|
-
href: item.href ??
|
|
130523
|
+
href: item.href ?? qtiItemHref(client2, item.identifier),
|
|
129903
130524
|
sequence: item.sequence
|
|
129904
130525
|
}))
|
|
129905
130526
|
});
|
|
@@ -129913,6 +130534,121 @@ class TimebackAssessmentsService {
|
|
|
129913
130534
|
}
|
|
129914
130535
|
return this.deps.timeback;
|
|
129915
130536
|
}
|
|
130537
|
+
reviewBankMasterySources(associations) {
|
|
130538
|
+
return associations.filter((association) => association.purpose === "mastery" && association.status === "live").map((association) => {
|
|
130539
|
+
const standard = assessmentStandardForRow(association);
|
|
130540
|
+
if (!association.assessmentKey || !standard) {
|
|
130541
|
+
throw new ValidationError(`Live mastery assessment ${association.qtiTestIdentifier} requires a managed identity and standard`);
|
|
130542
|
+
}
|
|
130543
|
+
if (assessmentKeyFromManagedQtiIdentifier(association.qtiTestIdentifier) !== association.assessmentKey) {
|
|
130544
|
+
throw new ValidationError(`Live mastery assessment ${association.qtiTestIdentifier} is not a managed publication for ${association.assessmentKey}`);
|
|
130545
|
+
}
|
|
130546
|
+
return {
|
|
130547
|
+
assessmentKey: association.assessmentKey,
|
|
130548
|
+
qtiTestIdentifier: association.qtiTestIdentifier,
|
|
130549
|
+
standard
|
|
130550
|
+
};
|
|
130551
|
+
});
|
|
130552
|
+
}
|
|
130553
|
+
async writeReviewBankSink(input) {
|
|
130554
|
+
function sinkInput(title, includeIdentifier = false) {
|
|
130555
|
+
return buildReviewBankSinkInput({
|
|
130556
|
+
identifier: input.plan.bankIdentifier,
|
|
130557
|
+
title,
|
|
130558
|
+
metadata: input.metadata,
|
|
130559
|
+
itemIdentifiers: input.plan.itemIdentifiers,
|
|
130560
|
+
itemHref: (identifier) => qtiItemHref(input.client, identifier),
|
|
130561
|
+
includeIdentifier
|
|
130562
|
+
});
|
|
130563
|
+
}
|
|
130564
|
+
if (input.existingSink) {
|
|
130565
|
+
await input.client.qtiApi.assessmentTests.update(input.plan.bankIdentifier, sinkInput(input.existingSink.title));
|
|
130566
|
+
return;
|
|
130567
|
+
}
|
|
130568
|
+
try {
|
|
130569
|
+
const createInput = sinkInput(REVIEW_BANK_TITLE, true);
|
|
130570
|
+
await input.client.qtiApi.assessmentTests.create(createInput);
|
|
130571
|
+
} catch (error88) {
|
|
130572
|
+
if (!isApiError(error88) || error88.statusCode !== 409) {
|
|
130573
|
+
throw error88;
|
|
130574
|
+
}
|
|
130575
|
+
const concurrentlyCreated = await input.client.qtiApi.assessmentTests.get(input.plan.bankIdentifier);
|
|
130576
|
+
assertReviewBankSinkOwnership(concurrentlyCreated, input.integrationId);
|
|
130577
|
+
await input.client.qtiApi.assessmentTests.update(input.plan.bankIdentifier, sinkInput(concurrentlyCreated.title));
|
|
130578
|
+
}
|
|
130579
|
+
}
|
|
130580
|
+
async installReviewBankAssociation(input) {
|
|
130581
|
+
const singleton = input.associations.find((association) => association.qtiTestIdentifier === input.sinkIdentifier);
|
|
130582
|
+
if (singleton && singleton.purpose !== "review") {
|
|
130583
|
+
throw new ValidationError("The reserved review-bank identifier is associated with another assessment purpose.");
|
|
130584
|
+
}
|
|
130585
|
+
const legacyReviews = input.associations.filter((association) => association.purpose === "review" && association.qtiTestIdentifier !== input.sinkIdentifier);
|
|
130586
|
+
const reservedKeyOwner = input.associations.find((association) => association.assessmentKey === input.sinkIdentifier && association.qtiTestIdentifier !== input.sinkIdentifier);
|
|
130587
|
+
if (reservedKeyOwner && reservedKeyOwner.purpose !== "review") {
|
|
130588
|
+
throw new ValidationError("The reserved review-bank assessment key is already in use.");
|
|
130589
|
+
}
|
|
130590
|
+
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);
|
|
130591
|
+
const legacyCurrent = legacyReviews.every((association) => association.status === "archived" && association.sortOrder === null && association.assessmentKey !== input.sinkIdentifier);
|
|
130592
|
+
const changed = !singletonCurrent || !legacyCurrent;
|
|
130593
|
+
await input.beforeInstall();
|
|
130594
|
+
for (const legacy of legacyReviews) {
|
|
130595
|
+
if (legacy.status !== "archived" || legacy.sortOrder !== null || legacy.assessmentKey === input.sinkIdentifier) {
|
|
130596
|
+
await input.tx.update(gameTimebackAssessmentTests).set({
|
|
130597
|
+
status: "archived",
|
|
130598
|
+
sortOrder: null,
|
|
130599
|
+
...legacy.assessmentKey === input.sinkIdentifier ? { assessmentKey: null } : {},
|
|
130600
|
+
updatedAt: new Date
|
|
130601
|
+
}).where(eq(gameTimebackAssessmentTests.id, legacy.id));
|
|
130602
|
+
}
|
|
130603
|
+
}
|
|
130604
|
+
if (singletonCurrent) {
|
|
130605
|
+
return changed;
|
|
130606
|
+
}
|
|
130607
|
+
if (singleton) {
|
|
130608
|
+
await input.tx.update(gameTimebackAssessmentTests).set({
|
|
130609
|
+
assessmentKey: input.sinkIdentifier,
|
|
130610
|
+
purpose: "review",
|
|
130611
|
+
status: "live",
|
|
130612
|
+
sortOrder: null,
|
|
130613
|
+
standardFramework: null,
|
|
130614
|
+
standardIdentifier: null,
|
|
130615
|
+
diagnosticKey: null,
|
|
130616
|
+
diagnosticRoutingManifest: null,
|
|
130617
|
+
updatedAt: new Date
|
|
130618
|
+
}).where(eq(gameTimebackAssessmentTests.id, singleton.id));
|
|
130619
|
+
return changed;
|
|
130620
|
+
}
|
|
130621
|
+
await input.tx.insert(gameTimebackAssessmentTests).values({
|
|
130622
|
+
integrationId: input.integrationId,
|
|
130623
|
+
assessmentKey: input.sinkIdentifier,
|
|
130624
|
+
qtiTestIdentifier: input.sinkIdentifier,
|
|
130625
|
+
purpose: "review",
|
|
130626
|
+
status: "live",
|
|
130627
|
+
sortOrder: null,
|
|
130628
|
+
standardFramework: null,
|
|
130629
|
+
standardIdentifier: null,
|
|
130630
|
+
diagnosticKey: null,
|
|
130631
|
+
diagnosticRoutingManifest: null
|
|
130632
|
+
});
|
|
130633
|
+
return changed;
|
|
130634
|
+
}
|
|
130635
|
+
reviewMappingResult(result) {
|
|
130636
|
+
setAttribute("app.assessment.operation", "update_review_mapping");
|
|
130637
|
+
setAttribute("app.assessment.review_mapping_item_count", result.itemCount);
|
|
130638
|
+
setAttribute("app.assessment.review_mapping_added_item_count", result.addedItemCount);
|
|
130639
|
+
setAttribute("app.assessment.review_mapping_removed_item_count", result.removedItemCount);
|
|
130640
|
+
return result;
|
|
130641
|
+
}
|
|
130642
|
+
async withLockedIntegrationAssessmentRows(integrationId, action) {
|
|
130643
|
+
return this.deps.db.transaction(async (tx) => {
|
|
130644
|
+
const [integration] = await tx.select({ id: gameTimebackIntegrations.id }).from(gameTimebackIntegrations).where(and(eq(gameTimebackIntegrations.id, integrationId), isActiveGameTimebackIntegrationStatus())).for("update");
|
|
130645
|
+
if (!integration) {
|
|
130646
|
+
throw new NotFoundError(`Integration not found: ${integrationId}`);
|
|
130647
|
+
}
|
|
130648
|
+
const rows = await tx.select().from(gameTimebackAssessmentTests).where(eq(gameTimebackAssessmentTests.integrationId, integrationId)).orderBy(gameTimebackAssessmentTests.id).for("update");
|
|
130649
|
+
return action(tx, rows);
|
|
130650
|
+
});
|
|
130651
|
+
}
|
|
129916
130652
|
async withLockedAssessmentAssociations(integrationId, qtiTestIdentifier, action) {
|
|
129917
130653
|
return this.deps.db.transaction(async (tx) => {
|
|
129918
130654
|
const rows = await tx.select().from(gameTimebackAssessmentTests).where(eq(gameTimebackAssessmentTests.qtiTestIdentifier, qtiTestIdentifier)).orderBy(gameTimebackAssessmentTests.id).for("update");
|
|
@@ -129997,8 +130733,16 @@ class TimebackAssessmentsService {
|
|
|
129997
130733
|
items: client2.qtiApi.assessmentTests.testParts(qtiTestIdentifier).sections(section.partIdentifier).items(section.sectionIdentifier)
|
|
129998
130734
|
};
|
|
129999
130735
|
}
|
|
130000
|
-
|
|
130001
|
-
|
|
130736
|
+
async findReviewBankSink(client2, integrationId) {
|
|
130737
|
+
const identifier = reviewBankSinkIdentifier(integrationId);
|
|
130738
|
+
try {
|
|
130739
|
+
return await client2.qtiApi.assessmentTests.get(identifier);
|
|
130740
|
+
} catch (error88) {
|
|
130741
|
+
if (isApiError(error88) && error88.statusCode === 404) {
|
|
130742
|
+
return null;
|
|
130743
|
+
}
|
|
130744
|
+
throw error88;
|
|
130745
|
+
}
|
|
130002
130746
|
}
|
|
130003
130747
|
async ensureImmutableQtiResource(get, create) {
|
|
130004
130748
|
try {
|
|
@@ -130039,7 +130783,7 @@ class TimebackAssessmentsService {
|
|
|
130039
130783
|
test: loaded.test,
|
|
130040
130784
|
questions: loaded.questions,
|
|
130041
130785
|
diagnosticRoutingManifest,
|
|
130042
|
-
itemHref: (identifier) =>
|
|
130786
|
+
itemHref: (identifier) => qtiItemHref(client2, identifier)
|
|
130043
130787
|
});
|
|
130044
130788
|
await this.ensureManagedQtiPublication(client2, plan);
|
|
130045
130789
|
return plan;
|
|
@@ -130121,15 +130865,11 @@ class TimebackAssessmentsService {
|
|
|
130121
130865
|
const plan = buildQtiLibraryListPlan(params);
|
|
130122
130866
|
return list(plan.params);
|
|
130123
130867
|
}
|
|
130124
|
-
async validateAssessmentHasQuestions(loaded
|
|
130868
|
+
async validateAssessmentHasQuestions(loaded) {
|
|
130125
130869
|
assertAssessmentHasQuestions(loaded.questions.questions);
|
|
130126
130870
|
for (const { question } of loaded.questions.questions) {
|
|
130127
130871
|
assertPlayableQtiQuestion(question);
|
|
130128
130872
|
}
|
|
130129
|
-
if (reviewGameSlug) {
|
|
130130
|
-
assertQtiTestOwnedByGame(loaded.test, reviewGameSlug);
|
|
130131
|
-
await this.writeReviewMapping(this.requireClient(), loaded.test, loaded.questions);
|
|
130132
|
-
}
|
|
130133
130873
|
}
|
|
130134
130874
|
async prepareDiagnosticAssociationChanges(row, nextPurpose, requested) {
|
|
130135
130875
|
if (row.purpose !== "diagnostic" && nextPurpose === "diagnostic" && !requested) {
|
|
@@ -130158,22 +130898,9 @@ class TimebackAssessmentsService {
|
|
|
130158
130898
|
}
|
|
130159
130899
|
};
|
|
130160
130900
|
}
|
|
130161
|
-
async rebuildReviewMapping(client2, qtiTestIdentifier, scope) {
|
|
130162
|
-
const [test, references] = await Promise.all([
|
|
130163
|
-
client2.qtiApi.assessmentTests.get(qtiTestIdentifier),
|
|
130164
|
-
client2.qtiApi.assessmentTests.getQuestions(qtiTestIdentifier)
|
|
130165
|
-
]);
|
|
130166
|
-
if (scope.kind === "owned") {
|
|
130167
|
-
assertQtiTestOwnedByGame(test, scope.gameSlug);
|
|
130168
|
-
}
|
|
130169
|
-
return this.writeReviewMapping(client2, test, await hydrateQtiTestQuestions(client2, references));
|
|
130170
|
-
}
|
|
130171
|
-
async writeReviewMapping(client2, test, questions) {
|
|
130172
|
-
const prepared = await prepareReviewMappingUpdate(test, questions);
|
|
130173
|
-
await client2.qtiApi.assessmentTests.update(test.identifier, prepared.update);
|
|
130174
|
-
return prepared.summary;
|
|
130175
|
-
}
|
|
130176
130901
|
}
|
|
130902
|
+
var REVIEW_BANK_TITLE = "Review Bank";
|
|
130903
|
+
var REVIEW_BANK_SYNC_ATTEMPTS = 2;
|
|
130177
130904
|
var init_timeback_assessments_service = __esm(async () => {
|
|
130178
130905
|
init_drizzle_orm();
|
|
130179
130906
|
init_helpers_index();
|
|
@@ -192833,7 +193560,7 @@ var listAssessments;
|
|
|
192833
193560
|
var createAssessment;
|
|
192834
193561
|
var attachExistingAssessments;
|
|
192835
193562
|
var updateAssessment;
|
|
192836
|
-
var
|
|
193563
|
+
var synchronizeReviewBank;
|
|
192837
193564
|
var reorderAssessments;
|
|
192838
193565
|
var reorderQuestions;
|
|
192839
193566
|
var removeAssessment;
|
|
@@ -193206,7 +193933,9 @@ var init_timeback_controller = __esm(() => {
|
|
|
193206
193933
|
attemptId,
|
|
193207
193934
|
input: {
|
|
193208
193935
|
submissionId: body2.submissionId,
|
|
193209
|
-
xpAwarded: body2.xpAwarded
|
|
193936
|
+
xpAwarded: body2.xpAwarded,
|
|
193937
|
+
...body2.masteredUnits !== undefined ? { masteredUnits: body2.masteredUnits } : {},
|
|
193938
|
+
...body2.masteredUnitsAbsolute !== undefined ? { masteredUnitsAbsolute: body2.masteredUnitsAbsolute } : {}
|
|
193210
193939
|
},
|
|
193211
193940
|
game: {
|
|
193212
193941
|
appName: body2.appName,
|
|
@@ -193540,13 +194269,13 @@ var init_timeback_controller = __esm(() => {
|
|
|
193540
194269
|
const body2 = await parseRequestBody(ctx.request, UpdateAssessmentRequestSchema);
|
|
193541
194270
|
return ctx.services.timebackAssessments.updateAssessment(integrationId, testIdentifier, body2);
|
|
193542
194271
|
});
|
|
193543
|
-
|
|
193544
|
-
const { gameId, courseId
|
|
193545
|
-
if (!gameId || !courseId
|
|
193546
|
-
throw ApiError.badRequest("Missing gameId
|
|
194272
|
+
synchronizeReviewBank = requireDeveloper(async (ctx) => {
|
|
194273
|
+
const { gameId, courseId } = ctx.params;
|
|
194274
|
+
if (!gameId || !courseId) {
|
|
194275
|
+
throw ApiError.badRequest("Missing gameId or courseId parameter");
|
|
193547
194276
|
}
|
|
193548
194277
|
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
193549
|
-
return ctx.services.timebackAssessments.
|
|
194278
|
+
return ctx.services.timebackAssessments.synchronizeReviewBank(integrationId);
|
|
193550
194279
|
});
|
|
193551
194280
|
reorderAssessments = requireDeveloper(async (ctx) => {
|
|
193552
194281
|
const { gameId, courseId } = ctx.params;
|
|
@@ -193697,7 +194426,7 @@ var init_timeback_controller = __esm(() => {
|
|
|
193697
194426
|
createAssessment,
|
|
193698
194427
|
attachExistingAssessments,
|
|
193699
194428
|
updateAssessment,
|
|
193700
|
-
|
|
194429
|
+
synchronizeReviewBank,
|
|
193701
194430
|
reorderAssessments,
|
|
193702
194431
|
removeAssessment,
|
|
193703
194432
|
reorderQuestions,
|