@playcademy/sandbox 0.8.1-beta.1 → 0.8.1-beta.3
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/cli.js +1132 -493
- package/dist/server.js +1132 -493
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -1127,7 +1127,7 @@ var package_default;
|
|
|
1127
1127
|
var init_package = __esm(() => {
|
|
1128
1128
|
package_default = {
|
|
1129
1129
|
name: "@playcademy/sandbox",
|
|
1130
|
-
version: "0.8.1-beta.
|
|
1130
|
+
version: "0.8.1-beta.3",
|
|
1131
1131
|
description: "Local development server for Playcademy game development",
|
|
1132
1132
|
type: "module",
|
|
1133
1133
|
exports: {
|
|
@@ -1497,6 +1497,9 @@ async function runWithConcurrency(items, concurrency, worker) {
|
|
|
1497
1497
|
}
|
|
1498
1498
|
|
|
1499
1499
|
// ../utils/src/timeback.ts
|
|
1500
|
+
function reviewBankSinkIdentifier(integrationId) {
|
|
1501
|
+
return `playcademy-review-bank.${integrationId}`;
|
|
1502
|
+
}
|
|
1500
1503
|
function playcademySupportedQtiInteractionType(value) {
|
|
1501
1504
|
if (typeof value !== "string") {
|
|
1502
1505
|
return;
|
|
@@ -9480,9 +9483,12 @@ function classifyAssessmentSubmission(attempt, submissionId) {
|
|
|
9480
9483
|
}
|
|
9481
9484
|
return isAssessmentAttemptOpen(attempt) ? "submit" : "reject";
|
|
9482
9485
|
}
|
|
9486
|
+
function isSameAssessmentAward(recorded, input) {
|
|
9487
|
+
return recorded.xpAwarded === input.xpAwarded && recorded.masteredUnits === input.masteredUnits && recorded.masteredUnitsAbsolute === input.masteredUnitsAbsolute;
|
|
9488
|
+
}
|
|
9483
9489
|
function classifyAssessmentFinalization(attempt, input) {
|
|
9484
9490
|
if (isAssessmentAttemptCompleted(attempt)) {
|
|
9485
|
-
return attempt.submissionId === input.submissionId && attempt.
|
|
9491
|
+
return attempt.submissionId === input.submissionId && attempt.award !== undefined && isSameAssessmentAward(attempt.award, input) ? "replay" : "conflict";
|
|
9486
9492
|
}
|
|
9487
9493
|
if (!isAssessmentAttemptAwaitingAward(attempt) || attempt.submissionId !== input.submissionId) {
|
|
9488
9494
|
return "reject";
|
|
@@ -10760,39 +10766,21 @@ function compareNewestCompletedAssessmentAttempt(left, right) {
|
|
|
10760
10766
|
return newestAttempt(left, right, "scoreDate");
|
|
10761
10767
|
}
|
|
10762
10768
|
function compareAssessmentCatalogOrder(left, right) {
|
|
10763
|
-
if (left.sortOrder !== null || right.sortOrder !== null) {
|
|
10764
|
-
if (left.sortOrder === null) {
|
|
10765
|
-
return 1;
|
|
10766
|
-
}
|
|
10767
|
-
if (right.sortOrder === null) {
|
|
10768
|
-
return -1;
|
|
10769
|
-
}
|
|
10770
|
-
const orderDifference = left.sortOrder - right.sortOrder;
|
|
10771
|
-
if (orderDifference) {
|
|
10772
|
-
return orderDifference;
|
|
10773
|
-
}
|
|
10774
|
-
}
|
|
10775
|
-
const leftUpdatedAt = Date.parse(left.updatedAt);
|
|
10776
|
-
const rightUpdatedAt = Date.parse(right.updatedAt);
|
|
10777
|
-
const leftHasValidDate = Number.isFinite(leftUpdatedAt);
|
|
10778
|
-
const rightHasValidDate = Number.isFinite(rightUpdatedAt);
|
|
10779
|
-
if (leftHasValidDate && rightHasValidDate) {
|
|
10780
|
-
const updatedDifference = leftUpdatedAt - rightUpdatedAt;
|
|
10781
|
-
if (updatedDifference) {
|
|
10782
|
-
return updatedDifference;
|
|
10783
|
-
}
|
|
10784
|
-
} else if (leftHasValidDate !== rightHasValidDate) {
|
|
10785
|
-
return leftHasValidDate ? -1 : 1;
|
|
10786
|
-
}
|
|
10787
10769
|
if (left.assessmentKey !== null && right.assessmentKey !== null) {
|
|
10788
|
-
const keyDifference = left.assessmentKey
|
|
10770
|
+
const keyDifference = compareCodePoints(left.assessmentKey, right.assessmentKey);
|
|
10789
10771
|
if (keyDifference) {
|
|
10790
10772
|
return keyDifference;
|
|
10791
10773
|
}
|
|
10792
10774
|
} else if (left.assessmentKey !== null || right.assessmentKey !== null) {
|
|
10793
10775
|
return left.assessmentKey === null ? 1 : -1;
|
|
10794
10776
|
}
|
|
10795
|
-
return left.qtiTestIdentifier
|
|
10777
|
+
return compareCodePoints(left.qtiTestIdentifier, right.qtiTestIdentifier);
|
|
10778
|
+
}
|
|
10779
|
+
function compareCodePoints(left, right) {
|
|
10780
|
+
if (left === right) {
|
|
10781
|
+
return 0;
|
|
10782
|
+
}
|
|
10783
|
+
return left < right ? -1 : 1;
|
|
10796
10784
|
}
|
|
10797
10785
|
function orderRuntimeAssessmentTests(tests) {
|
|
10798
10786
|
return [...tests].toSorted(compareAssessmentCatalogOrder);
|
|
@@ -10963,7 +10951,7 @@ async function reviewBankRevision(input) {
|
|
|
10963
10951
|
})}`);
|
|
10964
10952
|
return `review-bank-v1:${bankRevisionUuid}`;
|
|
10965
10953
|
}
|
|
10966
|
-
async function
|
|
10954
|
+
async function buildReviewBankIndexFromReferences(input) {
|
|
10967
10955
|
return {
|
|
10968
10956
|
...input,
|
|
10969
10957
|
bankRevision: await reviewBankRevision(input)
|
|
@@ -10992,7 +10980,7 @@ async function buildReviewBankIndex(assessment, questions, options = {}) {
|
|
|
10992
10980
|
standards
|
|
10993
10981
|
};
|
|
10994
10982
|
});
|
|
10995
|
-
return
|
|
10983
|
+
return buildReviewBankIndexFromReferences({
|
|
10996
10984
|
bankIdentifier: assessment.identifier,
|
|
10997
10985
|
sourceContentRevision: assessment.contentRevision,
|
|
10998
10986
|
items
|
|
@@ -11097,7 +11085,7 @@ async function reviewBankIndexFromManifest(metadata2, input) {
|
|
|
11097
11085
|
}
|
|
11098
11086
|
}
|
|
11099
11087
|
}
|
|
11100
|
-
const index = await
|
|
11088
|
+
const index = await buildReviewBankIndexFromReferences({
|
|
11101
11089
|
bankIdentifier,
|
|
11102
11090
|
sourceContentRevision,
|
|
11103
11091
|
items: input.membershipItemIdentifiers.map((itemIdentifier) => ({
|
|
@@ -11452,6 +11440,18 @@ function isRoutedDiagnosticAttemptMetadata(value, itemSubmissions, responseVersi
|
|
|
11452
11440
|
function isReviewAttemptMetadata(value) {
|
|
11453
11441
|
return isRecord(value) && typeof value.requestFingerprint === "string" && typeof value.bankRevision === "string" && Array.isArray(value.standards) && value.standards.every(isAssessmentStandardRef) && Number.isInteger(value.candidateItemsPerStandard) && Array.isArray(value.selections) && value.selections.every((selection) => isRecord(selection) && isAssessmentStandardRef(selection.standard) && typeof selection.itemIdentifier === "string") && isReviewFulfillment(value.fulfillment);
|
|
11454
11442
|
}
|
|
11443
|
+
function isOptionalInteger(value) {
|
|
11444
|
+
return value === undefined || Number.isInteger(value);
|
|
11445
|
+
}
|
|
11446
|
+
function isMasteryWriteWarning(value) {
|
|
11447
|
+
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);
|
|
11448
|
+
}
|
|
11449
|
+
function isAssessmentAwardRecord(value) {
|
|
11450
|
+
if (value === undefined) {
|
|
11451
|
+
return true;
|
|
11452
|
+
}
|
|
11453
|
+
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));
|
|
11454
|
+
}
|
|
11455
11455
|
function isMasteryAttemptMetadata(value) {
|
|
11456
11456
|
return isRecord(value) && typeof value.requestFingerprint === "string" && isAssessmentStandardRef(value.standard);
|
|
11457
11457
|
}
|
|
@@ -11476,7 +11476,7 @@ function isPlaycademyAssessmentResultMetadataV1(value) {
|
|
|
11476
11476
|
const review = metadata2.review;
|
|
11477
11477
|
const diagnostic = metadata2.diagnostic;
|
|
11478
11478
|
const routedDiagnostic = metadata2.purpose === "diagnostic" && diagnostic !== undefined && Array.isArray(metadata2.itemSubmissions) && Number.isInteger(metadata2.responseVersion) && isRoutedDiagnosticAttemptMetadata(diagnostic, metadata2.itemSubmissions, metadata2.responseVersion);
|
|
11479
|
-
return metadata2.version === 1 && typeof metadata2.activityId === "string" && isAssessmentPurpose(metadata2.purpose) && typeof metadata2.courseId === "string" && typeof metadata2.integrationId === "string" && typeof metadata2.enrollmentId === "string" && Boolean(selectedTest) && (selectedTest?.assessmentKey === undefined || isNonEmptyString(selectedTest.assessmentKey)) && typeof selectedTest?.identifier === "string" && typeof selectedTest.contentRevision === "string" && Number.isInteger(metadata2.attemptNumber) && Number.isInteger(metadata2.responseVersion) && Boolean(metadata2.responses) && typeof metadata2.responses === "object" && Array.isArray(metadata2.itemSubmissions) && metadata2.itemSubmissions.every(isAssessmentItemSubmission) && typeof metadata2.startedAt === "string" && typeof metadata2.updatedAt === "string" && (metadata2.finalizedAt === undefined || typeof metadata2.finalizedAt === "string") && (metadata2.purpose === "mastery" ? isMasteryAttemptMetadata(mastery) : mastery === undefined) && (metadata2.purpose === "review" ? isReviewAttemptMetadata(review) : review === undefined) && (metadata2.purpose === "diagnostic" ? diagnostic === undefined || routedDiagnostic : diagnostic === undefined) && (metadata2.submissionId === undefined || typeof metadata2.submissionId === "string") && (metadata2.score === undefined || isAssessmentScore(metadata2.score)) && isCompletionMetadata(metadata2.completion, metadata2.purpose, routedDiagnostic);
|
|
11479
|
+
return metadata2.version === 1 && typeof metadata2.activityId === "string" && isAssessmentPurpose(metadata2.purpose) && typeof metadata2.courseId === "string" && typeof metadata2.integrationId === "string" && typeof metadata2.enrollmentId === "string" && Boolean(selectedTest) && (selectedTest?.assessmentKey === undefined || isNonEmptyString(selectedTest.assessmentKey)) && typeof selectedTest?.identifier === "string" && typeof selectedTest.contentRevision === "string" && Number.isInteger(metadata2.attemptNumber) && Number.isInteger(metadata2.responseVersion) && Boolean(metadata2.responses) && typeof metadata2.responses === "object" && Array.isArray(metadata2.itemSubmissions) && metadata2.itemSubmissions.every(isAssessmentItemSubmission) && typeof metadata2.startedAt === "string" && typeof metadata2.updatedAt === "string" && (metadata2.finalizedAt === undefined || typeof metadata2.finalizedAt === "string") && isAssessmentAwardRecord(metadata2.award) && (metadata2.purpose === "mastery" ? isMasteryAttemptMetadata(mastery) : mastery === undefined) && (metadata2.purpose === "review" ? isReviewAttemptMetadata(review) : review === undefined) && (metadata2.purpose === "diagnostic" ? diagnostic === undefined || routedDiagnostic : diagnostic === undefined) && (metadata2.submissionId === undefined || typeof metadata2.submissionId === "string") && (metadata2.score === undefined || isAssessmentScore(metadata2.score)) && isCompletionMetadata(metadata2.completion, metadata2.purpose, routedDiagnostic);
|
|
11480
11480
|
}
|
|
11481
11481
|
function isPlaycademyReviewAssessmentItemResultMetadataV1(value) {
|
|
11482
11482
|
if (!isRecord(value) || !isRecord(value.responses)) {
|
|
@@ -11564,7 +11564,7 @@ function playcademyDiagnosticAssessmentItemResultMetadata(value) {
|
|
|
11564
11564
|
const normalized = metadata2.responses === undefined ? { ...metadata2, responses: {} } : metadata2;
|
|
11565
11565
|
return isPlaycademyDiagnosticAssessmentItemResultMetadataV1(normalized) ? normalized : null;
|
|
11566
11566
|
}
|
|
11567
|
-
var SCRIPT_SCHEMES, DATA_RASTER_IMAGE_PATTERN, QTI_MATHML_ALLOWED_TAGS, metadataSymbol, parser, SUPPORTED_QTI_INTERACTION_TYPES, URL_ATTRIBUTES, IMAGE_URL_ATTRIBUTES, INTERACTION_TYPES, EMPHASIS_TAGS, STRONG_TAGS, CONTENT_SKIPPED_TAGS, BLOCK_CONTENT_KINDS, GAP_MATCH_TOKEN_TAGS, STAGE_GRAPHIC_EXCLUDED_CONTAINERS, BLANK_SENTINEL = "", MARKED_BLANK_PATTERN, MARKED_BLANK_GLOBAL_PATTERN, MATCH_CORRECT_TEMPLATE = "http://www.imsglobal.org/question/qti_v3p0/rptemplates/match_correct", MAP_RESPONSE_TEMPLATE = "https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/map_response", MAP_RESPONSE_TEMPLATES, RESPONSE_PROCESSING_TEMPLATES, VOID_BODY_ELEMENTS, URL_ATTRIBUTES2, IMAGE_URL_ATTRIBUTES2, VOID_CHOICE_ELEMENTS, NUMERIC_COMPARISON_ATTRIBUTES, RESPONSE_CARDINALITIES, RESPONSE_BASE_TYPES, POINT_INTERACTION_TYPES, INDEPENDENT_PROCESSING_TAGS, UUID_PATTERN, COMMON_CORE_MATH_FRAMEWORK_ALIASES, COMMON_CORE_ELA_FRAMEWORK_ALIASES, COMMON_CORE_FRAMEWORK_ALIASES, ASSESSMENT_ATTEMPT_OPEN, ASSESSMENT_ATTEMPT_COMPLETED, ASSESSMENT_ATTEMPT_AWAITING_AWARD, ASSESSMENT_ATTEMPT_SUPERSEDED, ASSESSMENT_RUNTIME_ERROR_STATUS, ROUTING_KEY_MAX_LENGTH = 128, ROUTING_RESULT_KEY_MAX_LENGTH = 256, ROUTING_STAGE_LIMIT = 32, ROUTING_TRACK_LIMIT = 128, ROUTING_NODE_LIMIT = 1000, ROUTING_TERMINAL_LIMIT = 1000, ROUTING_GROUP_LIMIT = 128, ROUTING_PREDICATE_DEPTH_LIMIT = 8, ROUTING_EXPLORATION_STATE_LIMIT = 200000, RoutingKeySchema, RoutingResultKeySchema, DiagnosticTrackOutcomeSchema, DiagnosticActivationPredicateSchema, DiagnosticRoutingTransitionSchema, DiagnosticRoutingManifestV1Schema, GRADE_VALUES, POINT_RESPONSE_PATTERN, REVIEW_SELECTION_POLICY_VERSION = "unseen-first-least-recently-delivered-v1", PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY = "playcademyReviewBank", PLAYCADEMY_REVIEW_BANK_MANIFEST_VERSION = 1, DEFAULT_REVIEW_SELECTION_POLICY, ASSESSMENT_RUNTIME_FIXTURE_BUNDLE_VERSION =
|
|
11567
|
+
var SCRIPT_SCHEMES, DATA_RASTER_IMAGE_PATTERN, QTI_MATHML_ALLOWED_TAGS, metadataSymbol, parser, SUPPORTED_QTI_INTERACTION_TYPES, URL_ATTRIBUTES, IMAGE_URL_ATTRIBUTES, INTERACTION_TYPES, EMPHASIS_TAGS, STRONG_TAGS, CONTENT_SKIPPED_TAGS, BLOCK_CONTENT_KINDS, GAP_MATCH_TOKEN_TAGS, STAGE_GRAPHIC_EXCLUDED_CONTAINERS, BLANK_SENTINEL = "", MARKED_BLANK_PATTERN, MARKED_BLANK_GLOBAL_PATTERN, MATCH_CORRECT_TEMPLATE = "http://www.imsglobal.org/question/qti_v3p0/rptemplates/match_correct", MAP_RESPONSE_TEMPLATE = "https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/map_response", MAP_RESPONSE_TEMPLATES, RESPONSE_PROCESSING_TEMPLATES, VOID_BODY_ELEMENTS, URL_ATTRIBUTES2, IMAGE_URL_ATTRIBUTES2, VOID_CHOICE_ELEMENTS, NUMERIC_COMPARISON_ATTRIBUTES, RESPONSE_CARDINALITIES, RESPONSE_BASE_TYPES, POINT_INTERACTION_TYPES, INDEPENDENT_PROCESSING_TAGS, UUID_PATTERN, COMMON_CORE_MATH_FRAMEWORK_ALIASES, COMMON_CORE_ELA_FRAMEWORK_ALIASES, COMMON_CORE_FRAMEWORK_ALIASES, ASSESSMENT_ATTEMPT_OPEN, ASSESSMENT_ATTEMPT_COMPLETED, ASSESSMENT_ATTEMPT_AWAITING_AWARD, ASSESSMENT_ATTEMPT_SUPERSEDED, ASSESSMENT_RUNTIME_ERROR_STATUS, ROUTING_KEY_MAX_LENGTH = 128, ROUTING_RESULT_KEY_MAX_LENGTH = 256, ROUTING_STAGE_LIMIT = 32, ROUTING_TRACK_LIMIT = 128, ROUTING_NODE_LIMIT = 1000, ROUTING_TERMINAL_LIMIT = 1000, ROUTING_GROUP_LIMIT = 128, ROUTING_PREDICATE_DEPTH_LIMIT = 8, ROUTING_EXPLORATION_STATE_LIMIT = 200000, RoutingKeySchema, RoutingResultKeySchema, DiagnosticTrackOutcomeSchema, DiagnosticActivationPredicateSchema, DiagnosticRoutingTransitionSchema, DiagnosticRoutingManifestV1Schema, GRADE_VALUES, POINT_RESPONSE_PATTERN, REVIEW_SELECTION_POLICY_VERSION = "unseen-first-least-recently-delivered-v1", PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY = "playcademyReviewBank", PLAYCADEMY_REVIEW_BANK_MANIFEST_VERSION = 1, DEFAULT_REVIEW_SELECTION_POLICY, ASSESSMENT_RUNTIME_FIXTURE_BUNDLE_VERSION = 8, RuntimeSubjectSchema, RuntimeGradeSchema, OptionalQueryGradeSchema, AssessmentResponseValueSchema, AssessmentRuntimeIdentitySchema, ResponseKeySchema, StartAssessmentBaseSchema, AssessmentStandardRefSchema, LatestAssessmentFilterBaseSchema, LatestAssessmentFiltersSchema, LatestRuntimeAssessmentQuerySchema, StartAssessmentBodySchema, SaveAssessmentBodySchema, SubmitAssessmentItemBodySchema, SubmitAssessmentBodySchema, FinalizeAssessmentBodySchema, StartRuntimeAssessmentRequestSchema, SaveRuntimeAssessmentRequestSchema, SubmitAssessmentItemRuntimeRequestSchema, SubmitRuntimeAssessmentRequestSchema, FinalizeRuntimeAssessmentRequestSchema;
|
|
11568
11568
|
var init_assessment_runtime2 = __esm(() => {
|
|
11569
11569
|
init_timeback3();
|
|
11570
11570
|
init_timeback3();
|
|
@@ -11914,7 +11914,12 @@ var init_assessment_runtime2 = __esm(() => {
|
|
|
11914
11914
|
});
|
|
11915
11915
|
FinalizeAssessmentBodySchema = exports_external.object({
|
|
11916
11916
|
submissionId: exports_external.string().trim().min(1),
|
|
11917
|
-
xpAwarded: exports_external.number().finite().nonnegative()
|
|
11917
|
+
xpAwarded: exports_external.number().finite().nonnegative(),
|
|
11918
|
+
masteredUnits: exports_external.number().finite().optional(),
|
|
11919
|
+
masteredUnitsAbsolute: exports_external.number().finite().int().optional()
|
|
11920
|
+
}).refine((body2) => body2.masteredUnits === undefined || body2.masteredUnitsAbsolute === undefined, {
|
|
11921
|
+
message: "Provide either masteredUnits or masteredUnitsAbsolute, not both",
|
|
11922
|
+
path: ["masteredUnits"]
|
|
11918
11923
|
});
|
|
11919
11924
|
StartRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.and(StartAssessmentBodySchema);
|
|
11920
11925
|
SaveRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.extend(SaveAssessmentBodySchema.shape);
|
|
@@ -11923,10 +11928,10 @@ var init_assessment_runtime2 = __esm(() => {
|
|
|
11923
11928
|
appName: exports_external.string().trim().min(1),
|
|
11924
11929
|
sensorUrl: exports_external.string().url()
|
|
11925
11930
|
});
|
|
11926
|
-
FinalizeRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.extend(
|
|
11931
|
+
FinalizeRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.extend({
|
|
11927
11932
|
appName: exports_external.string().trim().min(1),
|
|
11928
11933
|
sensorUrl: exports_external.string().url()
|
|
11929
|
-
});
|
|
11934
|
+
}).and(FinalizeAssessmentBodySchema);
|
|
11930
11935
|
});
|
|
11931
11936
|
|
|
11932
11937
|
// ../api-core/src/errors/assessment-runtime.error.ts
|
|
@@ -47109,7 +47114,16 @@ function requireMasteryStandard(input, context2) {
|
|
|
47109
47114
|
});
|
|
47110
47115
|
}
|
|
47111
47116
|
}
|
|
47112
|
-
|
|
47117
|
+
function rejectSystemManagedReview(input, context2) {
|
|
47118
|
+
if (input.purpose === "review") {
|
|
47119
|
+
context2.addIssue({
|
|
47120
|
+
code: "custom",
|
|
47121
|
+
path: ["purpose"],
|
|
47122
|
+
message: "The review bank is system-managed and cannot be managed manually"
|
|
47123
|
+
});
|
|
47124
|
+
}
|
|
47125
|
+
}
|
|
47126
|
+
var TimebackGradeSchema, TimebackSubjectSchema, CourseGoalsSchema, UpdateGameTimebackIntegrationRequestSchema, CreateGameTimebackIntegrationRequestSchema, TimebackActivityDataSchema, EndActivityRequestSchema, GameRunMetricsSchema, GameCourseMetricsSchema, GameMetricsResponseSchema, AdvanceCourseRequestSchema, UnenrollCourseRequestSchema, HeartbeatRequestSchema, PopulateStudentRequestSchema, DerivedPlatformCourseConfigSchema, TimebackBaseConfigSchema, PlatformTimebackSetupRequestSchema, AdminTimebackMutationBaseSchema, AdminAttributionDateSchema, ADMIN_GRANT_XP_MIN = -1e5, ADMIN_GRANT_XP_MAX = 1e5, ADMIN_GRANT_XP_AMOUNT_RANGE_MESSAGE, GrantTimebackXpRequestSchema, AdjustTimebackTimeRequestSchema, AdjustTimebackMasteryRequestSchema, ReconcileMasteryForConfigChangeSchema, EnrollStudentRequestSchema, UnenrollStudentRequestSchema, ReactivateEnrollmentRequestSchema, VerifyTimebackMetricDiscrepancyRequestSchema, AssessmentPurposeSchema, AssessmentStatusSchema, AssessmentKeySchema, DiagnosticKeySchema, DiagnosticRoutingManifestSchema, DiagnosticAssessmentDefinitionInputSchema, AssessmentStandardRefSchema2, CreateAssessmentRequestSchema, UpdateAssessmentRequestSchema, CopyAssessmentRequestSchema, AssessmentAssociationImportEntrySchema, AttachExistingAssessmentsRequestSchema, ReorderQuestionsRequestSchema;
|
|
47113
47127
|
var init_schemas4 = __esm(() => {
|
|
47114
47128
|
init_esm();
|
|
47115
47129
|
init_src();
|
|
@@ -47370,7 +47384,10 @@ var init_schemas4 = __esm(() => {
|
|
|
47370
47384
|
title: exports_external.string().min(1, "Assessment title is required"),
|
|
47371
47385
|
purpose: AssessmentPurposeSchema,
|
|
47372
47386
|
standard: AssessmentStandardRefSchema2.optional()
|
|
47373
|
-
}).superRefine(
|
|
47387
|
+
}).superRefine((input, context2) => {
|
|
47388
|
+
requireMasteryStandard(input, context2);
|
|
47389
|
+
rejectSystemManagedReview(input, context2);
|
|
47390
|
+
});
|
|
47374
47391
|
UpdateAssessmentRequestSchema = exports_external.object({
|
|
47375
47392
|
title: exports_external.string().trim().min(1, "Assessment title is required").optional(),
|
|
47376
47393
|
purpose: AssessmentPurposeSchema.optional(),
|
|
@@ -47379,21 +47396,24 @@ var init_schemas4 = __esm(() => {
|
|
|
47379
47396
|
status: AssessmentStatusSchema.optional()
|
|
47380
47397
|
}).refine((input) => input.title !== undefined || input.purpose !== undefined || input.standard !== undefined || input.diagnostic !== undefined || input.status !== undefined, {
|
|
47381
47398
|
message: "Title, purpose, standard, diagnostic, or status is required"
|
|
47382
|
-
});
|
|
47399
|
+
}).superRefine(rejectSystemManagedReview);
|
|
47383
47400
|
CopyAssessmentRequestSchema = exports_external.object({
|
|
47384
47401
|
assessmentKey: AssessmentKeySchema,
|
|
47385
47402
|
testIdentifier: exports_external.string().trim().min(1, "Assessment identifier is required"),
|
|
47386
47403
|
purpose: AssessmentPurposeSchema,
|
|
47387
47404
|
standard: AssessmentStandardRefSchema2.optional()
|
|
47388
|
-
}).superRefine(
|
|
47405
|
+
}).superRefine((input, context2) => {
|
|
47406
|
+
requireMasteryStandard(input, context2);
|
|
47407
|
+
rejectSystemManagedReview(input, context2);
|
|
47408
|
+
});
|
|
47389
47409
|
AssessmentAssociationImportEntrySchema = exports_external.object({
|
|
47390
47410
|
assessmentKey: AssessmentKeySchema,
|
|
47391
47411
|
qtiTestIdentifier: exports_external.string().trim().min(1, "Assessment identifier is required"),
|
|
47392
47412
|
purpose: AssessmentPurposeSchema,
|
|
47393
|
-
standard: AssessmentStandardRefSchema2.optional()
|
|
47394
|
-
sortOrder: exports_external.number().int().nonnegative().nullable()
|
|
47413
|
+
standard: AssessmentStandardRefSchema2.optional()
|
|
47395
47414
|
}).superRefine((input, context2) => {
|
|
47396
47415
|
requireMasteryStandard(input, context2);
|
|
47416
|
+
rejectSystemManagedReview(input, context2);
|
|
47397
47417
|
if (input.purpose !== "mastery" && input.standard !== undefined) {
|
|
47398
47418
|
context2.addIssue({
|
|
47399
47419
|
code: "custom",
|
|
@@ -47440,10 +47460,6 @@ var init_schemas4 = __esm(() => {
|
|
|
47440
47460
|
seenKeys.add(assessment.assessmentKey);
|
|
47441
47461
|
});
|
|
47442
47462
|
});
|
|
47443
|
-
ReorderAssessmentsRequestSchema = exports_external.object({
|
|
47444
|
-
purpose: AssessmentPurposeSchema,
|
|
47445
|
-
testIdentifiers: exports_external.array(exports_external.string().trim().min(1, "Assessment identifier is required")).min(1, "At least one assessment is required")
|
|
47446
|
-
});
|
|
47447
47463
|
ReorderQuestionsRequestSchema = exports_external.object({
|
|
47448
47464
|
items: exports_external.array(exports_external.object({
|
|
47449
47465
|
identifier: exports_external.string().min(1),
|
|
@@ -90872,6 +90888,7 @@ function buildAssessmentCompletionEvent(input) {
|
|
|
90872
90888
|
totalQuestions: input.totalQuestions,
|
|
90873
90889
|
correctQuestions: input.correctQuestions,
|
|
90874
90890
|
xpEarned: input.xpAwarded,
|
|
90891
|
+
...input.masteredUnits ? { masteredUnits: input.masteredUnits } : {},
|
|
90875
90892
|
attemptNumber: input.attemptNumber,
|
|
90876
90893
|
process: false,
|
|
90877
90894
|
subject: input.subject,
|
|
@@ -90880,7 +90897,10 @@ function buildAssessmentCompletionEvent(input) {
|
|
|
90880
90897
|
eventId: input.eventId,
|
|
90881
90898
|
eventTime: input.submittedAt,
|
|
90882
90899
|
runId: input.attemptId,
|
|
90883
|
-
generatedExtensions:
|
|
90900
|
+
generatedExtensions: {
|
|
90901
|
+
...metadata2,
|
|
90902
|
+
...input.pctCompleteApp !== undefined ? { pctCompleteApp: input.pctCompleteApp } : {}
|
|
90903
|
+
},
|
|
90884
90904
|
eventExtensions: {
|
|
90885
90905
|
playcademy: {
|
|
90886
90906
|
assessmentPurpose: input.purpose,
|
|
@@ -90934,6 +90954,85 @@ function deriveSourcedIds(courseId) {
|
|
|
90934
90954
|
componentResource: `${courseId}-cr`
|
|
90935
90955
|
};
|
|
90936
90956
|
}
|
|
90957
|
+
function hasMasteryAwardRequest(request) {
|
|
90958
|
+
return request.masteredUnits !== undefined || request.masteredUnitsAbsolute !== undefined;
|
|
90959
|
+
}
|
|
90960
|
+
|
|
90961
|
+
class ActivityMasteryAward {
|
|
90962
|
+
mastery;
|
|
90963
|
+
events;
|
|
90964
|
+
constructor(mastery, events) {
|
|
90965
|
+
this.mastery = mastery;
|
|
90966
|
+
this.events = events;
|
|
90967
|
+
}
|
|
90968
|
+
async evaluate(input) {
|
|
90969
|
+
const ids = deriveSourcedIds(input.courseId);
|
|
90970
|
+
const progress = await this.mastery.checkProgress({
|
|
90971
|
+
studentId: input.studentId,
|
|
90972
|
+
courseId: input.courseId,
|
|
90973
|
+
resourceId: ids.resource,
|
|
90974
|
+
masteredUnits: input.masteredUnits ?? 0,
|
|
90975
|
+
masteredUnitsAbsolute: input.masteredUnitsAbsolute
|
|
90976
|
+
});
|
|
90977
|
+
if (!progress) {
|
|
90978
|
+
return {
|
|
90979
|
+
masteredUnitsApplied: input.masteredUnits ?? 0,
|
|
90980
|
+
masteryAchieved: false,
|
|
90981
|
+
masteryRevoked: false
|
|
90982
|
+
};
|
|
90983
|
+
}
|
|
90984
|
+
return {
|
|
90985
|
+
masteredUnitsApplied: progress.effectiveDelta,
|
|
90986
|
+
pctCompleteApp: progress.pctCompleteApp,
|
|
90987
|
+
masteryAchieved: progress.masteryAchieved,
|
|
90988
|
+
masteryRevoked: progress.masteryRevoked,
|
|
90989
|
+
...progress.writeWarning ? { warnings: [progress.writeWarning] } : {}
|
|
90990
|
+
};
|
|
90991
|
+
}
|
|
90992
|
+
async settle(evaluation, input) {
|
|
90993
|
+
let completionEntryWritten = false;
|
|
90994
|
+
if (evaluation.masteryAchieved) {
|
|
90995
|
+
setAttributes({ "app.timeback.mastery_achieved": true });
|
|
90996
|
+
completionEntryWritten = await this.mastery.createCompletionEntry(input.studentId, input.courseId, input.classId, input.appName);
|
|
90997
|
+
await this.emitCourseCompletionHistoryEvent(input);
|
|
90998
|
+
}
|
|
90999
|
+
if (evaluation.masteryRevoked) {
|
|
91000
|
+
await this.mastery.revokeCompletionEntry(input.studentId, input.courseId, input.classId, input.appName);
|
|
91001
|
+
}
|
|
91002
|
+
return { completionEntryWritten };
|
|
91003
|
+
}
|
|
91004
|
+
async emitCourseCompletionHistoryEvent(input) {
|
|
91005
|
+
const ids = deriveSourcedIds(input.courseId);
|
|
91006
|
+
await this.events.emitActivityEvent({
|
|
91007
|
+
eventId: input.completionHistoryEventId,
|
|
91008
|
+
studentId: input.studentId,
|
|
91009
|
+
studentEmail: input.studentEmail,
|
|
91010
|
+
gameId: input.gameId,
|
|
91011
|
+
activityId: input.activityId,
|
|
91012
|
+
activityName: "Course completed",
|
|
91013
|
+
courseId: ids.course,
|
|
91014
|
+
courseName: input.courseName,
|
|
91015
|
+
subject: input.subject,
|
|
91016
|
+
appName: input.appName,
|
|
91017
|
+
sensorUrl: input.sensorUrl,
|
|
91018
|
+
process: false,
|
|
91019
|
+
includeAttempt: false,
|
|
91020
|
+
eventExtensions: {
|
|
91021
|
+
playcademy: {
|
|
91022
|
+
eventKind: "course-completed",
|
|
91023
|
+
source: "gameplay"
|
|
91024
|
+
}
|
|
91025
|
+
},
|
|
91026
|
+
generatedExtensions: {
|
|
91027
|
+
playcademy: {
|
|
91028
|
+
eventKind: "course-completed",
|
|
91029
|
+
source: "gameplay",
|
|
91030
|
+
activityId: input.activityId
|
|
91031
|
+
}
|
|
91032
|
+
}
|
|
91033
|
+
}).catch(catchEvent("timeback.caliper_completion_event_failed"));
|
|
91034
|
+
}
|
|
91035
|
+
}
|
|
90937
91036
|
function validateProgressData(progressData) {
|
|
90938
91037
|
if (!progressData.subject) {
|
|
90939
91038
|
throw new ConfigurationError("subject", "Subject is required for Caliper events. Provide it in progressData.subject");
|
|
@@ -90949,12 +91048,12 @@ function validateProgressData(progressData) {
|
|
|
90949
91048
|
class ActivityRecord {
|
|
90950
91049
|
core;
|
|
90951
91050
|
students;
|
|
90952
|
-
|
|
91051
|
+
masteryAward;
|
|
90953
91052
|
events;
|
|
90954
|
-
constructor(core3, students,
|
|
91053
|
+
constructor(core3, students, masteryAward, events) {
|
|
90955
91054
|
this.core = core3;
|
|
90956
91055
|
this.students = students;
|
|
90957
|
-
this.
|
|
91056
|
+
this.masteryAward = masteryAward;
|
|
90958
91057
|
this.events = events;
|
|
90959
91058
|
}
|
|
90960
91059
|
async record(courseId, studentIdentifier, progressData) {
|
|
@@ -90980,52 +91079,32 @@ class ActivityRecord {
|
|
|
90980
91079
|
throw error88;
|
|
90981
91080
|
}
|
|
90982
91081
|
const legacyLineItemId = `${ids.course}-${activityId}-assessment`;
|
|
90983
|
-
const [currentAttemptNumber,
|
|
91082
|
+
const [currentAttemptNumber, mastery] = await Promise.all([
|
|
90984
91083
|
this.resolveAttemptNumber(attemptNumber, studentId, caliperLineItemId, legacyLineItemId),
|
|
90985
|
-
this.
|
|
91084
|
+
this.masteryAward.evaluate({
|
|
90986
91085
|
studentId,
|
|
90987
91086
|
courseId,
|
|
90988
|
-
|
|
90989
|
-
masteredUnits: progressData.masteredUnits ?? 0,
|
|
91087
|
+
masteredUnits: progressData.masteredUnits,
|
|
90990
91088
|
masteredUnitsAbsolute: progressData.masteredUnitsAbsolute
|
|
90991
91089
|
})
|
|
90992
91090
|
]);
|
|
90993
91091
|
setAttributes({ "app.timeback.attempt_number": currentAttemptNumber });
|
|
90994
|
-
|
|
90995
|
-
const
|
|
90996
|
-
|
|
90997
|
-
|
|
90998
|
-
|
|
90999
|
-
|
|
91000
|
-
|
|
91001
|
-
|
|
91002
|
-
|
|
91003
|
-
|
|
91004
|
-
|
|
91005
|
-
|
|
91006
|
-
|
|
91007
|
-
|
|
91008
|
-
|
|
91009
|
-
|
|
91010
|
-
}
|
|
91011
|
-
if (masteryAchieved) {
|
|
91012
|
-
completionEntryWritten = await this.mastery.createCompletionEntry(studentId, courseId, progressData.classId, progressData.appName);
|
|
91013
|
-
await this.emitCourseCompletionHistoryEvent({
|
|
91014
|
-
studentId,
|
|
91015
|
-
studentEmail,
|
|
91016
|
-
gameId: progressData.gameId,
|
|
91017
|
-
activityId,
|
|
91018
|
-
courseId: ids.course,
|
|
91019
|
-
courseName,
|
|
91020
|
-
subject: progressData.subject,
|
|
91021
|
-
appName: progressData.appName,
|
|
91022
|
-
sensorUrl: progressData.sensorUrl,
|
|
91023
|
-
eventId: progressData.completionHistoryEventId
|
|
91024
|
-
});
|
|
91025
|
-
}
|
|
91026
|
-
if (masteryProgress?.masteryRevoked) {
|
|
91027
|
-
await this.mastery.revokeCompletionEntry(studentId, courseId, progressData.classId, progressData.appName);
|
|
91028
|
-
}
|
|
91092
|
+
const effectiveMasteredUnits = mastery.masteredUnitsApplied;
|
|
91093
|
+
const { pctCompleteApp, warnings } = mastery;
|
|
91094
|
+
const extensions = pctCompleteApp !== undefined ? { ...progressData.extensions, pctCompleteApp } : progressData.extensions;
|
|
91095
|
+
const { completionEntryWritten } = await this.masteryAward.settle(mastery, {
|
|
91096
|
+
studentId,
|
|
91097
|
+
studentEmail,
|
|
91098
|
+
courseId,
|
|
91099
|
+
classId: progressData.classId,
|
|
91100
|
+
gameId: progressData.gameId,
|
|
91101
|
+
activityId,
|
|
91102
|
+
courseName,
|
|
91103
|
+
subject: progressData.subject,
|
|
91104
|
+
appName: progressData.appName,
|
|
91105
|
+
sensorUrl: progressData.sensorUrl,
|
|
91106
|
+
completionHistoryEventId: progressData.completionHistoryEventId
|
|
91107
|
+
});
|
|
91029
91108
|
try {
|
|
91030
91109
|
await this.events.emitActivityEvent({
|
|
91031
91110
|
studentId,
|
|
@@ -91046,7 +91125,7 @@ class ActivityRecord {
|
|
|
91046
91125
|
appName: progressData.appName,
|
|
91047
91126
|
sensorUrl: progressData.sensorUrl,
|
|
91048
91127
|
eventId: progressData.eventId,
|
|
91049
|
-
extensions
|
|
91128
|
+
extensions,
|
|
91050
91129
|
...progressData.runId ? { runId: progressData.runId } : {}
|
|
91051
91130
|
}).catch((error88) => {
|
|
91052
91131
|
setAttributes({ "app.timeback.caliper_emit_failed": true });
|
|
@@ -91067,36 +91146,6 @@ class ActivityRecord {
|
|
|
91067
91146
|
...warnings ? { warnings } : {}
|
|
91068
91147
|
};
|
|
91069
91148
|
}
|
|
91070
|
-
async emitCourseCompletionHistoryEvent(data) {
|
|
91071
|
-
await this.events.emitActivityEvent({
|
|
91072
|
-
eventId: data.eventId,
|
|
91073
|
-
studentId: data.studentId,
|
|
91074
|
-
studentEmail: data.studentEmail,
|
|
91075
|
-
gameId: data.gameId,
|
|
91076
|
-
activityId: data.activityId,
|
|
91077
|
-
activityName: "Course completed",
|
|
91078
|
-
courseId: data.courseId,
|
|
91079
|
-
courseName: data.courseName,
|
|
91080
|
-
subject: data.subject,
|
|
91081
|
-
appName: data.appName,
|
|
91082
|
-
sensorUrl: data.sensorUrl,
|
|
91083
|
-
process: false,
|
|
91084
|
-
includeAttempt: false,
|
|
91085
|
-
eventExtensions: {
|
|
91086
|
-
playcademy: {
|
|
91087
|
-
eventKind: "course-completed",
|
|
91088
|
-
source: "gameplay"
|
|
91089
|
-
}
|
|
91090
|
-
},
|
|
91091
|
-
generatedExtensions: {
|
|
91092
|
-
playcademy: {
|
|
91093
|
-
eventKind: "course-completed",
|
|
91094
|
-
source: "gameplay",
|
|
91095
|
-
activityId: data.activityId
|
|
91096
|
-
}
|
|
91097
|
-
}
|
|
91098
|
-
}).catch(catchEvent("timeback.caliper_completion_event_failed"));
|
|
91099
|
-
}
|
|
91100
91149
|
async resolveAttemptNumber(providedAttemptNumber, studentId, caliperLineItemId, legacyLineItemId) {
|
|
91101
91150
|
if (providedAttemptNumber) {
|
|
91102
91151
|
setAttributes({ "app.timeback.attempt_source": "provided" });
|
|
@@ -91494,9 +91543,12 @@ class ActivityEvents {
|
|
|
91494
91543
|
}
|
|
91495
91544
|
}
|
|
91496
91545
|
function createActivityNamespace(core3, deps) {
|
|
91497
|
-
const
|
|
91546
|
+
const masteryAward = new ActivityMasteryAward(deps.mastery, deps.events);
|
|
91547
|
+
const record3 = new ActivityRecord(core3, deps.students, masteryAward, deps.events);
|
|
91498
91548
|
const session2 = new ActivitySession(deps.students, deps.events);
|
|
91499
91549
|
return {
|
|
91550
|
+
evaluateMasteryAward: (input) => masteryAward.evaluate(input),
|
|
91551
|
+
settleMasteryAward: (evaluation, input) => masteryAward.settle(evaluation, input),
|
|
91500
91552
|
record: (courseId, studentIdentifier, progressData) => record3.record(courseId, studentIdentifier, progressData),
|
|
91501
91553
|
session: (courseId, studentIdentifier, sessionData) => session2.record(courseId, studentIdentifier, sessionData),
|
|
91502
91554
|
listEvents: (params) => deps.events.listEvents(params),
|
|
@@ -92780,8 +92832,9 @@ class TimebackClient2 {
|
|
|
92780
92832
|
this.core.caches.clearAll();
|
|
92781
92833
|
}
|
|
92782
92834
|
}
|
|
92783
|
-
var TimebackError, TimebackAuthenticationError, StudentNotFoundError, ConfigurationError, UUID_REGEX2, ONEROSTER_PATHS, TIMEBACK_API_URLS, QTI_API_URL = "https://qti.alpha-1edtech.ai/api", TIMEBACK_AUTH_URLS, CALIPER_API_URLS, ENV_VARS, ONEROSTER_STATUS, SCORE_STATUS, CACHE_DEFAULTS, PLAYCADEMY_DEFAULTS, RESOURCE_DEFAULTS, DEFAULT_TIMEOUT = 30000, SUBJECT_VALUES2, GRADE_VALUES3, MASTERY_WRITE_CAPPED_WARNING_CODE = "MASTERY_WRITE_CAPPED", EmailSchema;
|
|
92835
|
+
var TimebackError, TimebackAuthenticationError, StudentNotFoundError, ConfigurationError, UUID_REGEX2, ONEROSTER_PATHS, NO_MASTERY_AWARD, TIMEBACK_API_URLS, QTI_API_URL = "https://qti.alpha-1edtech.ai/api", TIMEBACK_AUTH_URLS, CALIPER_API_URLS, ENV_VARS, ONEROSTER_STATUS, SCORE_STATUS, CACHE_DEFAULTS, PLAYCADEMY_DEFAULTS, RESOURCE_DEFAULTS, DEFAULT_TIMEOUT = 30000, SUBJECT_VALUES2, GRADE_VALUES3, MASTERY_WRITE_CAPPED_WARNING_CODE = "MASTERY_WRITE_CAPPED", EmailSchema;
|
|
92784
92836
|
var init_dist5 = __esm(async () => {
|
|
92837
|
+
init_spans();
|
|
92785
92838
|
init_spans();
|
|
92786
92839
|
init_src();
|
|
92787
92840
|
init_spans();
|
|
@@ -92842,6 +92895,11 @@ var init_dist5 = __esm(async () => {
|
|
|
92842
92895
|
courses: "/ims/oneroster/rostering/v1p2/courses",
|
|
92843
92896
|
componentResources: "/ims/oneroster/rostering/v1p2/courses/component-resources"
|
|
92844
92897
|
};
|
|
92898
|
+
NO_MASTERY_AWARD = {
|
|
92899
|
+
masteredUnitsApplied: 0,
|
|
92900
|
+
masteryAchieved: false,
|
|
92901
|
+
masteryRevoked: false
|
|
92902
|
+
};
|
|
92845
92903
|
TIMEBACK_API_URLS = {
|
|
92846
92904
|
production: "https://api.alpha-1edtech.ai",
|
|
92847
92905
|
staging: "https://api.staging.alpha-1edtech.com"
|
|
@@ -95607,6 +95665,62 @@ function stringField2(value) {
|
|
|
95607
95665
|
function firstStringField2(...values) {
|
|
95608
95666
|
return values.map(stringField2).find(Boolean) ?? "";
|
|
95609
95667
|
}
|
|
95668
|
+
function normalizedWhitespace2(value) {
|
|
95669
|
+
return value.normalize("NFKC").trim().replace(/\s+/g, " ");
|
|
95670
|
+
}
|
|
95671
|
+
function normalizedIdentityCase2(value) {
|
|
95672
|
+
return value.toLocaleUpperCase("en-US");
|
|
95673
|
+
}
|
|
95674
|
+
function frameworkAliasKey2(value) {
|
|
95675
|
+
return value.normalize("NFKC").toLocaleLowerCase("en-US").replace(/[^a-z0-9]+/g, "");
|
|
95676
|
+
}
|
|
95677
|
+
function isCommonCoreMathIdentifier2(identifier) {
|
|
95678
|
+
const canonical = normalizedIdentityCase2(normalizedWhitespace2(identifier));
|
|
95679
|
+
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);
|
|
95680
|
+
}
|
|
95681
|
+
function isCommonCoreElaIdentifier2(identifier) {
|
|
95682
|
+
const canonical = normalizedIdentityCase2(normalizedWhitespace2(identifier));
|
|
95683
|
+
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);
|
|
95684
|
+
}
|
|
95685
|
+
function canonicalFramework2(authoredFramework, identifier) {
|
|
95686
|
+
const aliasKey = frameworkAliasKey2(authoredFramework);
|
|
95687
|
+
if (COMMON_CORE_MATH_FRAMEWORK_ALIASES2.has(aliasKey) || COMMON_CORE_FRAMEWORK_ALIASES2.has(aliasKey) && isCommonCoreMathIdentifier2(identifier)) {
|
|
95688
|
+
return "CCSS.Math";
|
|
95689
|
+
}
|
|
95690
|
+
if (COMMON_CORE_ELA_FRAMEWORK_ALIASES2.has(aliasKey) || COMMON_CORE_FRAMEWORK_ALIASES2.has(aliasKey) && isCommonCoreElaIdentifier2(identifier)) {
|
|
95691
|
+
return "CCSS.ELA-Literacy";
|
|
95692
|
+
}
|
|
95693
|
+
if (COMMON_CORE_FRAMEWORK_ALIASES2.has(aliasKey)) {
|
|
95694
|
+
return "CCSS";
|
|
95695
|
+
}
|
|
95696
|
+
return normalizedIdentityCase2(authoredFramework);
|
|
95697
|
+
}
|
|
95698
|
+
function canonicalIdentifier2(framework, authoredIdentifier) {
|
|
95699
|
+
const identifier = normalizedIdentityCase2(normalizedWhitespace2(authoredIdentifier));
|
|
95700
|
+
if (framework === "CCSS.Math") {
|
|
95701
|
+
return identifier.replace(/^CCSS\.MATH\.CONTENT\./, "").replace(/^CCSS\.MATH\.PRACTICE\./, "").replace(/^CCSS\.MATH\./, "");
|
|
95702
|
+
}
|
|
95703
|
+
if (framework === "CCSS.ELA-Literacy") {
|
|
95704
|
+
return identifier.replace(/^CCSS\.ELA-LITERACY\./, "");
|
|
95705
|
+
}
|
|
95706
|
+
return identifier;
|
|
95707
|
+
}
|
|
95708
|
+
function canonicalAssessmentStandardRef2(standard) {
|
|
95709
|
+
const authoredFramework = normalizedWhitespace2(standard.framework);
|
|
95710
|
+
const framework = canonicalFramework2(authoredFramework, standard.identifier);
|
|
95711
|
+
const identifier = canonicalIdentifier2(framework, standard.identifier);
|
|
95712
|
+
return {
|
|
95713
|
+
framework,
|
|
95714
|
+
identifier
|
|
95715
|
+
};
|
|
95716
|
+
}
|
|
95717
|
+
function assessmentStandardRefKey2(standard) {
|
|
95718
|
+
const canonical = canonicalAssessmentStandardRef2(standard);
|
|
95719
|
+
return JSON.stringify([
|
|
95720
|
+
normalizedIdentityCase2(canonical.framework),
|
|
95721
|
+
normalizedIdentityCase2(canonical.identifier)
|
|
95722
|
+
]);
|
|
95723
|
+
}
|
|
95610
95724
|
function dedupeStandards2(standards) {
|
|
95611
95725
|
const deduped = new Map;
|
|
95612
95726
|
for (const standard of standards) {
|
|
@@ -98340,6 +98454,22 @@ var init_locks = __esm(() => {
|
|
|
98340
98454
|
});
|
|
98341
98455
|
|
|
98342
98456
|
// ../api-core/src/utils/assessment-runtime-lock.util.ts
|
|
98457
|
+
async function crossAssessmentAttemptLockBarrier(lock, db2, attemptId) {
|
|
98458
|
+
const maxAttempts = 5;
|
|
98459
|
+
for (let attempt = 1;attempt <= maxAttempts; attempt += 1) {
|
|
98460
|
+
try {
|
|
98461
|
+
await lock(db2, [assessmentRuntimeAttemptLockKey(attemptId)], async () => {
|
|
98462
|
+
return;
|
|
98463
|
+
});
|
|
98464
|
+
return;
|
|
98465
|
+
} catch (error88) {
|
|
98466
|
+
if (!(error88 instanceof ServiceUnavailableError) || attempt === maxAttempts) {
|
|
98467
|
+
throw error88;
|
|
98468
|
+
}
|
|
98469
|
+
await sleep(25);
|
|
98470
|
+
}
|
|
98471
|
+
}
|
|
98472
|
+
}
|
|
98343
98473
|
function normalizeKeys(keys) {
|
|
98344
98474
|
return [...new Set(keys)].toSorted();
|
|
98345
98475
|
}
|
|
@@ -99353,6 +99483,97 @@ var init_timeback_qti_authoring_util = __esm(() => {
|
|
|
99353
99483
|
});
|
|
99354
99484
|
|
|
99355
99485
|
// ../api-core/src/utils/timeback-assessment-runtime.util.ts
|
|
99486
|
+
function assessmentPreparationContentionError(expectedResponseVersion, responseVersion, kind) {
|
|
99487
|
+
if (expectedResponseVersion === responseVersion) {
|
|
99488
|
+
return new ServiceUnavailableError(`The ${kind} response state changed repeatedly while this item was being prepared. Try again shortly.`, {
|
|
99489
|
+
retryable: true,
|
|
99490
|
+
reason: "PREPARATION_CONTENTION",
|
|
99491
|
+
expectedResponseVersion,
|
|
99492
|
+
responseVersion
|
|
99493
|
+
});
|
|
99494
|
+
}
|
|
99495
|
+
return AssessmentRuntimeError.from(assessmentResponseVersionConflict(expectedResponseVersion, responseVersion));
|
|
99496
|
+
}
|
|
99497
|
+
function assessmentTrackableCourse(integration) {
|
|
99498
|
+
if (!isTimebackGrade2(integration.grade) || !isTimebackSubject(integration.subject)) {
|
|
99499
|
+
return null;
|
|
99500
|
+
}
|
|
99501
|
+
return { grade: integration.grade, subject: integration.subject };
|
|
99502
|
+
}
|
|
99503
|
+
function buildAssessmentActivityData(input) {
|
|
99504
|
+
if (!input.course) {
|
|
99505
|
+
return;
|
|
99506
|
+
}
|
|
99507
|
+
return {
|
|
99508
|
+
activityId: input.activityId,
|
|
99509
|
+
activityName: input.activityName,
|
|
99510
|
+
...input.course,
|
|
99511
|
+
courseId: input.courseId
|
|
99512
|
+
};
|
|
99513
|
+
}
|
|
99514
|
+
function buildDiagnosticAssessmentSnapshot(input) {
|
|
99515
|
+
return {
|
|
99516
|
+
attemptId: input.attemptId,
|
|
99517
|
+
responseVersion: input.metadata.responseVersion,
|
|
99518
|
+
status: "in_progress",
|
|
99519
|
+
flow: "platform-routed-item-submit",
|
|
99520
|
+
...input.activityData ? { activityData: input.activityData } : {},
|
|
99521
|
+
assessment: assessmentPresentationForAttempt(input.assessment, input.attemptId),
|
|
99522
|
+
selection: {
|
|
99523
|
+
kind: "platform-routed-diagnostic",
|
|
99524
|
+
purpose: "diagnostic",
|
|
99525
|
+
definitionId: input.metadata.diagnostic.definitionId,
|
|
99526
|
+
diagnosticKey: input.metadata.diagnostic.diagnosticKey,
|
|
99527
|
+
routingRevision: input.metadata.diagnostic.routingRevision
|
|
99528
|
+
},
|
|
99529
|
+
routing: projectDiagnosticRoutingSnapshot(input.metadata.diagnostic.routingRevision, input.state),
|
|
99530
|
+
completion: null
|
|
99531
|
+
};
|
|
99532
|
+
}
|
|
99533
|
+
function buildConventionalAssessmentSnapshot(input) {
|
|
99534
|
+
const metadata2 = input.metadata;
|
|
99535
|
+
let selection;
|
|
99536
|
+
if (metadata2.purpose === "review") {
|
|
99537
|
+
selection = {
|
|
99538
|
+
kind: "standards-review",
|
|
99539
|
+
purpose: metadata2.purpose,
|
|
99540
|
+
standards: [...metadata2.review.standards],
|
|
99541
|
+
candidateItemsPerStandard: metadata2.review.candidateItemsPerStandard,
|
|
99542
|
+
selections: [...metadata2.review.selections],
|
|
99543
|
+
fulfillment: metadata2.review.fulfillment
|
|
99544
|
+
};
|
|
99545
|
+
} else if (metadata2.purpose === "mastery") {
|
|
99546
|
+
selection = {
|
|
99547
|
+
kind: "standard-quiz",
|
|
99548
|
+
purpose: metadata2.purpose,
|
|
99549
|
+
standard: metadata2.mastery.standard
|
|
99550
|
+
};
|
|
99551
|
+
} else {
|
|
99552
|
+
selection = { kind: "fixed-test", purpose: metadata2.purpose };
|
|
99553
|
+
}
|
|
99554
|
+
return {
|
|
99555
|
+
attemptId: input.attemptId,
|
|
99556
|
+
responseVersion: metadata2.responseVersion,
|
|
99557
|
+
status: "in_progress",
|
|
99558
|
+
flow: assessmentFlowForPurpose(metadata2.purpose),
|
|
99559
|
+
...input.activityData ? { activityData: input.activityData } : {},
|
|
99560
|
+
assessment: assessmentPresentationForAttempt(input.assessment, input.attemptId),
|
|
99561
|
+
responses: metadata2.responses,
|
|
99562
|
+
itemSubmissions: metadata2.itemSubmissions,
|
|
99563
|
+
score: null,
|
|
99564
|
+
selection
|
|
99565
|
+
};
|
|
99566
|
+
}
|
|
99567
|
+
function buildAssessmentAwardRecord(input, mastery) {
|
|
99568
|
+
return {
|
|
99569
|
+
xpAwarded: input.xpAwarded,
|
|
99570
|
+
...input.masteredUnits !== undefined ? { masteredUnits: input.masteredUnits } : {},
|
|
99571
|
+
...input.masteredUnitsAbsolute !== undefined ? { masteredUnitsAbsolute: input.masteredUnitsAbsolute } : {},
|
|
99572
|
+
masteredUnitsApplied: mastery.masteredUnitsApplied,
|
|
99573
|
+
...mastery.pctCompleteApp !== undefined ? { pctCompleteApp: mastery.pctCompleteApp } : {},
|
|
99574
|
+
...mastery.warnings ? { warnings: mastery.warnings } : {}
|
|
99575
|
+
};
|
|
99576
|
+
}
|
|
99356
99577
|
function stageAssessmentAttemptSupersession(attempt) {
|
|
99357
99578
|
Object.assign(attempt.result, ASSESSMENT_ATTEMPT_SUPERSEDED);
|
|
99358
99579
|
Object.assign(attempt.selection, ASSESSMENT_ATTEMPT_SUPERSEDED);
|
|
@@ -99563,13 +99784,35 @@ function buildAssessmentSubmitResult(input) {
|
|
|
99563
99784
|
}
|
|
99564
99785
|
return { ...base, purpose: input.metadata.purpose };
|
|
99565
99786
|
}
|
|
99787
|
+
function assessmentAwardFromResult(result, metadata2) {
|
|
99788
|
+
if (metadata2.award) {
|
|
99789
|
+
return metadata2.award;
|
|
99790
|
+
}
|
|
99791
|
+
const xp = result.metadata?.xp;
|
|
99792
|
+
return typeof xp === "number" && Number.isFinite(xp) && xp >= 0 ? { xpAwarded: xp, masteredUnitsApplied: 0 } : null;
|
|
99793
|
+
}
|
|
99566
99794
|
function buildCompletedAssessmentSubmitResult(input) {
|
|
99567
|
-
const
|
|
99568
|
-
|
|
99795
|
+
const attemptId = input.result.sourcedId;
|
|
99796
|
+
const award = assessmentAwardFromResult(input.result, input.metadata);
|
|
99797
|
+
if (award === null) {
|
|
99798
|
+
throw new GoneError("This completed assessment is missing its recorded XP.", {
|
|
99799
|
+
attemptId
|
|
99800
|
+
});
|
|
99801
|
+
}
|
|
99802
|
+
const submitted = buildAssessmentSubmitResult({ attemptId, metadata: input.metadata });
|
|
99803
|
+
if (!submitted) {
|
|
99804
|
+
throw new GoneError("This completed assessment has incomplete persisted results.", {
|
|
99805
|
+
attemptId
|
|
99806
|
+
});
|
|
99807
|
+
}
|
|
99808
|
+
return {
|
|
99569
99809
|
...submitted,
|
|
99570
99810
|
status: "completed",
|
|
99571
|
-
xpAwarded:
|
|
99572
|
-
|
|
99811
|
+
xpAwarded: award.xpAwarded,
|
|
99812
|
+
masteredUnitsApplied: award.masteredUnitsApplied,
|
|
99813
|
+
...award.pctCompleteApp !== undefined ? { pctCompleteApp: award.pctCompleteApp } : {},
|
|
99814
|
+
...award.warnings ? { warnings: award.warnings } : {}
|
|
99815
|
+
};
|
|
99573
99816
|
}
|
|
99574
99817
|
function assessmentAttemptId(input) {
|
|
99575
99818
|
return deterministicUUID([
|
|
@@ -99896,6 +100139,22 @@ function assessmentFixtureScoringKeys(assessment, questions) {
|
|
|
99896
100139
|
...Object.keys(responseAreas).length > 0 ? { responseAreas } : {}
|
|
99897
100140
|
};
|
|
99898
100141
|
}
|
|
100142
|
+
function prepareDiagnosticAssessmentResponses(assessment, current, input) {
|
|
100143
|
+
const update2 = { [input.itemIdentifier]: input.responses };
|
|
100144
|
+
validateAssessmentResponseUpdate(assessment, update2);
|
|
100145
|
+
const responses = applyAssessmentResponseUpdate(current, update2);
|
|
100146
|
+
const item = assessment.items.find((candidate) => candidate.identifier === input.itemIdentifier);
|
|
100147
|
+
const itemResponses = responses[input.itemIdentifier];
|
|
100148
|
+
const missingResponse = item?.interactions.find((interaction) => itemResponses?.[interaction.responseIdentifier] === undefined);
|
|
100149
|
+
if (!item || !itemResponses || missingResponse) {
|
|
100150
|
+
throw new AssessmentRuntimeError("INVALID_RESPONSE", `Diagnostic item ${input.itemIdentifier} requires a response for every interaction.`, {
|
|
100151
|
+
itemIdentifier: input.itemIdentifier,
|
|
100152
|
+
...missingResponse ? { responseIdentifier: missingResponse.responseIdentifier } : {}
|
|
100153
|
+
});
|
|
100154
|
+
}
|
|
100155
|
+
validateAssessmentResponses(assessment, { [input.itemIdentifier]: itemResponses });
|
|
100156
|
+
return responses;
|
|
100157
|
+
}
|
|
99899
100158
|
function validateAssessmentResponseUpdate(assessment, update2) {
|
|
99900
100159
|
const message = assessmentResponseUpdateValidationMessage(assessment, update2);
|
|
99901
100160
|
if (message) {
|
|
@@ -99982,7 +100241,7 @@ function buildAssessmentResultSubmission(input) {
|
|
|
99982
100241
|
}
|
|
99983
100242
|
};
|
|
99984
100243
|
}
|
|
99985
|
-
function
|
|
100244
|
+
function buildAssessmentResultAwardFinalization(input) {
|
|
99986
100245
|
const completion = input.metadata.completion;
|
|
99987
100246
|
if (!completion || completion.totalQuestions === undefined) {
|
|
99988
100247
|
throw new Error("A scored assessment must retain its completion facts before finalization");
|
|
@@ -99990,8 +100249,10 @@ function buildAssessmentResultXpFinalization(input) {
|
|
|
99990
100249
|
const metadata2 = {
|
|
99991
100250
|
...input.metadata,
|
|
99992
100251
|
finalizedAt: input.timestamp,
|
|
99993
|
-
updatedAt: input.timestamp
|
|
100252
|
+
updatedAt: input.timestamp,
|
|
100253
|
+
award: input.award
|
|
99994
100254
|
};
|
|
100255
|
+
const masteryRequested = input.award.masteredUnits !== undefined || input.award.masteredUnitsAbsolute !== undefined;
|
|
99995
100256
|
return {
|
|
99996
100257
|
metadata: metadata2,
|
|
99997
100258
|
resultUpdate: {
|
|
@@ -100003,7 +100264,8 @@ function buildAssessmentResultXpFinalization(input) {
|
|
|
100003
100264
|
...ASSESSMENT_ATTEMPT_COMPLETED,
|
|
100004
100265
|
metadata: {
|
|
100005
100266
|
...mergeAssessmentRuntimeMetadata(input.result.metadata, metadata2),
|
|
100006
|
-
xp: input.xpAwarded,
|
|
100267
|
+
xp: input.award.xpAwarded,
|
|
100268
|
+
...masteryRequested ? { masteredUnits: input.award.masteredUnitsApplied } : {},
|
|
100007
100269
|
totalQuestions: completion.totalQuestions,
|
|
100008
100270
|
correctQuestions: completion.correctQuestions,
|
|
100009
100271
|
appName: input.appName
|
|
@@ -100016,6 +100278,7 @@ var init_timeback_assessment_runtime_util = __esm(() => {
|
|
|
100016
100278
|
init_src();
|
|
100017
100279
|
init_assessment_runtime2();
|
|
100018
100280
|
init_qti();
|
|
100281
|
+
init_types2();
|
|
100019
100282
|
init_utils6();
|
|
100020
100283
|
init_timeback4();
|
|
100021
100284
|
init_uuid();
|
|
@@ -100024,6 +100287,210 @@ var init_timeback_assessment_runtime_util = __esm(() => {
|
|
|
100024
100287
|
});
|
|
100025
100288
|
|
|
100026
100289
|
// ../api-core/src/utils/timeback-review-mapping.util.ts
|
|
100290
|
+
function reviewBankTestItemIdentifiers(test) {
|
|
100291
|
+
return qtiTestParts(test).flatMap((part) => part["qti-assessment-section"].flatMap((section) => (section["qti-assessment-item-ref"] ?? []).map((reference) => reference.identifier)));
|
|
100292
|
+
}
|
|
100293
|
+
function canonicalSyncContributors(contributors) {
|
|
100294
|
+
return contributors.map((contributor) => {
|
|
100295
|
+
const canonical = canonicalContributorIdentity(contributor);
|
|
100296
|
+
return {
|
|
100297
|
+
...contributor,
|
|
100298
|
+
...canonical
|
|
100299
|
+
};
|
|
100300
|
+
}).toSorted((left, right) => compareCodeUnits(left.standardKey, right.standardKey) || compareCodeUnits(left.qtiTestIdentifier, right.qtiTestIdentifier) || compareCodeUnits(left.assessmentKey, right.assessmentKey));
|
|
100301
|
+
}
|
|
100302
|
+
function canonicalContributorIdentity(contributor) {
|
|
100303
|
+
const standard = canonicalReviewStandardRef(contributor.standard);
|
|
100304
|
+
if (!standard) {
|
|
100305
|
+
throw new ValidationError(`Mastery assessment ${contributor.qtiTestIdentifier} has an invalid standard`);
|
|
100306
|
+
}
|
|
100307
|
+
return {
|
|
100308
|
+
qtiTestIdentifier: contributor.qtiTestIdentifier,
|
|
100309
|
+
standard,
|
|
100310
|
+
standardKey: assessmentStandardRefKey2(standard)
|
|
100311
|
+
};
|
|
100312
|
+
}
|
|
100313
|
+
async function reviewBankContributorFingerprint(contributors) {
|
|
100314
|
+
const uniqueTuples = new Map;
|
|
100315
|
+
for (const contributor of contributors) {
|
|
100316
|
+
const { qtiTestIdentifier, standardKey } = canonicalContributorIdentity(contributor);
|
|
100317
|
+
const tuple3 = { qtiTestIdentifier, standardKey };
|
|
100318
|
+
uniqueTuples.set(canonicalJson2(tuple3), tuple3);
|
|
100319
|
+
}
|
|
100320
|
+
const digest = await sha256Hex(canonicalJson2([...uniqueTuples.values()].toSorted((left, right) => compareCodeUnits(left.standardKey, right.standardKey) || compareCodeUnits(left.qtiTestIdentifier, right.qtiTestIdentifier))));
|
|
100321
|
+
return `review-bank-contributors-v1:${digest}`;
|
|
100322
|
+
}
|
|
100323
|
+
function conflictWarning(input) {
|
|
100324
|
+
const retained = `${input.retainedStandard.framework} · ${input.retainedStandard.identifier}`;
|
|
100325
|
+
const skipped = `${input.skippedStandard.framework} · ${input.skippedStandard.identifier}`;
|
|
100326
|
+
return {
|
|
100327
|
+
code: "QUESTION_STANDARD_CONFLICT",
|
|
100328
|
+
...input,
|
|
100329
|
+
message: `Question ${input.itemIdentifier} is referenced by multiple mastery standards; retained ${retained} and skipped ${skipped} from ${input.skippedQtiTestIdentifier}.`
|
|
100330
|
+
};
|
|
100331
|
+
}
|
|
100332
|
+
async function prepareReviewBankSynchronization(input) {
|
|
100333
|
+
const contributors = canonicalSyncContributors(input.contributors);
|
|
100334
|
+
const selectedByItem = new Map;
|
|
100335
|
+
const warningKeys = new Set;
|
|
100336
|
+
const warnings = [];
|
|
100337
|
+
for (const contributor of contributors) {
|
|
100338
|
+
const itemIdentifiers2 = [
|
|
100339
|
+
...new Set(reviewBankTestItemIdentifiers(contributor.test))
|
|
100340
|
+
].toSorted();
|
|
100341
|
+
for (const itemIdentifier of itemIdentifiers2) {
|
|
100342
|
+
const selected = selectedByItem.get(itemIdentifier);
|
|
100343
|
+
if (!selected) {
|
|
100344
|
+
selectedByItem.set(itemIdentifier, {
|
|
100345
|
+
standard: contributor.standard,
|
|
100346
|
+
standardKey: contributor.standardKey
|
|
100347
|
+
});
|
|
100348
|
+
} else if (selected.standardKey !== contributor.standardKey) {
|
|
100349
|
+
const warningKey = `${itemIdentifier}\x00${contributor.standardKey}`;
|
|
100350
|
+
if (!warningKeys.has(warningKey)) {
|
|
100351
|
+
warningKeys.add(warningKey);
|
|
100352
|
+
warnings.push(conflictWarning({
|
|
100353
|
+
itemIdentifier,
|
|
100354
|
+
retainedStandard: selected.standard,
|
|
100355
|
+
skippedStandard: contributor.standard,
|
|
100356
|
+
skippedQtiTestIdentifier: contributor.qtiTestIdentifier
|
|
100357
|
+
}));
|
|
100358
|
+
}
|
|
100359
|
+
}
|
|
100360
|
+
}
|
|
100361
|
+
}
|
|
100362
|
+
const itemIdentifiers = [...selectedByItem.keys()].toSorted();
|
|
100363
|
+
const sourceContentRevision = input.contributorFingerprint ?? await reviewBankContributorFingerprint(contributors);
|
|
100364
|
+
const bank = await buildReviewBankIndexFromReferences({
|
|
100365
|
+
bankIdentifier: input.bankIdentifier,
|
|
100366
|
+
sourceContentRevision,
|
|
100367
|
+
items: itemIdentifiers.map((itemIdentifier) => ({
|
|
100368
|
+
itemIdentifier,
|
|
100369
|
+
standards: [selectedByItem.get(itemIdentifier).standard]
|
|
100370
|
+
}))
|
|
100371
|
+
});
|
|
100372
|
+
const manifest = await buildReviewBankManifest(bank);
|
|
100373
|
+
return {
|
|
100374
|
+
bankIdentifier: input.bankIdentifier,
|
|
100375
|
+
contributorFingerprint: sourceContentRevision,
|
|
100376
|
+
itemIdentifiers,
|
|
100377
|
+
manifest,
|
|
100378
|
+
warnings,
|
|
100379
|
+
summary: {
|
|
100380
|
+
itemCount: itemIdentifiers.length,
|
|
100381
|
+
standardCount: Object.keys(manifest.itemsByStandard).length,
|
|
100382
|
+
contributorCount: contributors.length,
|
|
100383
|
+
sourceFingerprint: manifest.sourceFingerprint
|
|
100384
|
+
}
|
|
100385
|
+
};
|
|
100386
|
+
}
|
|
100387
|
+
function reviewBankSinkMetadata(integrationId, plan) {
|
|
100388
|
+
return {
|
|
100389
|
+
ownerSystem: PLAYCADEMY_QTI_OWNER_SYSTEM,
|
|
100390
|
+
integrationId,
|
|
100391
|
+
[PLAYCADEMY_REVIEW_BANK_SINK_METADATA_KEY]: {
|
|
100392
|
+
version: PLAYCADEMY_REVIEW_BANK_SINK_VERSION,
|
|
100393
|
+
integrationId,
|
|
100394
|
+
contributorFingerprint: plan.contributorFingerprint,
|
|
100395
|
+
warnings: plan.warnings
|
|
100396
|
+
},
|
|
100397
|
+
[PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY]: plan.manifest
|
|
100398
|
+
};
|
|
100399
|
+
}
|
|
100400
|
+
function reviewBankSinkMarker(test) {
|
|
100401
|
+
const parsed = ReviewBankSinkMarkerSchema.safeParse(test.metadata?.[PLAYCADEMY_REVIEW_BANK_SINK_METADATA_KEY]);
|
|
100402
|
+
return parsed.success ? parsed.data : null;
|
|
100403
|
+
}
|
|
100404
|
+
function reviewBankSinkState(test, integrationId) {
|
|
100405
|
+
assertReviewBankSinkOwnership(test, integrationId);
|
|
100406
|
+
const marker = reviewBankSinkMarker(test);
|
|
100407
|
+
return {
|
|
100408
|
+
contributorFingerprint: marker?.contributorFingerprint ?? null,
|
|
100409
|
+
warnings: marker?.warnings ?? []
|
|
100410
|
+
};
|
|
100411
|
+
}
|
|
100412
|
+
async function unchangedReviewBankSynchronizationResult(input) {
|
|
100413
|
+
const sinkState = reviewBankSinkState(input.sink, input.integrationId);
|
|
100414
|
+
if (sinkState.contributorFingerprint !== input.contributorFingerprint) {
|
|
100415
|
+
return null;
|
|
100416
|
+
}
|
|
100417
|
+
const membershipItemIdentifiers = reviewBankTestItemIdentifiers(input.sink);
|
|
100418
|
+
const bank = await reviewBankIndexFromManifest(input.sink.metadata, {
|
|
100419
|
+
bankIdentifier: input.sink.identifier,
|
|
100420
|
+
membershipItemIdentifiers
|
|
100421
|
+
});
|
|
100422
|
+
const manifest = input.sink.metadata?.[PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY];
|
|
100423
|
+
if (!bank || typeof manifest?.sourceFingerprint !== "string") {
|
|
100424
|
+
return null;
|
|
100425
|
+
}
|
|
100426
|
+
const standardCount = new Set(bank.items.flatMap((item) => item.standards.map(assessmentStandardRefKey2))).size;
|
|
100427
|
+
return {
|
|
100428
|
+
qtiTestIdentifier: input.sink.identifier,
|
|
100429
|
+
itemCount: bank.items.length,
|
|
100430
|
+
standardCount,
|
|
100431
|
+
contributorCount: input.contributorCount,
|
|
100432
|
+
sourceFingerprint: manifest.sourceFingerprint,
|
|
100433
|
+
addedItemCount: 0,
|
|
100434
|
+
removedItemCount: 0,
|
|
100435
|
+
unchanged: true,
|
|
100436
|
+
warnings: sinkState.warnings
|
|
100437
|
+
};
|
|
100438
|
+
}
|
|
100439
|
+
function reviewBankSynchronizationDiff(input) {
|
|
100440
|
+
let previousMembershipReadable = true;
|
|
100441
|
+
let previousItemIdentifiers = [];
|
|
100442
|
+
if (input.existingSink) {
|
|
100443
|
+
try {
|
|
100444
|
+
previousItemIdentifiers = reviewBankTestItemIdentifiers(input.existingSink);
|
|
100445
|
+
} catch {
|
|
100446
|
+
previousMembershipReadable = false;
|
|
100447
|
+
}
|
|
100448
|
+
}
|
|
100449
|
+
const previousItems = new Set(previousItemIdentifiers);
|
|
100450
|
+
const desiredItems = new Set(input.plan.itemIdentifiers);
|
|
100451
|
+
const addedItemCount = input.plan.itemIdentifiers.filter((identifier) => !previousItems.has(identifier)).length;
|
|
100452
|
+
const removedItemCount = [...previousItems].filter((identifier) => !desiredItems.has(identifier)).length;
|
|
100453
|
+
const currentManifest = input.existingSink?.metadata?.[PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY];
|
|
100454
|
+
const currentContributorFingerprint = input.existingSink ? reviewBankSinkMarker(input.existingSink)?.contributorFingerprint ?? null : null;
|
|
100455
|
+
const canonicalMembership = previousItemIdentifiers.length === input.plan.itemIdentifiers.length && previousItemIdentifiers.every((identifier, index2) => identifier === input.plan.itemIdentifiers[index2]);
|
|
100456
|
+
const unchanged = input.existingSink !== null && previousMembershipReadable && canonicalMembership && canonicalJson2(currentManifest) === canonicalJson2(input.plan.manifest) && currentContributorFingerprint === input.plan.contributorFingerprint;
|
|
100457
|
+
return { addedItemCount, removedItemCount, unchanged };
|
|
100458
|
+
}
|
|
100459
|
+
function assertReviewBankSinkOwnership(test, integrationId) {
|
|
100460
|
+
const marker = reviewBankSinkMarker(test);
|
|
100461
|
+
if (marker?.integrationId !== integrationId || test.metadata?.ownerSystem !== PLAYCADEMY_QTI_OWNER_SYSTEM || test.identifier !== reviewBankSinkIdentifier(integrationId)) {
|
|
100462
|
+
throw new ValidationError(`QTI assessment ${test.identifier} is not the review-bank sink for this integration`);
|
|
100463
|
+
}
|
|
100464
|
+
}
|
|
100465
|
+
function buildReviewBankSinkInput(input) {
|
|
100466
|
+
return {
|
|
100467
|
+
...input.includeIdentifier ? { identifier: input.identifier } : {},
|
|
100468
|
+
title: input.title,
|
|
100469
|
+
metadata: input.metadata,
|
|
100470
|
+
"qti-test-part": [
|
|
100471
|
+
{
|
|
100472
|
+
identifier: `${input.identifier}-part1`,
|
|
100473
|
+
navigationMode: "linear",
|
|
100474
|
+
submissionMode: "individual",
|
|
100475
|
+
"qti-assessment-section": [
|
|
100476
|
+
{
|
|
100477
|
+
identifier: `${input.identifier}-section1`,
|
|
100478
|
+
title: "Questions",
|
|
100479
|
+
visible: true,
|
|
100480
|
+
required: true,
|
|
100481
|
+
fixed: false,
|
|
100482
|
+
sequence: 1,
|
|
100483
|
+
"qti-assessment-item-ref": input.itemIdentifiers.map((itemIdentifier, index2) => ({
|
|
100484
|
+
identifier: itemIdentifier,
|
|
100485
|
+
href: input.itemHref(itemIdentifier),
|
|
100486
|
+
sequence: index2 + 1
|
|
100487
|
+
}))
|
|
100488
|
+
}
|
|
100489
|
+
]
|
|
100490
|
+
}
|
|
100491
|
+
]
|
|
100492
|
+
};
|
|
100493
|
+
}
|
|
100027
100494
|
function reviewMappingIssueSummary(label, identifiers) {
|
|
100028
100495
|
if (identifiers.length === 0) {
|
|
100029
100496
|
return null;
|
|
@@ -100072,11 +100539,47 @@ async function prepareReviewMappingUpdate(test, questions) {
|
|
|
100072
100539
|
}
|
|
100073
100540
|
};
|
|
100074
100541
|
}
|
|
100542
|
+
var PLAYCADEMY_REVIEW_BANK_SINK_METADATA_KEY = "playcademyReviewBankSink", PLAYCADEMY_REVIEW_BANK_SINK_VERSION = 1, StoredSinkWarningSchema, ReviewBankSinkMarkerSchema;
|
|
100075
100543
|
var init_timeback_review_mapping_util = __esm(() => {
|
|
100544
|
+
init_esm();
|
|
100076
100545
|
init_assessment_runtime2();
|
|
100546
|
+
init_qti();
|
|
100547
|
+
init_timeback3();
|
|
100077
100548
|
init_errors2();
|
|
100078
100549
|
init_timeback_assessment_runtime_util();
|
|
100079
100550
|
init_timeback_qti_authoring_util();
|
|
100551
|
+
StoredSinkWarningSchema = exports_external.object({
|
|
100552
|
+
code: exports_external.literal("QUESTION_STANDARD_CONFLICT"),
|
|
100553
|
+
itemIdentifier: exports_external.string(),
|
|
100554
|
+
retainedStandard: exports_external.object({ framework: exports_external.string(), identifier: exports_external.string() }),
|
|
100555
|
+
skippedStandard: exports_external.object({ framework: exports_external.string(), identifier: exports_external.string() }),
|
|
100556
|
+
skippedQtiTestIdentifier: exports_external.string()
|
|
100557
|
+
});
|
|
100558
|
+
ReviewBankSinkMarkerSchema = exports_external.object({
|
|
100559
|
+
version: exports_external.literal(PLAYCADEMY_REVIEW_BANK_SINK_VERSION),
|
|
100560
|
+
integrationId: exports_external.string(),
|
|
100561
|
+
contributorFingerprint: exports_external.string().nullable().catch(null),
|
|
100562
|
+
warnings: exports_external.array(StoredSinkWarningSchema).nullable().catch(null)
|
|
100563
|
+
}).transform((marker) => {
|
|
100564
|
+
if (!marker.contributorFingerprint || !marker.warnings) {
|
|
100565
|
+
return { ...marker, contributorFingerprint: null, warnings: [] };
|
|
100566
|
+
}
|
|
100567
|
+
const warnings = [];
|
|
100568
|
+
for (const warning of marker.warnings) {
|
|
100569
|
+
const retained = canonicalReviewStandardRef(warning.retainedStandard);
|
|
100570
|
+
const skipped = canonicalReviewStandardRef(warning.skippedStandard);
|
|
100571
|
+
if (!retained || !skipped) {
|
|
100572
|
+
return { ...marker, contributorFingerprint: null, warnings: [] };
|
|
100573
|
+
}
|
|
100574
|
+
warnings.push(conflictWarning({
|
|
100575
|
+
itemIdentifier: warning.itemIdentifier,
|
|
100576
|
+
retainedStandard: retained,
|
|
100577
|
+
skippedStandard: skipped,
|
|
100578
|
+
skippedQtiTestIdentifier: warning.skippedQtiTestIdentifier
|
|
100579
|
+
}));
|
|
100580
|
+
}
|
|
100581
|
+
return { ...marker, warnings };
|
|
100582
|
+
});
|
|
100080
100583
|
});
|
|
100081
100584
|
|
|
100082
100585
|
// ../api-core/src/utils/timeback-assessment-publication.util.ts
|
|
@@ -100380,11 +100883,6 @@ function assertAssessmentHasQuestions(questions) {
|
|
|
100380
100883
|
throw new ValidationError("An assessment must contain at least one question to publish");
|
|
100381
100884
|
}
|
|
100382
100885
|
}
|
|
100383
|
-
function assertReviewAssessmentHasStandards(standardCounts) {
|
|
100384
|
-
if (standardCounts.some((count) => count !== 1)) {
|
|
100385
|
-
throw new ValidationError("Every question in a review assessment must have exactly one standards alignment");
|
|
100386
|
-
}
|
|
100387
|
-
}
|
|
100388
100886
|
function planAssessmentRemoval(status) {
|
|
100389
100887
|
if (status === "draft") {
|
|
100390
100888
|
return { kind: "delete", action: "discarded", operation: "discard_draft" };
|
|
@@ -100402,9 +100900,6 @@ function buildAssessmentAssociationUpdates(row, input) {
|
|
|
100402
100900
|
updates.standardFramework = null;
|
|
100403
100901
|
updates.standardIdentifier = null;
|
|
100404
100902
|
}
|
|
100405
|
-
if (row.status === "live" && input.purpose !== row.purpose) {
|
|
100406
|
-
updates.sortOrder = null;
|
|
100407
|
-
}
|
|
100408
100903
|
}
|
|
100409
100904
|
if (input.standard !== undefined) {
|
|
100410
100905
|
updates.standardFramework = input.standard.framework;
|
|
@@ -100412,9 +100907,6 @@ function buildAssessmentAssociationUpdates(row, input) {
|
|
|
100412
100907
|
}
|
|
100413
100908
|
if (input.status !== undefined) {
|
|
100414
100909
|
updates.status = input.status;
|
|
100415
|
-
if (input.status === "archived") {
|
|
100416
|
-
updates.sortOrder = null;
|
|
100417
|
-
}
|
|
100418
100910
|
}
|
|
100419
100911
|
return updates;
|
|
100420
100912
|
}
|
|
@@ -100423,30 +100915,17 @@ function assessmentStandardForRow(row) {
|
|
|
100423
100915
|
}
|
|
100424
100916
|
function validateUniqueAssessmentIdentifiers(testIdentifiers) {
|
|
100425
100917
|
if (new Set(testIdentifiers).size !== testIdentifiers.length) {
|
|
100426
|
-
throw new ValidationError("Assessment
|
|
100427
|
-
}
|
|
100428
|
-
}
|
|
100429
|
-
function assertAssessmentOrderUpdateSucceeded(updatedRow) {
|
|
100430
|
-
if (!updatedRow) {
|
|
100431
|
-
throw new ValidationError("Assessment order changed while it was being saved. Refresh and try again.");
|
|
100432
|
-
}
|
|
100433
|
-
return updatedRow;
|
|
100434
|
-
}
|
|
100435
|
-
function lockOrderAssessmentRows(rows) {
|
|
100436
|
-
return rows.toSorted((left, right) => left.id.localeCompare(right.id));
|
|
100437
|
-
}
|
|
100438
|
-
function orderLiveAssessmentRows(liveRows, purpose, testIdentifiers) {
|
|
100439
|
-
const rowsByIdentifier = new Map(liveRows.map((row) => [row.qtiTestIdentifier, row]));
|
|
100440
|
-
if (liveRows.length !== testIdentifiers.length || testIdentifiers.some((identifier) => !rowsByIdentifier.has(identifier))) {
|
|
100441
|
-
throw new ValidationError(`Assessment order must include every live ${purpose} assessment exactly once`);
|
|
100918
|
+
throw new ValidationError("Assessment manifests must contain unique QTI test identifiers");
|
|
100442
100919
|
}
|
|
100443
|
-
return testIdentifiers.map((identifier) => rowsByIdentifier.get(identifier));
|
|
100444
100920
|
}
|
|
100445
100921
|
var init_timeback_assessment_rules_util = __esm(() => {
|
|
100446
100922
|
init_errors2();
|
|
100447
100923
|
});
|
|
100448
100924
|
|
|
100449
100925
|
// ../api-core/src/utils/timeback-qti-hydration.util.ts
|
|
100926
|
+
function qtiItemHref(client2, itemIdentifier) {
|
|
100927
|
+
return `${client2.getQtiBaseUrl()}/assessment-items/${itemIdentifier}`;
|
|
100928
|
+
}
|
|
100450
100929
|
async function hydrateQtiTestQuestions(client2, references) {
|
|
100451
100930
|
const questions = await runWithConcurrency(references.questions, QTI_HYDRATION_CONCURRENCY, async (reference) => ({
|
|
100452
100931
|
...reference,
|
|
@@ -100469,6 +100948,28 @@ async function hydrateQtiTestQuestionSelection(client2, references, itemIdentifi
|
|
|
100469
100948
|
questions: selectedReferences
|
|
100470
100949
|
});
|
|
100471
100950
|
}
|
|
100951
|
+
async function hydrateQtiItemSelection(client2, test, itemIdentifiers) {
|
|
100952
|
+
const testPart = qtiTestParts(test)[0];
|
|
100953
|
+
const section = testPart?.["qti-assessment-section"][0];
|
|
100954
|
+
if (!testPart || !section) {
|
|
100955
|
+
throw new Error(`QTI assessment ${test.identifier} has no section to reference items from`);
|
|
100956
|
+
}
|
|
100957
|
+
const questions = await runWithConcurrency(itemIdentifiers, QTI_HYDRATION_CONCURRENCY, async (itemIdentifier) => ({
|
|
100958
|
+
reference: {
|
|
100959
|
+
identifier: itemIdentifier,
|
|
100960
|
+
href: qtiItemHref(client2, itemIdentifier),
|
|
100961
|
+
testPart: testPart.identifier,
|
|
100962
|
+
section: section.identifier
|
|
100963
|
+
},
|
|
100964
|
+
question: await client2.qtiApi.assessmentItems.get(itemIdentifier)
|
|
100965
|
+
}));
|
|
100966
|
+
return {
|
|
100967
|
+
assessmentTest: test.identifier,
|
|
100968
|
+
title: test.title,
|
|
100969
|
+
totalQuestions: questions.length,
|
|
100970
|
+
questions
|
|
100971
|
+
};
|
|
100972
|
+
}
|
|
100472
100973
|
async function loadQtiTestReferences(client2, identifier) {
|
|
100473
100974
|
const [test, references] = await Promise.all([
|
|
100474
100975
|
client2.qtiApi.assessmentTests.get(identifier),
|
|
@@ -100484,7 +100985,9 @@ async function loadHydratedQtiTest(client2, identifier) {
|
|
|
100484
100985
|
};
|
|
100485
100986
|
}
|
|
100486
100987
|
var QTI_HYDRATION_CONCURRENCY = 8;
|
|
100487
|
-
var init_timeback_qti_hydration_util = () => {
|
|
100988
|
+
var init_timeback_qti_hydration_util = __esm(() => {
|
|
100989
|
+
init_timeback_qti_authoring_util();
|
|
100990
|
+
});
|
|
100488
100991
|
|
|
100489
100992
|
// ../api-core/src/services/timeback-assessment-runtime.service.ts
|
|
100490
100993
|
var TimebackAssessmentRuntimeService;
|
|
@@ -100514,8 +101017,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
100514
101017
|
static EXPORT_CONCURRENCY = 4;
|
|
100515
101018
|
static ITEM_SUBMISSION_MAX_PREPARATION_ATTEMPTS = 3;
|
|
100516
101019
|
static ASSESSMENT_VERSION_CONFIRMATION_DELAY_MS = 25;
|
|
100517
|
-
static PROJECTION_BARRIER_MAX_ATTEMPTS = 5;
|
|
100518
|
-
static PROJECTION_BARRIER_RETRY_DELAY_MS = 25;
|
|
100519
101020
|
static SCORING_CONCURRENCY = 4;
|
|
100520
101021
|
static STALE_AWARD_AGE_MS = 15 * 60 * 1000;
|
|
100521
101022
|
deps;
|
|
@@ -101058,15 +101559,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
101058
101559
|
this.assertInProgress(attempt.result);
|
|
101059
101560
|
if (!preview || preview.responseVersion !== attempt.metadata.responseVersion || preview.submissionId !== input.submissionId || preview.itemIdentifier !== input.itemIdentifier) {
|
|
101060
101561
|
if (preparationAttempts >= TimebackAssessmentRuntimeService.ITEM_SUBMISSION_MAX_PREPARATION_ATTEMPTS) {
|
|
101061
|
-
|
|
101062
|
-
throw new ServiceUnavailableError("The assessment response state changed repeatedly while this item was being prepared. Try again shortly.", {
|
|
101063
|
-
retryable: true,
|
|
101064
|
-
reason: "PREPARATION_CONTENTION",
|
|
101065
|
-
expectedResponseVersion: input.expectedResponseVersion,
|
|
101066
|
-
responseVersion: attempt.metadata.responseVersion
|
|
101067
|
-
});
|
|
101068
|
-
}
|
|
101069
|
-
throw AssessmentRuntimeError.from(assessmentResponseVersionConflict(input.expectedResponseVersion, attempt.metadata.responseVersion));
|
|
101562
|
+
throw assessmentPreparationContentionError(input.expectedResponseVersion, attempt.metadata.responseVersion, "assessment");
|
|
101070
101563
|
}
|
|
101071
101564
|
return { action: "prepare" };
|
|
101072
101565
|
}
|
|
@@ -101189,15 +101682,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
101189
101682
|
this.assertInProgress(attempt.result);
|
|
101190
101683
|
if (!preview || preview.responseVersion !== metadata2.responseVersion || preview.submissionId !== input.submissionId || preview.routingNodeKey !== input.routingNodeKey || preview.itemIdentifier !== input.itemIdentifier) {
|
|
101191
101684
|
if (preparationAttempts >= TimebackAssessmentRuntimeService.ITEM_SUBMISSION_MAX_PREPARATION_ATTEMPTS) {
|
|
101192
|
-
|
|
101193
|
-
throw new ServiceUnavailableError("The diagnostic response state changed repeatedly while this item was being prepared. Try again shortly.", {
|
|
101194
|
-
retryable: true,
|
|
101195
|
-
reason: "PREPARATION_CONTENTION",
|
|
101196
|
-
expectedResponseVersion: input.expectedResponseVersion,
|
|
101197
|
-
responseVersion: metadata2.responseVersion
|
|
101198
|
-
});
|
|
101199
|
-
}
|
|
101200
|
-
throw AssessmentRuntimeError.from(assessmentResponseVersionConflict(input.expectedResponseVersion, metadata2.responseVersion));
|
|
101685
|
+
throw assessmentPreparationContentionError(input.expectedResponseVersion, metadata2.responseVersion, "diagnostic");
|
|
101201
101686
|
}
|
|
101202
101687
|
return { action: "prepare" };
|
|
101203
101688
|
}
|
|
@@ -101212,7 +101697,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
101212
101697
|
itemIdentifier: input.itemIdentifier
|
|
101213
101698
|
}));
|
|
101214
101699
|
}
|
|
101215
|
-
const responses =
|
|
101700
|
+
const responses = prepareDiagnosticAssessmentResponses(preview.assessment, metadata2.responses, input);
|
|
101216
101701
|
const scoring = preview.scoring;
|
|
101217
101702
|
if (!scoring || typeof scoring.isCorrect !== "boolean") {
|
|
101218
101703
|
throw AssessmentRuntimeError.from(assessmentFlowViolation(`Diagnostic item ${input.itemIdentifier} did not produce determinate binary grading.`, { itemIdentifier: input.itemIdentifier }));
|
|
@@ -101285,22 +101770,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
101285
101770
|
answered: submission.answered
|
|
101286
101771
|
};
|
|
101287
101772
|
}
|
|
101288
|
-
prepareDiagnosticResponses(assessment, current, input) {
|
|
101289
|
-
const update2 = { [input.itemIdentifier]: input.responses };
|
|
101290
|
-
validateAssessmentResponseUpdate(assessment, update2);
|
|
101291
|
-
const responses = applyAssessmentResponseUpdate(current, update2);
|
|
101292
|
-
const item = assessment.items.find((candidate) => candidate.identifier === input.itemIdentifier);
|
|
101293
|
-
const itemResponses = responses[input.itemIdentifier];
|
|
101294
|
-
const missingResponse = item?.interactions.find((interaction) => itemResponses?.[interaction.responseIdentifier] === undefined);
|
|
101295
|
-
if (!item || !itemResponses || missingResponse) {
|
|
101296
|
-
throw new AssessmentRuntimeError("INVALID_RESPONSE", `Diagnostic item ${input.itemIdentifier} requires a response for every interaction.`, {
|
|
101297
|
-
itemIdentifier: input.itemIdentifier,
|
|
101298
|
-
...missingResponse ? { responseIdentifier: missingResponse.responseIdentifier } : {}
|
|
101299
|
-
});
|
|
101300
|
-
}
|
|
101301
|
-
validateAssessmentResponses(assessment, { [input.itemIdentifier]: itemResponses });
|
|
101302
|
-
return responses;
|
|
101303
|
-
}
|
|
101304
101773
|
async prepareDiagnosticItemSubmission(params, input) {
|
|
101305
101774
|
try {
|
|
101306
101775
|
return await this.scoreDiagnosticItemSubmission(params, input);
|
|
@@ -101334,7 +101803,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
101334
101803
|
const expected = loaded.state.next;
|
|
101335
101804
|
let scoring = null;
|
|
101336
101805
|
if (loaded.state.status === "in-progress" && expected?.nodeKey === input.routingNodeKey && expected.itemIdentifier === input.itemIdentifier && attempt.metadata.responseVersion === input.expectedResponseVersion) {
|
|
101337
|
-
const responses =
|
|
101806
|
+
const responses = prepareDiagnosticAssessmentResponses(assessment, attempt.metadata.responses, input);
|
|
101338
101807
|
scoring = await this.scoreItem(assessment, responses, input.itemIdentifier);
|
|
101339
101808
|
}
|
|
101340
101809
|
return {
|
|
@@ -101605,11 +102074,14 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
101605
102074
|
inProgress: attempt.result.inProgress ?? "",
|
|
101606
102075
|
scoreStatus: attempt.result.scoreStatus,
|
|
101607
102076
|
submissionId: attempt.metadata.submissionId,
|
|
101608
|
-
|
|
102077
|
+
award: assessmentAwardFromResult(attempt.result, attempt.metadata) ?? undefined
|
|
101609
102078
|
}, input);
|
|
101610
102079
|
if (disposition === "replay") {
|
|
101611
102080
|
return {
|
|
101612
|
-
response:
|
|
102081
|
+
response: buildCompletedAssessmentSubmitResult({
|
|
102082
|
+
result: attempt.result,
|
|
102083
|
+
metadata: attempt.metadata
|
|
102084
|
+
}),
|
|
101613
102085
|
completion: this.replayCompletion(attempt, input.submissionId, params, game2)
|
|
101614
102086
|
};
|
|
101615
102087
|
}
|
|
@@ -101617,20 +102089,27 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
101617
102089
|
throw AssessmentRuntimeError.from(assessmentAwardConflict({
|
|
101618
102090
|
attemptId: attempt.result.sourcedId,
|
|
101619
102091
|
submissionId: input.submissionId,
|
|
101620
|
-
xpAwarded: input.xpAwarded
|
|
102092
|
+
xpAwarded: input.xpAwarded,
|
|
102093
|
+
...input.masteredUnits !== undefined ? { masteredUnits: input.masteredUnits } : {},
|
|
102094
|
+
...input.masteredUnitsAbsolute !== undefined ? { masteredUnitsAbsolute: input.masteredUnitsAbsolute } : {}
|
|
101621
102095
|
}));
|
|
101622
102096
|
}
|
|
101623
102097
|
if (disposition === "reject") {
|
|
101624
102098
|
throw AssessmentRuntimeError.from(assessmentAlreadySubmitted(attempt.result.sourcedId));
|
|
101625
102099
|
}
|
|
101626
|
-
const
|
|
101627
|
-
const
|
|
102100
|
+
const mastery = await this.evaluateMasteryAward(attempt, params.studentId, input);
|
|
102101
|
+
const awardRecord = buildAssessmentAwardRecord(input, mastery);
|
|
102102
|
+
const finalization = buildAssessmentResultAwardFinalization({
|
|
101628
102103
|
result: attempt.result,
|
|
101629
102104
|
metadata: attempt.metadata,
|
|
101630
|
-
|
|
102105
|
+
award: awardRecord,
|
|
101631
102106
|
appName: game2.appName,
|
|
101632
102107
|
timestamp: new Date().toISOString()
|
|
101633
102108
|
});
|
|
102109
|
+
const response = buildCompletedAssessmentSubmitResult({
|
|
102110
|
+
result: attempt.result,
|
|
102111
|
+
metadata: finalization.metadata
|
|
102112
|
+
});
|
|
101634
102113
|
const finalized = await this.requireClient().api.oneroster.assessmentResults.upsert(attempt.result.sourcedId, finalization.resultUpdate);
|
|
101635
102114
|
const finalizedForEmission = {
|
|
101636
102115
|
...finalized,
|
|
@@ -101649,25 +102128,78 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
101649
102128
|
"app.assessment.submission_id": input.submissionId,
|
|
101650
102129
|
"app.assessment.purpose": finalization.metadata.purpose,
|
|
101651
102130
|
"app.assessment.award_age_ms": this.awardAgeMs(attempt.result.scoreDate),
|
|
101652
|
-
"app.assessment.xp_awarded": input.xpAwarded
|
|
102131
|
+
"app.assessment.xp_awarded": input.xpAwarded,
|
|
102132
|
+
"app.assessment.mastered_units_applied": mastery.masteredUnitsApplied,
|
|
102133
|
+
"app.assessment.mastery_achieved": mastery.masteryAchieved,
|
|
102134
|
+
"app.assessment.mastery_revoked": mastery.masteryRevoked
|
|
101653
102135
|
});
|
|
102136
|
+
await this.settleMasteryAward(finalizedAttempt, mastery, input.submissionId, game2);
|
|
101654
102137
|
return {
|
|
101655
|
-
response
|
|
101656
|
-
...submitted,
|
|
101657
|
-
status: "completed",
|
|
101658
|
-
xpAwarded: input.xpAwarded
|
|
101659
|
-
},
|
|
102138
|
+
response,
|
|
101660
102139
|
completion: this.replayCompletion(finalizedAttempt, input.submissionId, params, game2)
|
|
101661
102140
|
};
|
|
101662
102141
|
});
|
|
101663
102142
|
if (award.completion) {
|
|
101664
102143
|
await this.emitCompletionBestEffort({
|
|
101665
102144
|
...award.completion,
|
|
101666
|
-
|
|
102145
|
+
award: award.response
|
|
101667
102146
|
});
|
|
101668
102147
|
}
|
|
101669
102148
|
return award.response;
|
|
101670
102149
|
}
|
|
102150
|
+
async evaluateMasteryAward(attempt, studentIdentifier, input) {
|
|
102151
|
+
if (!hasMasteryAwardRequest(input)) {
|
|
102152
|
+
return NO_MASTERY_AWARD;
|
|
102153
|
+
}
|
|
102154
|
+
const student = await this.requireClient().roster.resolveStudent(studentIdentifier);
|
|
102155
|
+
return this.requireClient().activity.evaluateMasteryAward({
|
|
102156
|
+
studentId: student.id,
|
|
102157
|
+
courseId: attempt.integration.courseId,
|
|
102158
|
+
...input.masteredUnits !== undefined ? { masteredUnits: input.masteredUnits } : {},
|
|
102159
|
+
...input.masteredUnitsAbsolute !== undefined ? { masteredUnitsAbsolute: input.masteredUnitsAbsolute } : {}
|
|
102160
|
+
});
|
|
102161
|
+
}
|
|
102162
|
+
async settleMasteryAward(attempt, mastery, submissionId, game2) {
|
|
102163
|
+
if (!mastery.masteryAchieved && !mastery.masteryRevoked) {
|
|
102164
|
+
return;
|
|
102165
|
+
}
|
|
102166
|
+
const integration = attempt.integration;
|
|
102167
|
+
const course = this.trackableCourse(integration, "assessment.mastery_settlement_invalid_course_metadata", { "app.assessment.attempt_id": attempt.result.sourcedId });
|
|
102168
|
+
if (!course) {
|
|
102169
|
+
setAttributes({ "app.assessment.mastery_completion_entry_missing": true });
|
|
102170
|
+
return;
|
|
102171
|
+
}
|
|
102172
|
+
try {
|
|
102173
|
+
const student = await this.requireClient().roster.resolveStudent(attempt.result.student.sourcedId);
|
|
102174
|
+
const completionHistoryEventId = `urn:uuid:${await deterministicUUID([
|
|
102175
|
+
"playcademy-assessment-course-completed-event-v1",
|
|
102176
|
+
attempt.result.sourcedId,
|
|
102177
|
+
submissionId
|
|
102178
|
+
].join("\x00"))}`;
|
|
102179
|
+
const settlement = await this.requireClient().activity.settleMasteryAward(mastery, {
|
|
102180
|
+
studentId: student.id,
|
|
102181
|
+
studentEmail: student.email,
|
|
102182
|
+
courseId: integration.courseId,
|
|
102183
|
+
gameId: integration.gameId,
|
|
102184
|
+
activityId: attempt.metadata.activityId,
|
|
102185
|
+
courseName: attempt.metadata.completion?.courseName ?? "Game Course",
|
|
102186
|
+
subject: course.subject,
|
|
102187
|
+
appName: game2.appName,
|
|
102188
|
+
sensorUrl: game2.sensorUrl,
|
|
102189
|
+
completionHistoryEventId
|
|
102190
|
+
});
|
|
102191
|
+
if (mastery.masteryAchieved && !settlement.completionEntryWritten) {
|
|
102192
|
+
setAttributes({ "app.assessment.mastery_completion_entry_missing": true });
|
|
102193
|
+
}
|
|
102194
|
+
} catch (error88) {
|
|
102195
|
+
setAttributes({ "app.assessment.mastery_completion_entry_missing": true });
|
|
102196
|
+
addEvent("assessment.mastery_settlement_failed", {
|
|
102197
|
+
"app.assessment.attempt_id": attempt.result.sourcedId,
|
|
102198
|
+
"exception.type": errorType(error88),
|
|
102199
|
+
"app.error.message": errorMessage(error88)
|
|
102200
|
+
});
|
|
102201
|
+
}
|
|
102202
|
+
}
|
|
101671
102203
|
async finalizeDiagnosticSubmission(input) {
|
|
101672
102204
|
const { attempt } = input;
|
|
101673
102205
|
const routing = await this.loadAttemptDiagnosticManifest(attempt.metadata, input.db);
|
|
@@ -101893,8 +102425,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
101893
102425
|
id: row.id,
|
|
101894
102426
|
assessmentKey: row.assessmentKey,
|
|
101895
102427
|
qtiTestIdentifier: row.qtiTestIdentifier,
|
|
101896
|
-
sortOrder: row.sortOrder,
|
|
101897
|
-
updatedAt: row.updatedAt.toISOString(),
|
|
101898
102428
|
standard: assessmentStandardForRow(row)
|
|
101899
102429
|
})).filter((test) => !matchesMasteryStandard || matchesMasteryStandard(test.standard));
|
|
101900
102430
|
}
|
|
@@ -102114,9 +102644,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
102114
102644
|
if (this.isSettled(resumed.result)) {
|
|
102115
102645
|
return this.snapshotForResult(resumed.result, resumed.metadata, context2.integration);
|
|
102116
102646
|
}
|
|
102117
|
-
const
|
|
102118
|
-
const currentSource = await this.hydrateReviewBankSelection(catalog, resumed.metadata.review.selections.map((selection) => selection.itemIdentifier));
|
|
102119
|
-
const source = this.pinnedReviewBankSource(currentSource, resumed.metadata);
|
|
102647
|
+
const source = await this.loadPinnedReviewBankSource(resumed.metadata);
|
|
102120
102648
|
const assessment = projectReviewAssessment(source.assessment, source.bank, resumed.metadata.review.selections);
|
|
102121
102649
|
await this.ensureReviewChildResults({
|
|
102122
102650
|
result: resumed.result,
|
|
@@ -102160,7 +102688,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
102160
102688
|
membershipItemIdentifiers: loaded.references.questions.map((question) => question.reference.identifier)
|
|
102161
102689
|
});
|
|
102162
102690
|
} catch (error88) {
|
|
102163
|
-
throw new ServiceUnavailableError(`Review bank ${identifier} has stale or invalid routing metadata. Use
|
|
102691
|
+
throw new ServiceUnavailableError(`Review bank ${identifier} has stale or invalid routing metadata. Use Sync Review Bank in assessment authoring.`, { reason: errorMessage(error88) });
|
|
102164
102692
|
}
|
|
102165
102693
|
if (!bank) {
|
|
102166
102694
|
throw new ServiceUnavailableError(`Review bank ${identifier} needs its authoring mapping updated before it can serve review questions.`);
|
|
@@ -102176,28 +102704,60 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
102176
102704
|
this.reviewBankCache.set(`${identifier}\x00${bank.sourceContentRevision}`, catalog);
|
|
102177
102705
|
return catalog;
|
|
102178
102706
|
}
|
|
102179
|
-
async
|
|
102180
|
-
const cacheKey2 = [
|
|
102181
|
-
catalog.test.identifier,
|
|
102182
|
-
catalog.bank.sourceContentRevision,
|
|
102183
|
-
...itemIdentifiers
|
|
102184
|
-
].join("\x00");
|
|
102707
|
+
async cachedReviewSelection(input) {
|
|
102708
|
+
const cacheKey2 = [input.identifier, input.contentRevision, ...input.itemIdentifiers].join("\x00");
|
|
102185
102709
|
const cached3 = this.reviewSelectionCache.get(cacheKey2);
|
|
102186
102710
|
if (cached3) {
|
|
102187
|
-
return
|
|
102711
|
+
return cached3;
|
|
102188
102712
|
}
|
|
102189
|
-
const questions = await
|
|
102190
|
-
const assessment = await buildPlayableAssessment(
|
|
102191
|
-
assessment.contentRevision =
|
|
102713
|
+
const { test, questions } = await input.hydrate();
|
|
102714
|
+
const assessment = await buildPlayableAssessment(test, questions);
|
|
102715
|
+
assessment.contentRevision = input.contentRevision;
|
|
102192
102716
|
this.reviewSelectionCache.set(cacheKey2, assessment);
|
|
102717
|
+
return assessment;
|
|
102718
|
+
}
|
|
102719
|
+
async hydrateReviewBankSelection(catalog, itemIdentifiers) {
|
|
102720
|
+
const assessment = await this.cachedReviewSelection({
|
|
102721
|
+
identifier: catalog.test.identifier,
|
|
102722
|
+
contentRevision: catalog.bank.sourceContentRevision,
|
|
102723
|
+
itemIdentifiers,
|
|
102724
|
+
hydrate: async () => ({
|
|
102725
|
+
test: catalog.test,
|
|
102726
|
+
questions: await hydrateQtiTestQuestionSelection(this.requireClient(), catalog.references, itemIdentifiers)
|
|
102727
|
+
})
|
|
102728
|
+
});
|
|
102193
102729
|
return { assessment, bank: catalog.bank };
|
|
102194
102730
|
}
|
|
102195
|
-
|
|
102731
|
+
async loadPinnedReviewBankSource(metadata2) {
|
|
102732
|
+
const itemIdentifiers = metadata2.review.selections.map((selection) => selection.itemIdentifier);
|
|
102733
|
+
let assessment;
|
|
102734
|
+
try {
|
|
102735
|
+
assessment = await this.cachedReviewSelection({
|
|
102736
|
+
identifier: metadata2.selectedTest.identifier,
|
|
102737
|
+
contentRevision: metadata2.selectedTest.contentRevision,
|
|
102738
|
+
itemIdentifiers,
|
|
102739
|
+
hydrate: async () => {
|
|
102740
|
+
const test = await this.requireClient().qtiApi.assessmentTests.get(metadata2.selectedTest.identifier);
|
|
102741
|
+
return {
|
|
102742
|
+
test,
|
|
102743
|
+
questions: await hydrateQtiItemSelection(this.requireClient(), test, itemIdentifiers)
|
|
102744
|
+
};
|
|
102745
|
+
}
|
|
102746
|
+
});
|
|
102747
|
+
} catch (error88) {
|
|
102748
|
+
if (isApiError(error88) && error88.statusCode === 404) {
|
|
102749
|
+
throw new AssessmentRuntimeError("SELECTED_TEST_VERSION_UNAVAILABLE", `A selected review item for ${metadata2.selectedTest.identifier} is no longer available.`, {
|
|
102750
|
+
identifier: metadata2.selectedTest.identifier,
|
|
102751
|
+
expectedRevision: metadata2.selectedTest.contentRevision
|
|
102752
|
+
});
|
|
102753
|
+
}
|
|
102754
|
+
throw error88;
|
|
102755
|
+
}
|
|
102196
102756
|
return {
|
|
102197
|
-
assessment
|
|
102757
|
+
assessment,
|
|
102198
102758
|
bank: {
|
|
102199
|
-
bankIdentifier:
|
|
102200
|
-
sourceContentRevision:
|
|
102759
|
+
bankIdentifier: assessment.identifier,
|
|
102760
|
+
sourceContentRevision: metadata2.selectedTest.contentRevision,
|
|
102201
102761
|
bankRevision: metadata2.review.bankRevision,
|
|
102202
102762
|
items: metadata2.review.selections.map((selection) => ({
|
|
102203
102763
|
itemIdentifier: selection.itemIdentifier,
|
|
@@ -102221,9 +102781,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
102221
102781
|
if (metadata2.purpose !== "review") {
|
|
102222
102782
|
return this.loadAssessment(metadata2.selectedTest.identifier, metadata2.selectedTest.contentRevision);
|
|
102223
102783
|
}
|
|
102224
|
-
const
|
|
102225
|
-
const currentSource = await this.hydrateReviewBankSelection(catalog, metadata2.review.selections.map((selection) => selection.itemIdentifier));
|
|
102226
|
-
const source = this.pinnedReviewBankSource(currentSource, metadata2);
|
|
102784
|
+
const source = await this.loadPinnedReviewBankSource(metadata2);
|
|
102227
102785
|
return projectReviewAssessment(source.assessment, source.bank, metadata2.review.selections);
|
|
102228
102786
|
}
|
|
102229
102787
|
isPlatformRoutedDiagnosticMetadata(metadata2) {
|
|
@@ -102709,7 +103267,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
102709
103267
|
async projectReviewItemResponse(params, projection) {
|
|
102710
103268
|
await this.putReviewChildResponse(projection.result, projection.metadata, projection.itemIdentifier);
|
|
102711
103269
|
try {
|
|
102712
|
-
await this.
|
|
103270
|
+
await crossAssessmentAttemptLockBarrier(this.deps.assessmentRuntimeLock, this.deps.db, params.attemptId);
|
|
102713
103271
|
const latest = await this.peekAttempt(params);
|
|
102714
103272
|
if (this.isSettled(latest.result) && latest.metadata.purpose === "review") {
|
|
102715
103273
|
await this.restoreCompletedReviewChildResponses(latest.result, latest.metadata, new Set([projection.itemIdentifier]));
|
|
@@ -102723,21 +103281,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
102723
103281
|
});
|
|
102724
103282
|
}
|
|
102725
103283
|
}
|
|
102726
|
-
async crossAttemptLockBarrier(attemptId) {
|
|
102727
|
-
for (let attempt = 1;attempt <= TimebackAssessmentRuntimeService.PROJECTION_BARRIER_MAX_ATTEMPTS; attempt += 1) {
|
|
102728
|
-
try {
|
|
102729
|
-
await this.deps.assessmentRuntimeLock(this.deps.db, [assessmentRuntimeAttemptLockKey(attemptId)], async () => {
|
|
102730
|
-
return;
|
|
102731
|
-
});
|
|
102732
|
-
return;
|
|
102733
|
-
} catch (error88) {
|
|
102734
|
-
if (!(error88 instanceof ServiceUnavailableError) || attempt === TimebackAssessmentRuntimeService.PROJECTION_BARRIER_MAX_ATTEMPTS) {
|
|
102735
|
-
throw error88;
|
|
102736
|
-
}
|
|
102737
|
-
await sleep(TimebackAssessmentRuntimeService.PROJECTION_BARRIER_RETRY_DELAY_MS);
|
|
102738
|
-
}
|
|
102739
|
-
}
|
|
102740
|
-
}
|
|
102741
103284
|
async restoreCompletedReviewChildResponses(result, metadata2, changedItemIdentifiers) {
|
|
102742
103285
|
const outcomes = metadata2.completion?.itemOutcomes;
|
|
102743
103286
|
const submissionId = metadata2.submissionId;
|
|
@@ -102883,7 +103426,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
102883
103426
|
attemptId: result.sourcedId,
|
|
102884
103427
|
responseVersion: metadata2.responseVersion,
|
|
102885
103428
|
status: "completed",
|
|
102886
|
-
result:
|
|
103429
|
+
result: buildCompletedAssessmentSubmitResult({ result, metadata: metadata2 })
|
|
102887
103430
|
};
|
|
102888
103431
|
}
|
|
102889
103432
|
const assessment = await this.loadAttemptAssessment(metadata2);
|
|
@@ -102900,58 +103443,37 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
102900
103443
|
return this.snapshot(result, metadata2, assessment, integration);
|
|
102901
103444
|
}
|
|
102902
103445
|
diagnosticSnapshot(result, metadata2, assessment, integration, state) {
|
|
102903
|
-
|
|
102904
|
-
return {
|
|
103446
|
+
return buildDiagnosticAssessmentSnapshot({
|
|
102905
103447
|
attemptId: result.sourcedId,
|
|
102906
|
-
|
|
102907
|
-
|
|
102908
|
-
|
|
102909
|
-
|
|
102910
|
-
|
|
102911
|
-
selection: {
|
|
102912
|
-
kind: "platform-routed-diagnostic",
|
|
102913
|
-
purpose: "diagnostic",
|
|
102914
|
-
definitionId: metadata2.diagnostic.definitionId,
|
|
102915
|
-
diagnosticKey: metadata2.diagnostic.diagnosticKey,
|
|
102916
|
-
routingRevision: metadata2.diagnostic.routingRevision
|
|
102917
|
-
},
|
|
102918
|
-
routing: this.diagnosticRoutingSnapshot(metadata2, state),
|
|
102919
|
-
completion: null
|
|
102920
|
-
};
|
|
103448
|
+
metadata: metadata2,
|
|
103449
|
+
assessment,
|
|
103450
|
+
state,
|
|
103451
|
+
activityData: this.activityData(metadata2, assessment, integration)
|
|
103452
|
+
});
|
|
102921
103453
|
}
|
|
102922
103454
|
diagnosticRoutingSnapshot(metadata2, state) {
|
|
102923
103455
|
return projectDiagnosticRoutingSnapshot(metadata2.diagnostic.routingRevision, state);
|
|
102924
103456
|
}
|
|
102925
103457
|
snapshot(result, metadata2, assessment, integration) {
|
|
102926
|
-
|
|
102927
|
-
const activityData = this.activityData(metadata2, assessment, integration);
|
|
102928
|
-
return {
|
|
103458
|
+
return buildConventionalAssessmentSnapshot({
|
|
102929
103459
|
attemptId: result.sourcedId,
|
|
102930
|
-
|
|
102931
|
-
|
|
102932
|
-
|
|
102933
|
-
|
|
102934
|
-
assessment: assessmentPresentationForAttempt(assessment, result.sourcedId),
|
|
102935
|
-
responses: metadata2.responses,
|
|
102936
|
-
itemSubmissions: metadata2.itemSubmissions,
|
|
102937
|
-
score: null,
|
|
102938
|
-
selection
|
|
102939
|
-
};
|
|
103460
|
+
metadata: metadata2,
|
|
103461
|
+
assessment,
|
|
103462
|
+
activityData: this.activityData(metadata2, assessment, integration)
|
|
103463
|
+
});
|
|
102940
103464
|
}
|
|
102941
103465
|
activityData(metadata2, assessment, integration) {
|
|
102942
103466
|
const course = this.trackableCourse(integration, "assessment.activity_tracking_invalid_course_metadata");
|
|
102943
|
-
|
|
102944
|
-
return;
|
|
102945
|
-
}
|
|
102946
|
-
return {
|
|
103467
|
+
return buildAssessmentActivityData({
|
|
102947
103468
|
activityId: metadata2.activityId,
|
|
102948
103469
|
activityName: assessment.title,
|
|
102949
|
-
|
|
102950
|
-
|
|
102951
|
-
};
|
|
103470
|
+
courseId: integration.courseId,
|
|
103471
|
+
course
|
|
103472
|
+
});
|
|
102952
103473
|
}
|
|
102953
103474
|
trackableCourse(integration, event, attributes2 = {}) {
|
|
102954
|
-
|
|
103475
|
+
const course = assessmentTrackableCourse(integration);
|
|
103476
|
+
if (!course) {
|
|
102955
103477
|
addEvent(event, {
|
|
102956
103478
|
...attributes2,
|
|
102957
103479
|
"app.timeback.integration_id": integration.id,
|
|
@@ -102960,27 +103482,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
102960
103482
|
});
|
|
102961
103483
|
return null;
|
|
102962
103484
|
}
|
|
102963
|
-
return
|
|
102964
|
-
}
|
|
102965
|
-
selectionContext(metadata2) {
|
|
102966
|
-
if (metadata2.purpose === "review") {
|
|
102967
|
-
return {
|
|
102968
|
-
kind: "standards-review",
|
|
102969
|
-
purpose: metadata2.purpose,
|
|
102970
|
-
standards: [...metadata2.review.standards],
|
|
102971
|
-
candidateItemsPerStandard: metadata2.review.candidateItemsPerStandard,
|
|
102972
|
-
selections: [...metadata2.review.selections],
|
|
102973
|
-
fulfillment: metadata2.review.fulfillment
|
|
102974
|
-
};
|
|
102975
|
-
}
|
|
102976
|
-
if (metadata2.purpose === "mastery") {
|
|
102977
|
-
return {
|
|
102978
|
-
kind: "standard-quiz",
|
|
102979
|
-
purpose: metadata2.purpose,
|
|
102980
|
-
standard: metadata2.mastery.standard
|
|
102981
|
-
};
|
|
102982
|
-
}
|
|
102983
|
-
return { kind: "fixed-test", purpose: metadata2.purpose };
|
|
103485
|
+
return course;
|
|
102984
103486
|
}
|
|
102985
103487
|
submittedResult(result, metadata2) {
|
|
102986
103488
|
const response = buildAssessmentSubmitResult({
|
|
@@ -102994,29 +103496,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
102994
103496
|
}
|
|
102995
103497
|
return response;
|
|
102996
103498
|
}
|
|
102997
|
-
completedResult(result, metadata2) {
|
|
102998
|
-
const xpAwarded = this.xpFromResult(result);
|
|
102999
|
-
if (xpAwarded === null) {
|
|
103000
|
-
throw new GoneError("This completed assessment is missing its recorded XP.", {
|
|
103001
|
-
attemptId: result.sourcedId
|
|
103002
|
-
});
|
|
103003
|
-
}
|
|
103004
|
-
const response = buildCompletedAssessmentSubmitResult({
|
|
103005
|
-
attemptId: result.sourcedId,
|
|
103006
|
-
metadata: metadata2,
|
|
103007
|
-
xpAwarded
|
|
103008
|
-
});
|
|
103009
|
-
if (!response) {
|
|
103010
|
-
throw new GoneError("This completed assessment has incomplete persisted results.", {
|
|
103011
|
-
attemptId: result.sourcedId
|
|
103012
|
-
});
|
|
103013
|
-
}
|
|
103014
|
-
return response;
|
|
103015
|
-
}
|
|
103016
|
-
xpFromResult(result) {
|
|
103017
|
-
const xp = result.metadata?.xp;
|
|
103018
|
-
return typeof xp === "number" && Number.isFinite(xp) && xp >= 0 ? xp : null;
|
|
103019
|
-
}
|
|
103020
103499
|
awardAgeMs(scoreDate) {
|
|
103021
103500
|
const submittedAt = Date.parse(scoreDate);
|
|
103022
103501
|
return Number.isFinite(submittedAt) ? Math.max(0, Date.now() - submittedAt) : -1;
|
|
@@ -103143,7 +103622,9 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
103143
103622
|
attemptNumber: attempt.metadata.attemptNumber,
|
|
103144
103623
|
totalQuestions: input.totalQuestions,
|
|
103145
103624
|
correctQuestions: input.correctQuestions,
|
|
103146
|
-
xpAwarded: input.xpAwarded,
|
|
103625
|
+
xpAwarded: input.award.xpAwarded,
|
|
103626
|
+
...input.award.masteredUnitsApplied !== 0 ? { masteredUnits: input.award.masteredUnitsApplied } : {},
|
|
103627
|
+
...input.award.pctCompleteApp !== undefined ? { pctCompleteApp: input.award.pctCompleteApp } : {},
|
|
103147
103628
|
submittedAt: attempt.result.scoreDate,
|
|
103148
103629
|
eventId,
|
|
103149
103630
|
...session2 ? { resumeId: session2.resumeId } : {}
|
|
@@ -103268,6 +103749,9 @@ function assertAssessmentImportCourseMatches(manifest, integration) {
|
|
|
103268
103749
|
throw new ValidationError(`This manifest is for ${manifest.subject} grade ${manifest.grade}, not ${integration.subject} grade ${integration.grade}.`);
|
|
103269
103750
|
}
|
|
103270
103751
|
}
|
|
103752
|
+
function assessmentImportsAffectReviewBank(targetStatus, entries) {
|
|
103753
|
+
return targetStatus === "live" && entries.some((entry) => entry.purpose === "mastery");
|
|
103754
|
+
}
|
|
103271
103755
|
function associationMetadataMatches(row, entry) {
|
|
103272
103756
|
return row.purpose === entry.purpose && (row.standardFramework ?? undefined) === entry.standard?.framework && (row.standardIdentifier ?? undefined) === entry.standard?.identifier;
|
|
103273
103757
|
}
|
|
@@ -103317,18 +103801,6 @@ function attachedAssessmentImportMessage(targetStatus, editable) {
|
|
|
103317
103801
|
}
|
|
103318
103802
|
return editable ? "Attached as a draft." : "Attached as a read-only draft owned by another app.";
|
|
103319
103803
|
}
|
|
103320
|
-
function liveReviewAssessmentImportFailures(targetStatus, candidates, existingRows) {
|
|
103321
|
-
if (targetStatus === "draft") {
|
|
103322
|
-
return new Map;
|
|
103323
|
-
}
|
|
103324
|
-
const reviews = candidates.filter((candidate) => candidate.purpose === "review");
|
|
103325
|
-
const hasExistingLiveReview = existingRows.some((row) => row.purpose === "review" && row.status === "live");
|
|
103326
|
-
if (reviews.length <= 1 && !hasExistingLiveReview) {
|
|
103327
|
-
return new Map;
|
|
103328
|
-
}
|
|
103329
|
-
const message = hasExistingLiveReview ? "Another review assessment is already live for this course." : "Only one review assessment can be imported live at a time.";
|
|
103330
|
-
return new Map(reviews.map((review) => [review.qtiTestIdentifier, message]));
|
|
103331
|
-
}
|
|
103332
103804
|
var init_timeback_assessment_import_util = __esm(() => {
|
|
103333
103805
|
init_errors2();
|
|
103334
103806
|
init_timeback_qti_authoring_util();
|
|
@@ -103400,7 +103872,9 @@ class TimebackAssessmentsService {
|
|
|
103400
103872
|
const rows = await this.deps.db.query.gameTimebackAssessmentTests.findMany({
|
|
103401
103873
|
where: eq(gameTimebackAssessmentTests.integrationId, integrationId)
|
|
103402
103874
|
});
|
|
103403
|
-
const
|
|
103875
|
+
const sinkIdentifier = reviewBankSinkIdentifier(integrationId);
|
|
103876
|
+
const visibleRows = rows.filter((row) => row.purpose !== "review" || row.qtiTestIdentifier === sinkIdentifier);
|
|
103877
|
+
const assessments2 = await runWithConcurrency(visibleRows, QTI_HYDRATION_CONCURRENCY, async (row) => {
|
|
103404
103878
|
try {
|
|
103405
103879
|
const test = await client2.qtiApi.assessmentTests.get(row.qtiTestIdentifier);
|
|
103406
103880
|
return {
|
|
@@ -103408,7 +103882,7 @@ class TimebackAssessmentsService {
|
|
|
103408
103882
|
title: test.title,
|
|
103409
103883
|
questionCount: countQtiTestItems(test),
|
|
103410
103884
|
available: true,
|
|
103411
|
-
editable: isQtiTestOwnedByGame(test, ownership.gameSlug)
|
|
103885
|
+
editable: row.purpose !== "review" && isQtiTestOwnedByGame(test, ownership.gameSlug)
|
|
103412
103886
|
};
|
|
103413
103887
|
} catch (error88) {
|
|
103414
103888
|
addEvent("assessment.qti_fetch_failed", {
|
|
@@ -103428,8 +103902,11 @@ class TimebackAssessmentsService {
|
|
|
103428
103902
|
return assessments2.toSorted((a, b) => a.title.localeCompare(b.title));
|
|
103429
103903
|
}
|
|
103430
103904
|
async createAssessment(integrationId, input) {
|
|
103431
|
-
if (input.purpose === "diagnostic") {
|
|
103432
|
-
throw new ValidationError("Adaptive diagnostics must be created by importing assessment JSON with a routing sidecar.");
|
|
103905
|
+
if (input.purpose === "diagnostic" || input.purpose === "review") {
|
|
103906
|
+
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.");
|
|
103907
|
+
}
|
|
103908
|
+
if (input.assessmentKey === reviewBankSinkIdentifier(integrationId)) {
|
|
103909
|
+
throw new ValidationError("This assessment key is reserved for the review bank.");
|
|
103433
103910
|
}
|
|
103434
103911
|
const client2 = this.requireClient();
|
|
103435
103912
|
const ownership = await this.requireQtiTestOwnershipContext(integrationId);
|
|
@@ -103505,24 +103982,41 @@ class TimebackAssessmentsService {
|
|
|
103505
103982
|
index: index2,
|
|
103506
103983
|
standard: this.requirePurposeStandard(assessment.purpose, assessment.standard)
|
|
103507
103984
|
}));
|
|
103508
|
-
const
|
|
103509
|
-
|
|
103510
|
-
|
|
103511
|
-
|
|
103512
|
-
const existingByIdentifier = new Map(existingRows.map((row) => [row.qtiTestIdentifier, row]));
|
|
103985
|
+
const sinkIdentifier = reviewBankSinkIdentifier(integrationId);
|
|
103986
|
+
if (entries.some((entry) => entry.assessmentKey === sinkIdentifier || entry.qtiTestIdentifier === sinkIdentifier || entry.purpose === "review")) {
|
|
103987
|
+
throw new ValidationError("The review bank is system-managed and cannot be attached manually.");
|
|
103988
|
+
}
|
|
103513
103989
|
const validated = await this.validateAssessmentImportEntries(client2, entries, manifest.targetStatus);
|
|
103514
103990
|
const results = [];
|
|
103515
|
-
const
|
|
103516
|
-
|
|
103517
|
-
existingByKey,
|
|
103518
|
-
existingByIdentifier,
|
|
103519
|
-
|
|
103520
|
-
|
|
103521
|
-
|
|
103522
|
-
|
|
103523
|
-
|
|
103524
|
-
|
|
103525
|
-
|
|
103991
|
+
const affectsReviewBank = assessmentImportsAffectReviewBank(manifest.targetStatus, entries);
|
|
103992
|
+
const planAndInsert = async (db2, existingRows) => {
|
|
103993
|
+
const existingByKey = new Map(existingRows.flatMap((row) => row.assessmentKey ? [[row.assessmentKey, row]] : []));
|
|
103994
|
+
const existingByIdentifier = new Map(existingRows.map((row) => [row.qtiTestIdentifier, row]));
|
|
103995
|
+
const pendingInserts = this.planAssessmentImports({
|
|
103996
|
+
validated,
|
|
103997
|
+
existingByKey,
|
|
103998
|
+
existingByIdentifier,
|
|
103999
|
+
gameSlug,
|
|
104000
|
+
results
|
|
104001
|
+
});
|
|
104002
|
+
await this.insertImportedAssessments({
|
|
104003
|
+
db: db2,
|
|
104004
|
+
integrationId,
|
|
104005
|
+
pending: pendingInserts,
|
|
104006
|
+
targetStatus: manifest.targetStatus,
|
|
104007
|
+
gameSlug,
|
|
104008
|
+
results,
|
|
104009
|
+
abortOnError: affectsReviewBank
|
|
104010
|
+
});
|
|
104011
|
+
};
|
|
104012
|
+
if (affectsReviewBank) {
|
|
104013
|
+
await this.withLockedIntegrationAssessmentRows(integrationId, async (tx, lockedAssociations) => planAndInsert(tx, lockedAssociations));
|
|
104014
|
+
} else {
|
|
104015
|
+
const existingRows = await this.deps.db.query.gameTimebackAssessmentTests.findMany({
|
|
104016
|
+
where: eq(gameTimebackAssessmentTests.integrationId, integrationId)
|
|
104017
|
+
});
|
|
104018
|
+
await planAndInsert(this.deps.db, existingRows);
|
|
104019
|
+
}
|
|
103526
104020
|
setAttribute("app.assessment.operation", "bulk_attach_existing");
|
|
103527
104021
|
setAttribute("app.assessment.bulk_attach.target_status", manifest.targetStatus);
|
|
103528
104022
|
setAttribute("app.assessment.bulk_attach.created", results.filter((result) => result.status === "created").length);
|
|
@@ -103535,11 +104029,6 @@ class TimebackAssessmentsService {
|
|
|
103535
104029
|
assertManagedAssessmentIdentity(loaded.test, entry.assessmentKey);
|
|
103536
104030
|
assertAssessmentHasQuestions(loaded.questions.questions);
|
|
103537
104031
|
assertPlayableAssessmentImportQuestions(targetStatus, loaded.questions.questions.map(({ question }) => question));
|
|
103538
|
-
if (entry.purpose === "review") {
|
|
103539
|
-
assertReviewAssessmentHasStandards(loaded.questions.questions.map(({ question }) => reviewRoutingStandardRefsFromMetadata(question.metadata, {
|
|
103540
|
-
customOnly: true
|
|
103541
|
-
}).length));
|
|
103542
|
-
}
|
|
103543
104032
|
return { entry, test: loaded.test };
|
|
103544
104033
|
} catch (error88) {
|
|
103545
104034
|
return { entry, error: error88 };
|
|
@@ -103553,7 +104042,6 @@ class TimebackAssessmentsService {
|
|
|
103553
104042
|
...test ? { title: test.title } : {},
|
|
103554
104043
|
purpose: entry.purpose,
|
|
103555
104044
|
...entry.standard ? { standard: entry.standard } : {},
|
|
103556
|
-
sourceSortOrder: entry.sortOrder,
|
|
103557
104045
|
...test && gameSlug ? { editable: isQtiTestOwnedByGame(test, gameSlug) } : {}
|
|
103558
104046
|
};
|
|
103559
104047
|
}
|
|
@@ -103585,25 +104073,13 @@ class TimebackAssessmentsService {
|
|
|
103585
104073
|
}
|
|
103586
104074
|
return pendingInserts;
|
|
103587
104075
|
}
|
|
103588
|
-
|
|
103589
|
-
for (const { entry, test } of pending) {
|
|
103590
|
-
const message = failures.get(entry.qtiTestIdentifier);
|
|
103591
|
-
if (message) {
|
|
103592
|
-
results[entry.index] = {
|
|
103593
|
-
...this.associationImportResultBase(entry, test, gameSlug),
|
|
103594
|
-
status: "failed",
|
|
103595
|
-
message
|
|
103596
|
-
};
|
|
103597
|
-
}
|
|
103598
|
-
}
|
|
103599
|
-
}
|
|
103600
|
-
async concurrentAssessmentImportDecision(integrationId, entry) {
|
|
104076
|
+
async concurrentAssessmentImportDecision(db2, integrationId, entry) {
|
|
103601
104077
|
try {
|
|
103602
104078
|
const [byKey, byIdentifier] = await Promise.all([
|
|
103603
|
-
|
|
104079
|
+
db2.query.gameTimebackAssessmentTests.findFirst({
|
|
103604
104080
|
where: and(eq(gameTimebackAssessmentTests.integrationId, integrationId), eq(gameTimebackAssessmentTests.assessmentKey, entry.assessmentKey))
|
|
103605
104081
|
}),
|
|
103606
|
-
|
|
104082
|
+
db2.query.gameTimebackAssessmentTests.findFirst({
|
|
103607
104083
|
where: and(eq(gameTimebackAssessmentTests.integrationId, integrationId), eq(gameTimebackAssessmentTests.qtiTestIdentifier, entry.qtiTestIdentifier))
|
|
103608
104084
|
})
|
|
103609
104085
|
]);
|
|
@@ -103615,39 +104091,52 @@ class TimebackAssessmentsService {
|
|
|
103615
104091
|
return null;
|
|
103616
104092
|
}
|
|
103617
104093
|
}
|
|
103618
|
-
async insertImportedAssessments(
|
|
103619
|
-
for (const { entry, test } of pending) {
|
|
103620
|
-
const base = this.associationImportResultBase(entry, test, gameSlug);
|
|
104094
|
+
async insertImportedAssessments(input) {
|
|
104095
|
+
for (const { entry, test } of input.pending) {
|
|
104096
|
+
const base = this.associationImportResultBase(entry, test, input.gameSlug);
|
|
103621
104097
|
try {
|
|
103622
|
-
const [row] = await
|
|
103623
|
-
integrationId,
|
|
104098
|
+
const [row] = await input.db.insert(gameTimebackAssessmentTests).values({
|
|
104099
|
+
integrationId: input.integrationId,
|
|
103624
104100
|
assessmentKey: entry.assessmentKey,
|
|
103625
104101
|
qtiTestIdentifier: entry.qtiTestIdentifier,
|
|
103626
104102
|
purpose: entry.purpose,
|
|
103627
|
-
status: targetStatus,
|
|
103628
|
-
sortOrder: entry.sortOrder,
|
|
104103
|
+
status: input.targetStatus,
|
|
103629
104104
|
standardFramework: entry.standard?.framework,
|
|
103630
104105
|
standardIdentifier: entry.standard?.identifier
|
|
103631
|
-
}).returning();
|
|
104106
|
+
}).onConflictDoNothing().returning();
|
|
103632
104107
|
if (!row) {
|
|
103633
|
-
|
|
104108
|
+
const concurrent = await this.concurrentAssessmentImportDecision(input.db, input.integrationId, entry);
|
|
104109
|
+
if (concurrent?.decision.kind === "skip" || concurrent?.decision.kind === "fail") {
|
|
104110
|
+
input.results[entry.index] = {
|
|
104111
|
+
...base,
|
|
104112
|
+
status: concurrent.decision.status,
|
|
104113
|
+
...concurrent.byKey ? { association: this.associationImportSummary(concurrent.byKey) } : {},
|
|
104114
|
+
message: concurrent.decision.message
|
|
104115
|
+
};
|
|
104116
|
+
} else {
|
|
104117
|
+
throw new Error("Assessment association insert conflicted");
|
|
104118
|
+
}
|
|
104119
|
+
} else {
|
|
104120
|
+
input.results[entry.index] = {
|
|
104121
|
+
...base,
|
|
104122
|
+
status: "created",
|
|
104123
|
+
association: this.associationImportSummary(row),
|
|
104124
|
+
message: attachedAssessmentImportMessage(input.targetStatus, base.editable ?? false)
|
|
104125
|
+
};
|
|
103634
104126
|
}
|
|
103635
|
-
results[entry.index] = {
|
|
103636
|
-
...base,
|
|
103637
|
-
status: "created",
|
|
103638
|
-
association: this.associationImportSummary(row),
|
|
103639
|
-
message: attachedAssessmentImportMessage(targetStatus, base.editable ?? false)
|
|
103640
|
-
};
|
|
103641
104127
|
} catch (error88) {
|
|
103642
|
-
|
|
104128
|
+
if (input.abortOnError) {
|
|
104129
|
+
throw error88;
|
|
104130
|
+
}
|
|
104131
|
+
input.results[entry.index] = {
|
|
103643
104132
|
...base,
|
|
103644
104133
|
status: "failed",
|
|
103645
104134
|
message: `Association attach failed: ${errorMessage(error88)}`
|
|
103646
104135
|
};
|
|
103647
104136
|
if (isUniqueViolation(error88)) {
|
|
103648
|
-
const concurrent = await this.concurrentAssessmentImportDecision(integrationId, entry);
|
|
104137
|
+
const concurrent = await this.concurrentAssessmentImportDecision(input.db, input.integrationId, entry);
|
|
103649
104138
|
if (concurrent?.decision.kind === "skip" || concurrent?.decision.kind === "fail") {
|
|
103650
|
-
results[entry.index] = {
|
|
104139
|
+
input.results[entry.index] = {
|
|
103651
104140
|
...base,
|
|
103652
104141
|
status: concurrent.decision.status,
|
|
103653
104142
|
...concurrent.byKey ? { association: this.associationImportSummary(concurrent.byKey) } : {},
|
|
@@ -103661,6 +104150,12 @@ class TimebackAssessmentsService {
|
|
|
103661
104150
|
async updateAssessment(integrationId, qtiTestIdentifier, input) {
|
|
103662
104151
|
try {
|
|
103663
104152
|
return await this.withLockedAssessmentAssociations(integrationId, qtiTestIdentifier, async (row, tx, associations) => {
|
|
104153
|
+
if (row.purpose === "review" || qtiTestIdentifier === reviewBankSinkIdentifier(integrationId)) {
|
|
104154
|
+
throw new ValidationError("The review bank is system-managed and cannot be edited.");
|
|
104155
|
+
}
|
|
104156
|
+
if (input.purpose === "review") {
|
|
104157
|
+
throw new ValidationError("Assessment purpose cannot be changed to the system-managed review bank.");
|
|
104158
|
+
}
|
|
103664
104159
|
const nextPurpose = input.purpose ?? row.purpose;
|
|
103665
104160
|
assertPurposeChangeDraft(row, nextPurpose);
|
|
103666
104161
|
const requestedStandard = input.standard ? this.canonicalAssessmentStandard(input.standard) : undefined;
|
|
@@ -103679,19 +104174,10 @@ class TimebackAssessmentsService {
|
|
|
103679
104174
|
...associationUpdates,
|
|
103680
104175
|
...diagnosticChanges.updates
|
|
103681
104176
|
};
|
|
103682
|
-
const nextStatus = input.status ?? row.status;
|
|
103683
104177
|
const publishing = input.status !== undefined && isAssessmentPublicationTransition(row.status, input.status);
|
|
103684
|
-
const activatingReview = nextPurpose === "review" && nextStatus === "live" && (row.purpose !== "review" || row.status !== "live");
|
|
103685
104178
|
if (input.status !== undefined) {
|
|
103686
104179
|
validateAssessmentStatusTransition(row.status, input.status);
|
|
103687
104180
|
}
|
|
103688
|
-
if (!publishing && !activatingReview && nextPurpose === "review" && row.purpose !== "review") {
|
|
103689
|
-
const ownership = await this.requireQtiTestOwnershipContext(integrationId, tx);
|
|
103690
|
-
await this.rebuildReviewMapping(this.requireClient(), qtiTestIdentifier, {
|
|
103691
|
-
kind: "owned",
|
|
103692
|
-
gameSlug: ownership.gameSlug
|
|
103693
|
-
});
|
|
103694
|
-
}
|
|
103695
104181
|
if (input.title !== undefined) {
|
|
103696
104182
|
assertDraftAssessment(row);
|
|
103697
104183
|
assertAllAssessmentAssociationsDraft(associations);
|
|
@@ -103701,7 +104187,7 @@ class TimebackAssessmentsService {
|
|
|
103701
104187
|
assertQtiTestOwnedByGame(test, ownership.gameSlug);
|
|
103702
104188
|
await client2.qtiApi.assessmentTests.update(qtiTestIdentifier, buildQtiTestUpdateInput(test, input.title));
|
|
103703
104189
|
}
|
|
103704
|
-
if (publishing
|
|
104190
|
+
if (publishing) {
|
|
103705
104191
|
if (nextPurpose === "diagnostic" && !nextDiagnostic) {
|
|
103706
104192
|
throw new ValidationError("Platform-routed diagnostics require a diagnostic key and routing manifest before publication.");
|
|
103707
104193
|
}
|
|
@@ -103711,8 +104197,7 @@ class TimebackAssessmentsService {
|
|
|
103711
104197
|
updates.diagnosticKey = nextDiagnostic.diagnosticKey;
|
|
103712
104198
|
updates.diagnosticRoutingManifest = nextDiagnostic.routingManifest;
|
|
103713
104199
|
} else {
|
|
103714
|
-
|
|
103715
|
-
await this.validateAssessmentHasQuestions(loaded, reviewOwnership?.gameSlug);
|
|
104200
|
+
await this.validateAssessmentHasQuestions(loaded);
|
|
103716
104201
|
}
|
|
103717
104202
|
if (publishing) {
|
|
103718
104203
|
const publication = await this.publishManagedAssessment(row, nextPurpose, nextDiagnostic?.routingManifest ?? null, loaded);
|
|
@@ -103735,20 +104220,20 @@ class TimebackAssessmentsService {
|
|
|
103735
104220
|
if (databaseConstraintName(error88) === "game_timeback_assessment_tests_one_live_diagnostic_key_idx") {
|
|
103736
104221
|
throw new ValidationError("Only one live revision of a diagnostic key is allowed. Archive the current revision before publishing another.");
|
|
103737
104222
|
}
|
|
103738
|
-
if (databaseConstraintName(error88) === "game_timeback_assessment_tests_one_live_review_idx") {
|
|
103739
|
-
throw new ValidationError("Only one live review assessment is allowed. Archive the current review bank before publishing another.");
|
|
103740
|
-
}
|
|
103741
104223
|
}
|
|
103742
104224
|
throw error88;
|
|
103743
104225
|
}
|
|
103744
104226
|
}
|
|
103745
104227
|
async removeAssessment(integrationId, qtiTestIdentifier) {
|
|
103746
104228
|
return this.withLockedAssessmentAssociations(integrationId, qtiTestIdentifier, async (row, tx) => {
|
|
104229
|
+
if (qtiTestIdentifier === reviewBankSinkIdentifier(integrationId)) {
|
|
104230
|
+
throw new ValidationError("The review bank is permanent and cannot be removed or archived.");
|
|
104231
|
+
}
|
|
103747
104232
|
const plan = planAssessmentRemoval(row.status);
|
|
103748
104233
|
if (plan.kind === "delete") {
|
|
103749
104234
|
await tx.delete(gameTimebackAssessmentTests).where(eq(gameTimebackAssessmentTests.id, row.id));
|
|
103750
104235
|
} else if (plan.kind === "archive") {
|
|
103751
|
-
await tx.update(gameTimebackAssessmentTests).set({ status: "archived",
|
|
104236
|
+
await tx.update(gameTimebackAssessmentTests).set({ status: "archived", updatedAt: new Date }).where(eq(gameTimebackAssessmentTests.id, row.id));
|
|
103752
104237
|
}
|
|
103753
104238
|
if (plan.kind !== "none") {
|
|
103754
104239
|
setAttribute("app.assessment.operation", plan.operation);
|
|
@@ -103756,24 +104241,6 @@ class TimebackAssessmentsService {
|
|
|
103756
104241
|
return { action: plan.action };
|
|
103757
104242
|
});
|
|
103758
104243
|
}
|
|
103759
|
-
async reorderAssessments(integrationId, purpose, testIdentifiers) {
|
|
103760
|
-
await this.requireIntegration(integrationId);
|
|
103761
|
-
validateUniqueAssessmentIdentifiers(testIdentifiers);
|
|
103762
|
-
const updatedAt = new Date;
|
|
103763
|
-
await this.deps.db.transaction(async (tx) => {
|
|
103764
|
-
const liveRows = await tx.query.gameTimebackAssessmentTests.findMany({
|
|
103765
|
-
where: and(eq(gameTimebackAssessmentTests.integrationId, integrationId), eq(gameTimebackAssessmentTests.purpose, purpose), eq(gameTimebackAssessmentTests.status, "live"))
|
|
103766
|
-
});
|
|
103767
|
-
const orderedRows = orderLiveAssessmentRows(liveRows, purpose, testIdentifiers);
|
|
103768
|
-
const positionByRowId = new Map(orderedRows.map((row, index2) => [row.id, index2 + 1]));
|
|
103769
|
-
for (const row of lockOrderAssessmentRows(orderedRows)) {
|
|
103770
|
-
const testIdentifier = row.qtiTestIdentifier;
|
|
103771
|
-
const [updatedRow] = await tx.update(gameTimebackAssessmentTests).set({ sortOrder: positionByRowId.get(row.id), updatedAt }).where(and(eq(gameTimebackAssessmentTests.id, row.id), eq(gameTimebackAssessmentTests.integrationId, integrationId), eq(gameTimebackAssessmentTests.qtiTestIdentifier, testIdentifier), eq(gameTimebackAssessmentTests.purpose, purpose), eq(gameTimebackAssessmentTests.status, "live"))).returning({ id: gameTimebackAssessmentTests.id });
|
|
103772
|
-
assertAssessmentOrderUpdateSucceeded(updatedRow);
|
|
103773
|
-
}
|
|
103774
|
-
});
|
|
103775
|
-
setAttribute("app.assessment.operation", "reorder_live_assessments");
|
|
103776
|
-
}
|
|
103777
104244
|
async listQuestions(integrationId, qtiTestIdentifier) {
|
|
103778
104245
|
const client2 = this.requireClient();
|
|
103779
104246
|
await this.requireAssessmentRow(integrationId, qtiTestIdentifier);
|
|
@@ -103814,19 +104281,96 @@ class TimebackAssessmentsService {
|
|
|
103814
104281
|
});
|
|
103815
104282
|
return { ...result, questions };
|
|
103816
104283
|
}
|
|
103817
|
-
async
|
|
103818
|
-
|
|
103819
|
-
|
|
103820
|
-
|
|
104284
|
+
async synchronizeReviewBank(integrationId) {
|
|
104285
|
+
const client2 = this.requireClient();
|
|
104286
|
+
await this.requireIntegration(integrationId);
|
|
104287
|
+
const sinkIdentifier = reviewBankSinkIdentifier(integrationId);
|
|
104288
|
+
for (let attempt = 0;attempt < REVIEW_BANK_SYNC_ATTEMPTS; attempt++) {
|
|
104289
|
+
const associations = await this.deps.db.query.gameTimebackAssessmentTests.findMany({
|
|
104290
|
+
where: eq(gameTimebackAssessmentTests.integrationId, integrationId)
|
|
104291
|
+
});
|
|
104292
|
+
const masterySources = this.reviewBankMasterySources(associations);
|
|
104293
|
+
const contributorFingerprint = await reviewBankContributorFingerprint(masterySources);
|
|
104294
|
+
const existingSink = await this.findReviewBankSink(client2, integrationId);
|
|
104295
|
+
if (existingSink) {
|
|
104296
|
+
assertReviewBankSinkOwnership(existingSink, integrationId);
|
|
104297
|
+
}
|
|
104298
|
+
let unchangedResult = null;
|
|
104299
|
+
if (existingSink) {
|
|
104300
|
+
try {
|
|
104301
|
+
unchangedResult = await unchangedReviewBankSynchronizationResult({
|
|
104302
|
+
sink: existingSink,
|
|
104303
|
+
integrationId,
|
|
104304
|
+
contributorFingerprint,
|
|
104305
|
+
contributorCount: masterySources.length
|
|
104306
|
+
});
|
|
104307
|
+
} catch (error88) {
|
|
104308
|
+
addEvent("assessment.review_mapping_noop_validation_failed", {
|
|
104309
|
+
"app.assessment.qti_test_identifier": existingSink.identifier,
|
|
104310
|
+
"exception.type": errorType(error88),
|
|
104311
|
+
"app.error.message": errorMessage(error88)
|
|
104312
|
+
});
|
|
104313
|
+
}
|
|
103821
104314
|
}
|
|
103822
|
-
|
|
103823
|
-
|
|
103824
|
-
|
|
104315
|
+
let plan = null;
|
|
104316
|
+
let addedItemCount = 0;
|
|
104317
|
+
let removedItemCount = 0;
|
|
104318
|
+
let catalogUnchanged = unchangedResult !== null;
|
|
104319
|
+
if (!unchangedResult) {
|
|
104320
|
+
const contributors = await runWithConcurrency(masterySources, QTI_HYDRATION_CONCURRENCY, async (source) => {
|
|
104321
|
+
const test = await client2.qtiApi.assessmentTests.get(source.qtiTestIdentifier);
|
|
104322
|
+
assertManagedAssessmentIdentity(test, source.assessmentKey);
|
|
104323
|
+
return { ...source, test };
|
|
104324
|
+
});
|
|
104325
|
+
plan = await prepareReviewBankSynchronization({
|
|
104326
|
+
bankIdentifier: sinkIdentifier,
|
|
104327
|
+
contributors,
|
|
104328
|
+
contributorFingerprint
|
|
104329
|
+
});
|
|
104330
|
+
const diff = reviewBankSynchronizationDiff({ existingSink, plan });
|
|
104331
|
+
addedItemCount = diff.addedItemCount;
|
|
104332
|
+
removedItemCount = diff.removedItemCount;
|
|
104333
|
+
catalogUnchanged = diff.unchanged;
|
|
104334
|
+
}
|
|
104335
|
+
const committed = await this.withLockedIntegrationAssessmentRows(integrationId, async (tx, lockedAssociations) => {
|
|
104336
|
+
const lockedSources = this.reviewBankMasterySources(lockedAssociations);
|
|
104337
|
+
const lockedFingerprint = await reviewBankContributorFingerprint(lockedSources);
|
|
104338
|
+
if (lockedFingerprint !== contributorFingerprint) {
|
|
104339
|
+
return null;
|
|
104340
|
+
}
|
|
104341
|
+
const associationChanged = await this.installReviewBankAssociation({
|
|
104342
|
+
tx,
|
|
104343
|
+
integrationId,
|
|
104344
|
+
associations: lockedAssociations,
|
|
104345
|
+
sinkIdentifier,
|
|
104346
|
+
beforeInstall: async () => {
|
|
104347
|
+
if (!catalogUnchanged) {
|
|
104348
|
+
await this.writeReviewBankSink({
|
|
104349
|
+
client: client2,
|
|
104350
|
+
integrationId,
|
|
104351
|
+
existingSink,
|
|
104352
|
+
plan,
|
|
104353
|
+
metadata: reviewBankSinkMetadata(integrationId, plan)
|
|
104354
|
+
});
|
|
104355
|
+
}
|
|
104356
|
+
}
|
|
104357
|
+
});
|
|
104358
|
+
const unchanged = catalogUnchanged && !associationChanged;
|
|
104359
|
+
const result = unchangedResult ? { ...unchangedResult, unchanged } : {
|
|
104360
|
+
qtiTestIdentifier: sinkIdentifier,
|
|
104361
|
+
...plan.summary,
|
|
104362
|
+
addedItemCount,
|
|
104363
|
+
removedItemCount,
|
|
104364
|
+
unchanged,
|
|
104365
|
+
warnings: plan.warnings
|
|
104366
|
+
};
|
|
104367
|
+
return this.reviewMappingResult(result);
|
|
103825
104368
|
});
|
|
103826
|
-
|
|
103827
|
-
|
|
103828
|
-
|
|
103829
|
-
}
|
|
104369
|
+
if (committed) {
|
|
104370
|
+
return committed;
|
|
104371
|
+
}
|
|
104372
|
+
}
|
|
104373
|
+
throw new ValidationError("Mastery assessments changed while the review bank was synchronizing. Try again.");
|
|
103830
104374
|
}
|
|
103831
104375
|
async listQuestionLibrary(integrationId, params) {
|
|
103832
104376
|
const client2 = this.requireClient();
|
|
@@ -103846,15 +104390,18 @@ class TimebackAssessmentsService {
|
|
|
103846
104390
|
purpose,
|
|
103847
104391
|
standard: standardInput
|
|
103848
104392
|
} = input;
|
|
103849
|
-
if (purpose === "diagnostic") {
|
|
103850
|
-
throw new ValidationError("Adaptive diagnostics must be created by importing assessment JSON with a routing sidecar.");
|
|
104393
|
+
if (purpose === "diagnostic" || purpose === "review") {
|
|
104394
|
+
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.");
|
|
104395
|
+
}
|
|
104396
|
+
if (assessmentKey === reviewBankSinkIdentifier(integrationId) || targetTestIdentifier === reviewBankSinkIdentifier(integrationId)) {
|
|
104397
|
+
throw new ValidationError("This assessment identity is reserved for the review bank.");
|
|
103851
104398
|
}
|
|
103852
104399
|
const client2 = this.requireClient();
|
|
103853
104400
|
const ownership = await this.requireQtiTestOwnershipContext(integrationId);
|
|
103854
104401
|
const { integration } = ownership;
|
|
103855
104402
|
const standard = this.requirePurposeStandard(purpose, standardInput);
|
|
103856
104403
|
const source = await client2.qtiApi.assessmentTests.get(sourceTestIdentifier);
|
|
103857
|
-
const itemPlan = buildQtiAssessmentItemCopyPlan(source, targetTestIdentifier, (identifier) =>
|
|
104404
|
+
const itemPlan = buildQtiAssessmentItemCopyPlan(source, targetTestIdentifier, (identifier) => qtiItemHref(client2, identifier));
|
|
103858
104405
|
const itemCopies = await runWithConcurrency(itemPlan, QTI_HYDRATION_CONCURRENCY, async (plan) => {
|
|
103859
104406
|
const sourceItem = await client2.qtiApi.assessmentItems.get(plan.sourceIdentifier);
|
|
103860
104407
|
return {
|
|
@@ -103979,7 +104526,7 @@ class TimebackAssessmentsService {
|
|
|
103979
104526
|
};
|
|
103980
104527
|
}
|
|
103981
104528
|
const section = await this.qtiSectionItems(client2, qtiTestIdentifier, undefined, undefined, ownership.test);
|
|
103982
|
-
const href =
|
|
104529
|
+
const href = qtiItemHref(client2, itemIdentifier);
|
|
103983
104530
|
let item;
|
|
103984
104531
|
let itemCreationAttempted = false;
|
|
103985
104532
|
let referenceCreationAttempted = false;
|
|
@@ -104089,7 +104636,7 @@ class TimebackAssessmentsService {
|
|
|
104089
104636
|
const result = await section.items.reorder({
|
|
104090
104637
|
items: items.map((item) => ({
|
|
104091
104638
|
identifier: item.identifier,
|
|
104092
|
-
href: item.href ??
|
|
104639
|
+
href: item.href ?? qtiItemHref(client2, item.identifier),
|
|
104093
104640
|
sequence: item.sequence
|
|
104094
104641
|
}))
|
|
104095
104642
|
});
|
|
@@ -104103,6 +104650,118 @@ class TimebackAssessmentsService {
|
|
|
104103
104650
|
}
|
|
104104
104651
|
return this.deps.timeback;
|
|
104105
104652
|
}
|
|
104653
|
+
reviewBankMasterySources(associations) {
|
|
104654
|
+
return associations.filter((association) => association.purpose === "mastery" && association.status === "live").map((association) => {
|
|
104655
|
+
const standard = assessmentStandardForRow(association);
|
|
104656
|
+
if (!association.assessmentKey || !standard) {
|
|
104657
|
+
throw new ValidationError(`Live mastery assessment ${association.qtiTestIdentifier} requires a managed identity and standard`);
|
|
104658
|
+
}
|
|
104659
|
+
if (assessmentKeyFromManagedQtiIdentifier(association.qtiTestIdentifier) !== association.assessmentKey) {
|
|
104660
|
+
throw new ValidationError(`Live mastery assessment ${association.qtiTestIdentifier} is not a managed publication for ${association.assessmentKey}`);
|
|
104661
|
+
}
|
|
104662
|
+
return {
|
|
104663
|
+
assessmentKey: association.assessmentKey,
|
|
104664
|
+
qtiTestIdentifier: association.qtiTestIdentifier,
|
|
104665
|
+
standard
|
|
104666
|
+
};
|
|
104667
|
+
});
|
|
104668
|
+
}
|
|
104669
|
+
async writeReviewBankSink(input) {
|
|
104670
|
+
function sinkInput(title, includeIdentifier = false) {
|
|
104671
|
+
return buildReviewBankSinkInput({
|
|
104672
|
+
identifier: input.plan.bankIdentifier,
|
|
104673
|
+
title,
|
|
104674
|
+
metadata: input.metadata,
|
|
104675
|
+
itemIdentifiers: input.plan.itemIdentifiers,
|
|
104676
|
+
itemHref: (identifier) => qtiItemHref(input.client, identifier),
|
|
104677
|
+
includeIdentifier
|
|
104678
|
+
});
|
|
104679
|
+
}
|
|
104680
|
+
if (input.existingSink) {
|
|
104681
|
+
await input.client.qtiApi.assessmentTests.update(input.plan.bankIdentifier, sinkInput(input.existingSink.title));
|
|
104682
|
+
return;
|
|
104683
|
+
}
|
|
104684
|
+
try {
|
|
104685
|
+
const createInput = sinkInput(REVIEW_BANK_TITLE, true);
|
|
104686
|
+
await input.client.qtiApi.assessmentTests.create(createInput);
|
|
104687
|
+
} catch (error88) {
|
|
104688
|
+
if (!isApiError(error88) || error88.statusCode !== 409) {
|
|
104689
|
+
throw error88;
|
|
104690
|
+
}
|
|
104691
|
+
const concurrentlyCreated = await input.client.qtiApi.assessmentTests.get(input.plan.bankIdentifier);
|
|
104692
|
+
assertReviewBankSinkOwnership(concurrentlyCreated, input.integrationId);
|
|
104693
|
+
await input.client.qtiApi.assessmentTests.update(input.plan.bankIdentifier, sinkInput(concurrentlyCreated.title));
|
|
104694
|
+
}
|
|
104695
|
+
}
|
|
104696
|
+
async installReviewBankAssociation(input) {
|
|
104697
|
+
const singleton = input.associations.find((association) => association.qtiTestIdentifier === input.sinkIdentifier);
|
|
104698
|
+
if (singleton && singleton.purpose !== "review") {
|
|
104699
|
+
throw new ValidationError("The reserved review-bank identifier is associated with another assessment purpose.");
|
|
104700
|
+
}
|
|
104701
|
+
const legacyReviews = input.associations.filter((association) => association.purpose === "review" && association.qtiTestIdentifier !== input.sinkIdentifier);
|
|
104702
|
+
const reservedKeyOwner = input.associations.find((association) => association.assessmentKey === input.sinkIdentifier && association.qtiTestIdentifier !== input.sinkIdentifier);
|
|
104703
|
+
if (reservedKeyOwner && reservedKeyOwner.purpose !== "review") {
|
|
104704
|
+
throw new ValidationError("The reserved review-bank assessment key is already in use.");
|
|
104705
|
+
}
|
|
104706
|
+
const singletonCurrent = Boolean(singleton && singleton.assessmentKey === input.sinkIdentifier && singleton.status === "live" && singleton.standardFramework === null && singleton.standardIdentifier === null && singleton.diagnosticKey === null && singleton.diagnosticRoutingManifest === null);
|
|
104707
|
+
const legacyCurrent = legacyReviews.every((association) => association.status === "archived" && association.assessmentKey !== input.sinkIdentifier);
|
|
104708
|
+
const changed = !singletonCurrent || !legacyCurrent;
|
|
104709
|
+
await input.beforeInstall();
|
|
104710
|
+
for (const legacy of legacyReviews) {
|
|
104711
|
+
if (legacy.status !== "archived" || legacy.assessmentKey === input.sinkIdentifier) {
|
|
104712
|
+
await input.tx.update(gameTimebackAssessmentTests).set({
|
|
104713
|
+
status: "archived",
|
|
104714
|
+
...legacy.assessmentKey === input.sinkIdentifier ? { assessmentKey: null } : {},
|
|
104715
|
+
updatedAt: new Date
|
|
104716
|
+
}).where(eq(gameTimebackAssessmentTests.id, legacy.id));
|
|
104717
|
+
}
|
|
104718
|
+
}
|
|
104719
|
+
if (singletonCurrent) {
|
|
104720
|
+
return changed;
|
|
104721
|
+
}
|
|
104722
|
+
if (singleton) {
|
|
104723
|
+
await input.tx.update(gameTimebackAssessmentTests).set({
|
|
104724
|
+
assessmentKey: input.sinkIdentifier,
|
|
104725
|
+
purpose: "review",
|
|
104726
|
+
status: "live",
|
|
104727
|
+
standardFramework: null,
|
|
104728
|
+
standardIdentifier: null,
|
|
104729
|
+
diagnosticKey: null,
|
|
104730
|
+
diagnosticRoutingManifest: null,
|
|
104731
|
+
updatedAt: new Date
|
|
104732
|
+
}).where(eq(gameTimebackAssessmentTests.id, singleton.id));
|
|
104733
|
+
return changed;
|
|
104734
|
+
}
|
|
104735
|
+
await input.tx.insert(gameTimebackAssessmentTests).values({
|
|
104736
|
+
integrationId: input.integrationId,
|
|
104737
|
+
assessmentKey: input.sinkIdentifier,
|
|
104738
|
+
qtiTestIdentifier: input.sinkIdentifier,
|
|
104739
|
+
purpose: "review",
|
|
104740
|
+
status: "live",
|
|
104741
|
+
standardFramework: null,
|
|
104742
|
+
standardIdentifier: null,
|
|
104743
|
+
diagnosticKey: null,
|
|
104744
|
+
diagnosticRoutingManifest: null
|
|
104745
|
+
});
|
|
104746
|
+
return changed;
|
|
104747
|
+
}
|
|
104748
|
+
reviewMappingResult(result) {
|
|
104749
|
+
setAttribute("app.assessment.operation", "update_review_mapping");
|
|
104750
|
+
setAttribute("app.assessment.review_mapping_item_count", result.itemCount);
|
|
104751
|
+
setAttribute("app.assessment.review_mapping_added_item_count", result.addedItemCount);
|
|
104752
|
+
setAttribute("app.assessment.review_mapping_removed_item_count", result.removedItemCount);
|
|
104753
|
+
return result;
|
|
104754
|
+
}
|
|
104755
|
+
async withLockedIntegrationAssessmentRows(integrationId, action) {
|
|
104756
|
+
return this.deps.db.transaction(async (tx) => {
|
|
104757
|
+
const [integration] = await tx.select({ id: gameTimebackIntegrations.id }).from(gameTimebackIntegrations).where(and(eq(gameTimebackIntegrations.id, integrationId), isActiveGameTimebackIntegrationStatus())).for("update");
|
|
104758
|
+
if (!integration) {
|
|
104759
|
+
throw new NotFoundError(`Integration not found: ${integrationId}`);
|
|
104760
|
+
}
|
|
104761
|
+
const rows = await tx.select().from(gameTimebackAssessmentTests).where(eq(gameTimebackAssessmentTests.integrationId, integrationId)).orderBy(gameTimebackAssessmentTests.id).for("update");
|
|
104762
|
+
return action(tx, rows);
|
|
104763
|
+
});
|
|
104764
|
+
}
|
|
104106
104765
|
async withLockedAssessmentAssociations(integrationId, qtiTestIdentifier, action) {
|
|
104107
104766
|
return this.deps.db.transaction(async (tx) => {
|
|
104108
104767
|
const rows = await tx.select().from(gameTimebackAssessmentTests).where(eq(gameTimebackAssessmentTests.qtiTestIdentifier, qtiTestIdentifier)).orderBy(gameTimebackAssessmentTests.id).for("update");
|
|
@@ -104187,8 +104846,16 @@ class TimebackAssessmentsService {
|
|
|
104187
104846
|
items: client2.qtiApi.assessmentTests.testParts(qtiTestIdentifier).sections(section.partIdentifier).items(section.sectionIdentifier)
|
|
104188
104847
|
};
|
|
104189
104848
|
}
|
|
104190
|
-
|
|
104191
|
-
|
|
104849
|
+
async findReviewBankSink(client2, integrationId) {
|
|
104850
|
+
const identifier = reviewBankSinkIdentifier(integrationId);
|
|
104851
|
+
try {
|
|
104852
|
+
return await client2.qtiApi.assessmentTests.get(identifier);
|
|
104853
|
+
} catch (error88) {
|
|
104854
|
+
if (isApiError(error88) && error88.statusCode === 404) {
|
|
104855
|
+
return null;
|
|
104856
|
+
}
|
|
104857
|
+
throw error88;
|
|
104858
|
+
}
|
|
104192
104859
|
}
|
|
104193
104860
|
async ensureImmutableQtiResource(get, create) {
|
|
104194
104861
|
try {
|
|
@@ -104229,7 +104896,7 @@ class TimebackAssessmentsService {
|
|
|
104229
104896
|
test: loaded.test,
|
|
104230
104897
|
questions: loaded.questions,
|
|
104231
104898
|
diagnosticRoutingManifest,
|
|
104232
|
-
itemHref: (identifier) =>
|
|
104899
|
+
itemHref: (identifier) => qtiItemHref(client2, identifier)
|
|
104233
104900
|
});
|
|
104234
104901
|
await this.ensureManagedQtiPublication(client2, plan);
|
|
104235
104902
|
return plan;
|
|
@@ -104273,7 +104940,6 @@ class TimebackAssessmentsService {
|
|
|
104273
104940
|
qtiTestIdentifier: row.qtiTestIdentifier,
|
|
104274
104941
|
purpose: row.purpose,
|
|
104275
104942
|
status: row.status,
|
|
104276
|
-
sortOrder: row.sortOrder,
|
|
104277
104943
|
standard: assessmentStandardForRow(row),
|
|
104278
104944
|
diagnostic: diagnosticDefinitionForRow(row),
|
|
104279
104945
|
createdAt: row.createdAt,
|
|
@@ -104311,15 +104977,11 @@ class TimebackAssessmentsService {
|
|
|
104311
104977
|
const plan = buildQtiLibraryListPlan(params);
|
|
104312
104978
|
return list(plan.params);
|
|
104313
104979
|
}
|
|
104314
|
-
async validateAssessmentHasQuestions(loaded
|
|
104980
|
+
async validateAssessmentHasQuestions(loaded) {
|
|
104315
104981
|
assertAssessmentHasQuestions(loaded.questions.questions);
|
|
104316
104982
|
for (const { question } of loaded.questions.questions) {
|
|
104317
104983
|
assertPlayableQtiQuestion(question);
|
|
104318
104984
|
}
|
|
104319
|
-
if (reviewGameSlug) {
|
|
104320
|
-
assertQtiTestOwnedByGame(loaded.test, reviewGameSlug);
|
|
104321
|
-
await this.writeReviewMapping(this.requireClient(), loaded.test, loaded.questions);
|
|
104322
|
-
}
|
|
104323
104985
|
}
|
|
104324
104986
|
async prepareDiagnosticAssociationChanges(row, nextPurpose, requested) {
|
|
104325
104987
|
if (row.purpose !== "diagnostic" && nextPurpose === "diagnostic" && !requested) {
|
|
@@ -104348,22 +105010,8 @@ class TimebackAssessmentsService {
|
|
|
104348
105010
|
}
|
|
104349
105011
|
};
|
|
104350
105012
|
}
|
|
104351
|
-
async rebuildReviewMapping(client2, qtiTestIdentifier, scope) {
|
|
104352
|
-
const [test, references] = await Promise.all([
|
|
104353
|
-
client2.qtiApi.assessmentTests.get(qtiTestIdentifier),
|
|
104354
|
-
client2.qtiApi.assessmentTests.getQuestions(qtiTestIdentifier)
|
|
104355
|
-
]);
|
|
104356
|
-
if (scope.kind === "owned") {
|
|
104357
|
-
assertQtiTestOwnedByGame(test, scope.gameSlug);
|
|
104358
|
-
}
|
|
104359
|
-
return this.writeReviewMapping(client2, test, await hydrateQtiTestQuestions(client2, references));
|
|
104360
|
-
}
|
|
104361
|
-
async writeReviewMapping(client2, test, questions) {
|
|
104362
|
-
const prepared = await prepareReviewMappingUpdate(test, questions);
|
|
104363
|
-
await client2.qtiApi.assessmentTests.update(test.identifier, prepared.update);
|
|
104364
|
-
return prepared.summary;
|
|
104365
|
-
}
|
|
104366
105013
|
}
|
|
105014
|
+
var REVIEW_BANK_TITLE = "Review Bank", REVIEW_BANK_SYNC_ATTEMPTS = 2;
|
|
104367
105015
|
var init_timeback_assessments_service = __esm(async () => {
|
|
104368
105016
|
init_drizzle_orm();
|
|
104369
105017
|
init_helpers_index();
|
|
@@ -164385,7 +165033,7 @@ function parseQtiLibraryParams(searchParams) {
|
|
|
164385
165033
|
limit
|
|
164386
165034
|
};
|
|
164387
165035
|
}
|
|
164388
|
-
var populateStudent, getUser, getUserEnrollments, getUserById, setupIntegration, getIntegrations, getRemovedIntegrations, createIntegration, updateIntegration, deactivateCourse, reactivateCourse, getIntegrationConfig, verifyIntegration, getConfig, deleteIntegrations, endActivity, heartbeat, advanceCourse, unenrollCourse, startRuntimeAssessment, getRuntimeAssessment, getLatestRuntimeAssessment, saveRuntimeAssessment, submitRuntimeAssessmentItem, submitRuntimeAssessment, finalizeRuntimeAssessment, exportRuntimeAssessmentFixture, getStudentXp, getStudentMastery, getStudentHighestGradeMastered, getRoster, getStudentOverview, getGameMetrics, getStudentActivity, getGradeLevelTestResults, getGradeLevelTestReview, getActivityDetail, listMetricDiscrepancies, verifyMetricDiscrepancy, grantXp, adjustTime, adjustMastery, reconcileMasteryForConfigChange, searchStudents, enrollStudent, unenrollStudent, reactivateEnrollment, listAssessments, createAssessment, attachExistingAssessments, updateAssessment,
|
|
165036
|
+
var populateStudent, getUser, getUserEnrollments, getUserById, setupIntegration, getIntegrations, getRemovedIntegrations, createIntegration, updateIntegration, deactivateCourse, reactivateCourse, getIntegrationConfig, verifyIntegration, getConfig, deleteIntegrations, endActivity, heartbeat, advanceCourse, unenrollCourse, startRuntimeAssessment, getRuntimeAssessment, getLatestRuntimeAssessment, saveRuntimeAssessment, submitRuntimeAssessmentItem, submitRuntimeAssessment, finalizeRuntimeAssessment, exportRuntimeAssessmentFixture, getStudentXp, getStudentMastery, getStudentHighestGradeMastered, getRoster, getStudentOverview, getGameMetrics, getStudentActivity, getGradeLevelTestResults, getGradeLevelTestReview, getActivityDetail, listMetricDiscrepancies, verifyMetricDiscrepancy, grantXp, adjustTime, adjustMastery, reconcileMasteryForConfigChange, searchStudents, enrollStudent, unenrollStudent, reactivateEnrollment, listAssessments, createAssessment, attachExistingAssessments, updateAssessment, synchronizeReviewBank, reorderQuestions, removeAssessment, listQuestions, listQuestionLibrary, listTestLibrary, copyAssessment, createQuestion, updateQuestion, removeQuestion, timeback2;
|
|
164389
165037
|
var init_timeback_controller = __esm(() => {
|
|
164390
165038
|
init_esm();
|
|
164391
165039
|
init_src();
|
|
@@ -164747,7 +165395,9 @@ var init_timeback_controller = __esm(() => {
|
|
|
164747
165395
|
attemptId,
|
|
164748
165396
|
input: {
|
|
164749
165397
|
submissionId: body2.submissionId,
|
|
164750
|
-
xpAwarded: body2.xpAwarded
|
|
165398
|
+
xpAwarded: body2.xpAwarded,
|
|
165399
|
+
...body2.masteredUnits !== undefined ? { masteredUnits: body2.masteredUnits } : {},
|
|
165400
|
+
...body2.masteredUnitsAbsolute !== undefined ? { masteredUnitsAbsolute: body2.masteredUnitsAbsolute } : {}
|
|
164751
165401
|
},
|
|
164752
165402
|
game: {
|
|
164753
165403
|
appName: body2.appName,
|
|
@@ -165081,23 +165731,13 @@ var init_timeback_controller = __esm(() => {
|
|
|
165081
165731
|
const body2 = await parseRequestBody(ctx.request, UpdateAssessmentRequestSchema);
|
|
165082
165732
|
return ctx.services.timebackAssessments.updateAssessment(integrationId, testIdentifier, body2);
|
|
165083
165733
|
});
|
|
165084
|
-
|
|
165085
|
-
const { gameId, courseId, testIdentifier } = ctx.params;
|
|
165086
|
-
if (!gameId || !courseId || !testIdentifier) {
|
|
165087
|
-
throw ApiError.badRequest("Missing gameId, courseId, or testIdentifier parameter");
|
|
165088
|
-
}
|
|
165089
|
-
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
165090
|
-
return ctx.services.timebackAssessments.updateReviewMapping(integrationId, testIdentifier);
|
|
165091
|
-
});
|
|
165092
|
-
reorderAssessments = requireDeveloper(async (ctx) => {
|
|
165734
|
+
synchronizeReviewBank = requireDeveloper(async (ctx) => {
|
|
165093
165735
|
const { gameId, courseId } = ctx.params;
|
|
165094
165736
|
if (!gameId || !courseId) {
|
|
165095
165737
|
throw ApiError.badRequest("Missing gameId or courseId parameter");
|
|
165096
165738
|
}
|
|
165097
|
-
const body2 = await parseRequestBody(ctx.request, ReorderAssessmentsRequestSchema);
|
|
165098
165739
|
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
165099
|
-
|
|
165100
|
-
return { success: true };
|
|
165740
|
+
return ctx.services.timebackAssessments.synchronizeReviewBank(integrationId);
|
|
165101
165741
|
});
|
|
165102
165742
|
reorderQuestions = requireDeveloper(async (ctx) => {
|
|
165103
165743
|
const { gameId, courseId, testIdentifier } = ctx.params;
|
|
@@ -165238,8 +165878,7 @@ var init_timeback_controller = __esm(() => {
|
|
|
165238
165878
|
createAssessment,
|
|
165239
165879
|
attachExistingAssessments,
|
|
165240
165880
|
updateAssessment,
|
|
165241
|
-
|
|
165242
|
-
reorderAssessments,
|
|
165881
|
+
synchronizeReviewBank,
|
|
165243
165882
|
removeAssessment,
|
|
165244
165883
|
reorderQuestions,
|
|
165245
165884
|
listQuestions,
|