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