@playcademy/vite-plugin 1.3.1 → 1.3.2-beta.1
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +3002 -873
- package/package.json +4 -4
package/dist/index.js
CHANGED
|
@@ -24317,7 +24317,7 @@ import path2 from "node:path";
|
|
|
24317
24317
|
// package.json
|
|
24318
24318
|
var package_default = {
|
|
24319
24319
|
name: "@playcademy/vite-plugin",
|
|
24320
|
-
version: "1.3.1",
|
|
24320
|
+
version: "1.3.2-beta.1",
|
|
24321
24321
|
type: "module",
|
|
24322
24322
|
exports: {
|
|
24323
24323
|
".": {
|
|
@@ -25934,7 +25934,7 @@ var package_default2;
|
|
|
25934
25934
|
var init_package = __esm(() => {
|
|
25935
25935
|
package_default2 = {
|
|
25936
25936
|
name: "@playcademy/sandbox",
|
|
25937
|
-
version: "0.8.
|
|
25937
|
+
version: "0.8.1-beta.4",
|
|
25938
25938
|
description: "Local development server for Playcademy game development",
|
|
25939
25939
|
type: "module",
|
|
25940
25940
|
exports: {
|
|
@@ -36526,6 +36526,9 @@ var RESPONSE_CARDINALITIES;
|
|
|
36526
36526
|
var RESPONSE_BASE_TYPES;
|
|
36527
36527
|
var POINT_INTERACTION_TYPES;
|
|
36528
36528
|
var INDEPENDENT_PROCESSING_TAGS;
|
|
36529
|
+
var SUPPORTED_BASE_TYPES;
|
|
36530
|
+
var SUPPORTED_CARDINALITIES;
|
|
36531
|
+
var MATCH_CORRECT_TEMPLATES;
|
|
36529
36532
|
var UUID_PATTERN;
|
|
36530
36533
|
var COMMON_CORE_MATH_FRAMEWORK_ALIASES;
|
|
36531
36534
|
var COMMON_CORE_ELA_FRAMEWORK_ALIASES;
|
|
@@ -36569,10 +36572,13 @@ var LatestAssessmentFilterBaseSchema;
|
|
|
36569
36572
|
var LatestAssessmentFiltersSchema;
|
|
36570
36573
|
var LatestRuntimeAssessmentQuerySchema;
|
|
36571
36574
|
var StartAssessmentBodySchema;
|
|
36575
|
+
var AssessmentPreparationReceiptSchema;
|
|
36576
|
+
var StartAssessmentRequestBodySchema;
|
|
36572
36577
|
var SaveAssessmentBodySchema;
|
|
36573
36578
|
var SubmitAssessmentItemBodySchema;
|
|
36574
36579
|
var SubmitAssessmentBodySchema;
|
|
36575
36580
|
var FinalizeAssessmentBodySchema;
|
|
36581
|
+
var PrepareRuntimeAssessmentRequestSchema;
|
|
36576
36582
|
var StartRuntimeAssessmentRequestSchema;
|
|
36577
36583
|
var SaveRuntimeAssessmentRequestSchema;
|
|
36578
36584
|
var SubmitAssessmentItemRuntimeRequestSchema;
|
|
@@ -36731,6 +36737,23 @@ var init_assessment_runtime2 = __esm(() => {
|
|
|
36731
36737
|
"qti-sum",
|
|
36732
36738
|
"qti-base-value"
|
|
36733
36739
|
]);
|
|
36740
|
+
SUPPORTED_BASE_TYPES = new Set([
|
|
36741
|
+
"string",
|
|
36742
|
+
"identifier",
|
|
36743
|
+
"integer",
|
|
36744
|
+
"float",
|
|
36745
|
+
"directedPair"
|
|
36746
|
+
]);
|
|
36747
|
+
SUPPORTED_CARDINALITIES = new Set([
|
|
36748
|
+
"single",
|
|
36749
|
+
"multiple",
|
|
36750
|
+
"ordered"
|
|
36751
|
+
]);
|
|
36752
|
+
MATCH_CORRECT_TEMPLATES = new Set([
|
|
36753
|
+
"http://www.imsglobal.org/question/qti_v3p0/rptemplates/match_correct",
|
|
36754
|
+
"https://www.imsglobal.org/question/qti_v3p0/rptemplates/match_correct",
|
|
36755
|
+
"https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/match_correct"
|
|
36756
|
+
].flatMap((template) => [template, `${template}.xml`]));
|
|
36734
36757
|
UUID_PATTERN = /^(?:urn:uuid:)?\{?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}?$/i;
|
|
36735
36758
|
COMMON_CORE_MATH_FRAMEWORK_ALIASES = new Set([
|
|
36736
36759
|
"ccssm",
|
|
@@ -36907,6 +36930,8 @@ var init_assessment_runtime2 = __esm(() => {
|
|
|
36907
36930
|
standard: AssessmentStandardRefSchema
|
|
36908
36931
|
})
|
|
36909
36932
|
]);
|
|
36933
|
+
AssessmentPreparationReceiptSchema = exports_external.string().max(32000);
|
|
36934
|
+
StartAssessmentRequestBodySchema = exports_external.intersection(StartAssessmentBodySchema, exports_external.object({ preparationReceipt: AssessmentPreparationReceiptSchema.optional() }));
|
|
36910
36935
|
SaveAssessmentBodySchema = exports_external.object({
|
|
36911
36936
|
expectedResponseVersion: exports_external.number().int().nonnegative(),
|
|
36912
36937
|
responses: exports_external.record(ResponseKeySchema, exports_external.record(ResponseKeySchema, AssessmentResponseValueSchema.nullable()))
|
|
@@ -36934,7 +36959,8 @@ var init_assessment_runtime2 = __esm(() => {
|
|
|
36934
36959
|
message: "Provide either masteredUnits or masteredUnitsAbsolute, not both",
|
|
36935
36960
|
path: ["masteredUnits"]
|
|
36936
36961
|
});
|
|
36937
|
-
|
|
36962
|
+
PrepareRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.and(StartAssessmentBodySchema);
|
|
36963
|
+
StartRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.and(StartAssessmentRequestBodySchema);
|
|
36938
36964
|
SaveRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.extend(SaveAssessmentBodySchema.shape);
|
|
36939
36965
|
SubmitAssessmentItemRuntimeRequestSchema = AssessmentRuntimeIdentitySchema.extend(SubmitAssessmentItemBodySchema.shape);
|
|
36940
36966
|
SubmitRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.extend(SubmitAssessmentBodySchema.shape).extend({
|
|
@@ -37458,6 +37484,7 @@ var init_schema = __esm(() => {
|
|
|
37458
37484
|
ltiTestMode: exports_external.boolean().default(false),
|
|
37459
37485
|
platformServiceJwt: platformServiceJwtConfigSchema.optional(),
|
|
37460
37486
|
uploadBucket: exports_external.string().optional(),
|
|
37487
|
+
assessmentArtifactBucket: exports_external.string().optional(),
|
|
37461
37488
|
cdnOrigin: exports_external.string().url().startsWith("https://").optional(),
|
|
37462
37489
|
queueIngressSecret: exports_external.string().optional(),
|
|
37463
37490
|
secretsManifestPepper: exports_external.string().optional()
|
|
@@ -63125,6 +63152,318 @@ var init_infra2 = __esm(() => {
|
|
|
63125
63152
|
init_alerts_service();
|
|
63126
63153
|
init_kv_backup_service();
|
|
63127
63154
|
});
|
|
63155
|
+
function nonEmptyIdentityValue(value) {
|
|
63156
|
+
return typeof value === "string" && value.length > 0 && value === value.trim();
|
|
63157
|
+
}
|
|
63158
|
+
function assessmentArtifactPairKeys(identity) {
|
|
63159
|
+
assertAssessmentArtifactIdentity(identity);
|
|
63160
|
+
const identitySegments = identity.kind === "assessment" ? ["qti", identity.qtiTestIdentifier] : [
|
|
63161
|
+
"review",
|
|
63162
|
+
identity.qtiTestIdentifier,
|
|
63163
|
+
identity.sourceContentRevision,
|
|
63164
|
+
identity.bankRevision
|
|
63165
|
+
];
|
|
63166
|
+
const identityPath = identitySegments.map(encodeURIComponent).join("/");
|
|
63167
|
+
const versionPath = `artifact-v${ASSESSMENT_ARTIFACT_SCHEMA_VERSION}/grader-v${ASSESSMENT_GRADER_PROTOCOL_VERSION}`;
|
|
63168
|
+
const prefix = `${ARTIFACT_KEY_PREFIX}/${identityPath}/${versionPath}`;
|
|
63169
|
+
return {
|
|
63170
|
+
playable: `${prefix}/playable.json`,
|
|
63171
|
+
scoring: `${prefix}/scoring.json`
|
|
63172
|
+
};
|
|
63173
|
+
}
|
|
63174
|
+
function jsonValidationMessage(value, path3, ancestors) {
|
|
63175
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") {
|
|
63176
|
+
return null;
|
|
63177
|
+
}
|
|
63178
|
+
if (typeof value === "number") {
|
|
63179
|
+
return Number.isFinite(value) ? null : `${path3} must contain a finite number`;
|
|
63180
|
+
}
|
|
63181
|
+
if (typeof value !== "object") {
|
|
63182
|
+
return `${path3} is not JSON-serializable`;
|
|
63183
|
+
}
|
|
63184
|
+
if (ancestors.has(value)) {
|
|
63185
|
+
return `${path3} contains a circular reference`;
|
|
63186
|
+
}
|
|
63187
|
+
const prototype = Object.getPrototypeOf(value);
|
|
63188
|
+
if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) {
|
|
63189
|
+
return `${path3} must contain only plain JSON objects`;
|
|
63190
|
+
}
|
|
63191
|
+
if (Array.isArray(value) && (Object.keys(value).length !== value.length || Object.keys(value).some((key, index2) => key !== String(index2)) || Reflect.ownKeys(value).length !== value.length + 1)) {
|
|
63192
|
+
return `${path3} must contain a dense JSON array`;
|
|
63193
|
+
}
|
|
63194
|
+
if (!Array.isArray(value) && Reflect.ownKeys(value).length !== Object.keys(value).length) {
|
|
63195
|
+
return `${path3} contains non-JSON object properties`;
|
|
63196
|
+
}
|
|
63197
|
+
ancestors.add(value);
|
|
63198
|
+
const entries = Array.isArray(value) ? value.map((entry2, index2) => [String(index2), entry2]) : Object.entries(value);
|
|
63199
|
+
for (const [key, entry2] of entries) {
|
|
63200
|
+
const message = jsonValidationMessage(entry2, `${path3}.${key}`, ancestors);
|
|
63201
|
+
if (message) {
|
|
63202
|
+
ancestors.delete(value);
|
|
63203
|
+
return message;
|
|
63204
|
+
}
|
|
63205
|
+
}
|
|
63206
|
+
ancestors.delete(value);
|
|
63207
|
+
return null;
|
|
63208
|
+
}
|
|
63209
|
+
function assertJsonValue(value, path3) {
|
|
63210
|
+
const message = jsonValidationMessage(value, path3, new Set);
|
|
63211
|
+
if (message) {
|
|
63212
|
+
throw new ValidationError(message);
|
|
63213
|
+
}
|
|
63214
|
+
}
|
|
63215
|
+
function isRecord6(value) {
|
|
63216
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
63217
|
+
}
|
|
63218
|
+
function hasExactlyKeys(value, keys) {
|
|
63219
|
+
const actual = Object.keys(value).toSorted();
|
|
63220
|
+
const expected = [...keys].toSorted();
|
|
63221
|
+
return actual.length === expected.length && actual.every((key, index2) => key === expected[index2]);
|
|
63222
|
+
}
|
|
63223
|
+
function assertAssessmentArtifactIdentity(identity) {
|
|
63224
|
+
if (!isRecord6(identity) || !nonEmptyIdentityValue(identity.qtiTestIdentifier)) {
|
|
63225
|
+
throw new ValidationError("Assessment artifact identity is invalid");
|
|
63226
|
+
}
|
|
63227
|
+
if (identity.kind === "assessment" && hasExactlyKeys(identity, ["kind", "qtiTestIdentifier", "contentRevision"]) && nonEmptyIdentityValue(identity.contentRevision)) {
|
|
63228
|
+
return;
|
|
63229
|
+
}
|
|
63230
|
+
if (identity.kind === "review-bank" && hasExactlyKeys(identity, [
|
|
63231
|
+
"kind",
|
|
63232
|
+
"qtiTestIdentifier",
|
|
63233
|
+
"sourceContentRevision",
|
|
63234
|
+
"bankRevision"
|
|
63235
|
+
]) && nonEmptyIdentityValue(identity.sourceContentRevision) && nonEmptyIdentityValue(identity.bankRevision)) {
|
|
63236
|
+
return;
|
|
63237
|
+
}
|
|
63238
|
+
throw new ValidationError("Assessment artifact identity is invalid");
|
|
63239
|
+
}
|
|
63240
|
+
function envelopeWithoutChecksum({
|
|
63241
|
+
checksum: _checksum,
|
|
63242
|
+
...content
|
|
63243
|
+
}) {
|
|
63244
|
+
return content;
|
|
63245
|
+
}
|
|
63246
|
+
async function prepareAssessmentArtifact(input) {
|
|
63247
|
+
assertAssessmentArtifactIdentity(input.identity);
|
|
63248
|
+
assertJsonValue(input.payload, "Assessment artifact payload");
|
|
63249
|
+
const content = {
|
|
63250
|
+
artifactKind: input.artifactKind,
|
|
63251
|
+
artifactSchemaVersion: ASSESSMENT_ARTIFACT_SCHEMA_VERSION,
|
|
63252
|
+
graderProtocolVersion: ASSESSMENT_GRADER_PROTOCOL_VERSION,
|
|
63253
|
+
identity: input.identity,
|
|
63254
|
+
payload: input.payload
|
|
63255
|
+
};
|
|
63256
|
+
const checksum = await sha256Hex(canonicalJson2(content));
|
|
63257
|
+
const envelope = {
|
|
63258
|
+
...content,
|
|
63259
|
+
checksum: { algorithm: "sha256", value: checksum }
|
|
63260
|
+
};
|
|
63261
|
+
return {
|
|
63262
|
+
envelope,
|
|
63263
|
+
bytes: textEncoder.encode(canonicalJson2(envelope)),
|
|
63264
|
+
key: assessmentArtifactPairKeys(input.identity)[input.artifactKind]
|
|
63265
|
+
};
|
|
63266
|
+
}
|
|
63267
|
+
function parseEnvelope(value) {
|
|
63268
|
+
if (!isRecord6(value) || !hasExactlyKeys(value, [
|
|
63269
|
+
"artifactKind",
|
|
63270
|
+
"artifactSchemaVersion",
|
|
63271
|
+
"graderProtocolVersion",
|
|
63272
|
+
"identity",
|
|
63273
|
+
"checksum",
|
|
63274
|
+
"payload"
|
|
63275
|
+
]) || value.artifactKind !== "playable" && value.artifactKind !== "scoring" || value.artifactSchemaVersion !== ASSESSMENT_ARTIFACT_SCHEMA_VERSION || value.graderProtocolVersion !== ASSESSMENT_GRADER_PROTOCOL_VERSION) {
|
|
63276
|
+
throw new ValidationError("Assessment artifact envelope is invalid or incompatible");
|
|
63277
|
+
}
|
|
63278
|
+
assertAssessmentArtifactIdentity(value.identity);
|
|
63279
|
+
if (!isRecord6(value.checksum) || !hasExactlyKeys(value.checksum, ["algorithm", "value"]) || value.checksum.algorithm !== "sha256" || typeof value.checksum.value !== "string" || !SHA256_HEX.test(value.checksum.value)) {
|
|
63280
|
+
throw new ValidationError("Assessment artifact checksum is invalid");
|
|
63281
|
+
}
|
|
63282
|
+
assertJsonValue(value.payload, "Assessment artifact payload");
|
|
63283
|
+
return value;
|
|
63284
|
+
}
|
|
63285
|
+
async function verifyAssessmentArtifactBytes(bytes) {
|
|
63286
|
+
let source;
|
|
63287
|
+
let parsed;
|
|
63288
|
+
try {
|
|
63289
|
+
source = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
63290
|
+
parsed = JSON.parse(source);
|
|
63291
|
+
} catch {
|
|
63292
|
+
throw new ValidationError("Assessment artifact is not valid UTF-8 JSON");
|
|
63293
|
+
}
|
|
63294
|
+
const envelope = parseEnvelope(parsed);
|
|
63295
|
+
const canonicalEnvelope = canonicalJson2(envelope);
|
|
63296
|
+
if (source !== canonicalEnvelope) {
|
|
63297
|
+
throw new ValidationError("Assessment artifact bytes are not canonically encoded");
|
|
63298
|
+
}
|
|
63299
|
+
const expectedChecksum = await sha256Hex(canonicalJson2(envelopeWithoutChecksum(envelope)));
|
|
63300
|
+
if (envelope.checksum.value !== expectedChecksum) {
|
|
63301
|
+
throw new ValidationError("Assessment artifact checksum does not match its contents");
|
|
63302
|
+
}
|
|
63303
|
+
return envelope;
|
|
63304
|
+
}
|
|
63305
|
+
function assessmentArtifactIdentitiesMatch(left, right) {
|
|
63306
|
+
return canonicalJson2(left) === canonicalJson2(right);
|
|
63307
|
+
}
|
|
63308
|
+
function assertMatchingAssessmentArtifactPair(playable, scoring, expectedIdentity) {
|
|
63309
|
+
if (playable.artifactKind !== "playable" || scoring.artifactKind !== "scoring" || playable.artifactSchemaVersion !== scoring.artifactSchemaVersion || playable.graderProtocolVersion !== scoring.graderProtocolVersion || !assessmentArtifactIdentitiesMatch(playable.identity, scoring.identity) || expectedIdentity && !assessmentArtifactIdentitiesMatch(playable.identity, expectedIdentity)) {
|
|
63310
|
+
throw new ValidationError("Assessment artifact pair does not describe one revision");
|
|
63311
|
+
}
|
|
63312
|
+
}
|
|
63313
|
+
var ASSESSMENT_ARTIFACT_SCHEMA_VERSION = 1;
|
|
63314
|
+
var ASSESSMENT_GRADER_PROTOCOL_VERSION = 1;
|
|
63315
|
+
var ARTIFACT_KEY_PREFIX = "assessment-artifacts";
|
|
63316
|
+
var SHA256_HEX;
|
|
63317
|
+
var textEncoder;
|
|
63318
|
+
var init_assessment_artifact_envelope_util = __esm(() => {
|
|
63319
|
+
init_errors2();
|
|
63320
|
+
SHA256_HEX = /^[a-f0-9]{64}$/;
|
|
63321
|
+
textEncoder = new TextEncoder;
|
|
63322
|
+
});
|
|
63323
|
+
function requireBucketName(deps) {
|
|
63324
|
+
const bucketName = deps.bucketName?.trim();
|
|
63325
|
+
if (!bucketName) {
|
|
63326
|
+
throw new ServiceUnavailableError("Assessment artifact storage is not configured");
|
|
63327
|
+
}
|
|
63328
|
+
return bucketName;
|
|
63329
|
+
}
|
|
63330
|
+
function bytesEqual(left, right) {
|
|
63331
|
+
return left.length === right.length && left.every((byte, index2) => byte === right[index2]);
|
|
63332
|
+
}
|
|
63333
|
+
async function holdsExpectedBytes(deps, bucketName, artifact) {
|
|
63334
|
+
const stored = await deps.storage.getObject(bucketName, artifact.key);
|
|
63335
|
+
if (!stored) {
|
|
63336
|
+
return false;
|
|
63337
|
+
}
|
|
63338
|
+
if (!bytesEqual(stored.body, artifact.bytes)) {
|
|
63339
|
+
throw new ConflictError("Immutable assessment artifact key contains different bytes", {
|
|
63340
|
+
key: artifact.key
|
|
63341
|
+
});
|
|
63342
|
+
}
|
|
63343
|
+
return true;
|
|
63344
|
+
}
|
|
63345
|
+
async function ensureStored(deps, bucketName, artifact, alreadyStored) {
|
|
63346
|
+
const created = !alreadyStored && await deps.storage.putObjectIfAbsent(bucketName, artifact.key, artifact.bytes, {
|
|
63347
|
+
contentType: "application/json"
|
|
63348
|
+
});
|
|
63349
|
+
if (!await holdsExpectedBytes(deps, bucketName, artifact)) {
|
|
63350
|
+
throw new ServiceUnavailableError("Assessment artifact disappeared during storage", {
|
|
63351
|
+
key: artifact.key
|
|
63352
|
+
});
|
|
63353
|
+
}
|
|
63354
|
+
return created ? "created" : "existing";
|
|
63355
|
+
}
|
|
63356
|
+
async function storeAssessmentArtifactPair(deps, input) {
|
|
63357
|
+
const bucketName = requireBucketName(deps);
|
|
63358
|
+
const [playable, scoring] = await Promise.all([
|
|
63359
|
+
prepareAssessmentArtifact({
|
|
63360
|
+
artifactKind: "playable",
|
|
63361
|
+
identity: input.identity,
|
|
63362
|
+
payload: input.playablePayload
|
|
63363
|
+
}),
|
|
63364
|
+
prepareAssessmentArtifact({
|
|
63365
|
+
artifactKind: "scoring",
|
|
63366
|
+
identity: input.identity,
|
|
63367
|
+
payload: input.scoringPayload
|
|
63368
|
+
})
|
|
63369
|
+
]);
|
|
63370
|
+
const [playableStored, scoringStored] = await Promise.all([
|
|
63371
|
+
holdsExpectedBytes(deps, bucketName, playable),
|
|
63372
|
+
holdsExpectedBytes(deps, bucketName, scoring)
|
|
63373
|
+
]);
|
|
63374
|
+
const playableStatus = await ensureStored(deps, bucketName, playable, playableStored);
|
|
63375
|
+
const scoringStatus = await ensureStored(deps, bucketName, scoring, scoringStored);
|
|
63376
|
+
const [storedPlayable, storedScoring] = await Promise.all([
|
|
63377
|
+
verifyAssessmentArtifactBytes(playable.bytes),
|
|
63378
|
+
verifyAssessmentArtifactBytes(scoring.bytes)
|
|
63379
|
+
]);
|
|
63380
|
+
assertMatchingAssessmentArtifactPair(storedPlayable, storedScoring, input.identity);
|
|
63381
|
+
return {
|
|
63382
|
+
playable: {
|
|
63383
|
+
key: playable.key,
|
|
63384
|
+
checksum: playable.envelope.checksum.value,
|
|
63385
|
+
status: playableStatus
|
|
63386
|
+
},
|
|
63387
|
+
scoring: {
|
|
63388
|
+
key: scoring.key,
|
|
63389
|
+
checksum: scoring.envelope.checksum.value,
|
|
63390
|
+
status: scoringStatus
|
|
63391
|
+
}
|
|
63392
|
+
};
|
|
63393
|
+
}
|
|
63394
|
+
async function loadAssessmentArtifactPair(deps, identity) {
|
|
63395
|
+
const bucketName = requireBucketName(deps);
|
|
63396
|
+
const keys = assessmentArtifactPairKeys(identity);
|
|
63397
|
+
const [playableObject, scoringObject] = await Promise.all([
|
|
63398
|
+
deps.storage.getObject(bucketName, keys.playable),
|
|
63399
|
+
deps.storage.getObject(bucketName, keys.scoring)
|
|
63400
|
+
]);
|
|
63401
|
+
if (!playableObject || !scoringObject) {
|
|
63402
|
+
return null;
|
|
63403
|
+
}
|
|
63404
|
+
const [playable, scoring] = await Promise.all([
|
|
63405
|
+
verifyAssessmentArtifactBytes(playableObject.body),
|
|
63406
|
+
verifyAssessmentArtifactBytes(scoringObject.body)
|
|
63407
|
+
]);
|
|
63408
|
+
assertMatchingAssessmentArtifactPair(playable, scoring, identity);
|
|
63409
|
+
return {
|
|
63410
|
+
playable,
|
|
63411
|
+
scoring
|
|
63412
|
+
};
|
|
63413
|
+
}
|
|
63414
|
+
async function loadVerifiedArtifact(deps, key) {
|
|
63415
|
+
const bucketName = requireBucketName(deps);
|
|
63416
|
+
const object = await deps.storage.getObject(bucketName, key);
|
|
63417
|
+
return object ? verifyAssessmentArtifactBytes(object.body) : null;
|
|
63418
|
+
}
|
|
63419
|
+
async function loadAssessmentArtifact(deps, artifactKind, identity) {
|
|
63420
|
+
const artifact = await loadVerifiedArtifact(deps, assessmentArtifactPairKeys(identity)[artifactKind]);
|
|
63421
|
+
if (artifact && (artifact.artifactKind !== artifactKind || !assessmentArtifactIdentitiesMatch(artifact.identity, identity))) {
|
|
63422
|
+
throw new ValidationError("Assessment artifact does not describe the requested revision");
|
|
63423
|
+
}
|
|
63424
|
+
return artifact;
|
|
63425
|
+
}
|
|
63426
|
+
async function loadManagedPlayableArtifact(deps, qtiTestIdentifier) {
|
|
63427
|
+
const key = assessmentArtifactPairKeys({
|
|
63428
|
+
kind: "assessment",
|
|
63429
|
+
qtiTestIdentifier,
|
|
63430
|
+
contentRevision: "unbound-managed-lookup"
|
|
63431
|
+
}).playable;
|
|
63432
|
+
const artifact = await loadVerifiedArtifact(deps, key);
|
|
63433
|
+
if (artifact && (artifact.artifactKind !== "playable" || artifact.identity.kind !== "assessment" || artifact.identity.qtiTestIdentifier !== qtiTestIdentifier)) {
|
|
63434
|
+
throw new ValidationError("Assessment artifact does not describe the requested assessment");
|
|
63435
|
+
}
|
|
63436
|
+
return artifact;
|
|
63437
|
+
}
|
|
63438
|
+
var init_assessment_artifact_storage_util = __esm(() => {
|
|
63439
|
+
init_errors2();
|
|
63440
|
+
init_assessment_artifact_envelope_util();
|
|
63441
|
+
});
|
|
63442
|
+
|
|
63443
|
+
class AssessmentArtifactsService {
|
|
63444
|
+
deps;
|
|
63445
|
+
constructor(deps) {
|
|
63446
|
+
this.deps = deps;
|
|
63447
|
+
}
|
|
63448
|
+
storePair(input) {
|
|
63449
|
+
return storeAssessmentArtifactPair(this.deps, input);
|
|
63450
|
+
}
|
|
63451
|
+
loadPair(identity) {
|
|
63452
|
+
return loadAssessmentArtifactPair(this.deps, identity);
|
|
63453
|
+
}
|
|
63454
|
+
loadPlayable(identity) {
|
|
63455
|
+
return loadAssessmentArtifact(this.deps, "playable", identity);
|
|
63456
|
+
}
|
|
63457
|
+
loadScoring(identity) {
|
|
63458
|
+
return loadAssessmentArtifact(this.deps, "scoring", identity);
|
|
63459
|
+
}
|
|
63460
|
+
loadManagedPlayable(qtiTestIdentifier) {
|
|
63461
|
+
return loadManagedPlayableArtifact(this.deps, qtiTestIdentifier);
|
|
63462
|
+
}
|
|
63463
|
+
}
|
|
63464
|
+
var init_assessment_artifacts_service = __esm(() => {
|
|
63465
|
+
init_assessment_artifact_storage_util();
|
|
63466
|
+
});
|
|
63128
63467
|
var require_is = __commonJS2((exports, module2) => {
|
|
63129
63468
|
var defined = function(val) {
|
|
63130
63469
|
return typeof val !== "undefined" && val !== null;
|
|
@@ -121439,6 +121778,365 @@ function supportedQtiQuestionInteractionType(question) {
|
|
|
121439
121778
|
return null;
|
|
121440
121779
|
}
|
|
121441
121780
|
}
|
|
121781
|
+
function childElements(node, localName2) {
|
|
121782
|
+
return node.children.filter((child) => child.kind === "element" && (!localName2 || child.localName === localName2));
|
|
121783
|
+
}
|
|
121784
|
+
function onlyWhitespaceText(nodes) {
|
|
121785
|
+
return nodes.every((node) => node.kind === "element" || node.value.trim() === "");
|
|
121786
|
+
}
|
|
121787
|
+
function onlyAttributes(node, allowed) {
|
|
121788
|
+
const names = Object.keys(node.attributes);
|
|
121789
|
+
return names.length === allowed.length && names.every((name3) => allowed.includes(name3));
|
|
121790
|
+
}
|
|
121791
|
+
function directText(node) {
|
|
121792
|
+
if (childElements(node).length > 0) {
|
|
121793
|
+
return null;
|
|
121794
|
+
}
|
|
121795
|
+
return node.children.map((child) => child.kind === "text" ? child.value : "").join("").trim();
|
|
121796
|
+
}
|
|
121797
|
+
function itemElement(root) {
|
|
121798
|
+
return root.localName === "qti-assessment-item" ? root : xmlDescendants(root, (candidate) => candidate.localName === "qti-assessment-item")[0];
|
|
121799
|
+
}
|
|
121800
|
+
function fallback(itemIdentifier, maxScore, reason) {
|
|
121801
|
+
return {
|
|
121802
|
+
strategy: "qti",
|
|
121803
|
+
itemIdentifier,
|
|
121804
|
+
maxScore,
|
|
121805
|
+
reason
|
|
121806
|
+
};
|
|
121807
|
+
}
|
|
121808
|
+
function parsedNumericValue(value, baseType) {
|
|
121809
|
+
const syntax = baseType === "integer" ? /^[+-]?\d+$/ : /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i;
|
|
121810
|
+
if (!syntax.test(value.trim())) {
|
|
121811
|
+
return null;
|
|
121812
|
+
}
|
|
121813
|
+
const numeric3 = Number(value);
|
|
121814
|
+
return Number.isFinite(numeric3) && (baseType !== "integer" || Number.isSafeInteger(numeric3)) ? numeric3 : null;
|
|
121815
|
+
}
|
|
121816
|
+
function numericCorrectValue(declaration) {
|
|
121817
|
+
if (declaration.cardinality !== "single" || declaration.correctValues.length !== 1 || declaration.baseType !== "integer" && declaration.baseType !== "float") {
|
|
121818
|
+
return null;
|
|
121819
|
+
}
|
|
121820
|
+
return parsedNumericValue(declaration.correctValues[0], declaration.baseType);
|
|
121821
|
+
}
|
|
121822
|
+
function declarationResult(raw, normalized) {
|
|
121823
|
+
const identifier = raw.attributes.identifier?.trim();
|
|
121824
|
+
if (!normalized || !identifier || normalized.identifier !== identifier) {
|
|
121825
|
+
return { reason: "invalid-correct-response" };
|
|
121826
|
+
}
|
|
121827
|
+
const cardinality = (raw.attributes.cardinality || "single").trim();
|
|
121828
|
+
if (!SUPPORTED_CARDINALITIES2.has(cardinality)) {
|
|
121829
|
+
return { reason: "unsupported-cardinality" };
|
|
121830
|
+
}
|
|
121831
|
+
const baseType = (raw.attributes["base-type"] || "string").trim();
|
|
121832
|
+
if (!SUPPORTED_BASE_TYPES2.has(baseType)) {
|
|
121833
|
+
return { reason: "unsupported-base-type" };
|
|
121834
|
+
}
|
|
121835
|
+
if (baseType === "directedPair" && cardinality !== "multiple") {
|
|
121836
|
+
return { reason: "unsupported-cardinality" };
|
|
121837
|
+
}
|
|
121838
|
+
const declaration = {
|
|
121839
|
+
identifier,
|
|
121840
|
+
cardinality,
|
|
121841
|
+
baseType,
|
|
121842
|
+
correctValues: [...normalized.correctValues]
|
|
121843
|
+
};
|
|
121844
|
+
if (declaration.correctValues.length === 0 || declaration.correctValues.some((value) => value.trim() === "") || declaration.cardinality === "single" && declaration.correctValues.length !== 1 && declaration.baseType !== "string") {
|
|
121845
|
+
return { reason: "invalid-correct-response" };
|
|
121846
|
+
}
|
|
121847
|
+
if (declaration.baseType === "directedPair" && (declaration.correctValues.some((value) => !DIRECTED_PAIR_VALUE.test(value)) || new Set(declaration.correctValues).size !== declaration.correctValues.length)) {
|
|
121848
|
+
return { reason: "invalid-correct-response" };
|
|
121849
|
+
}
|
|
121850
|
+
if ((declaration.baseType === "integer" || declaration.baseType === "float") && declaration.correctValues.some((value) => parsedNumericValue(value, declaration.baseType) === null)) {
|
|
121851
|
+
return { reason: "invalid-correct-response" };
|
|
121852
|
+
}
|
|
121853
|
+
return { declaration };
|
|
121854
|
+
}
|
|
121855
|
+
function scoringDeclarations(rawDeclarations, normalizedDeclarations) {
|
|
121856
|
+
const rawIdentifiers = rawDeclarations.map((declaration) => declaration.attributes.identifier?.trim());
|
|
121857
|
+
if (rawDeclarations.length !== normalizedDeclarations.length || rawIdentifiers.some((identifier) => !identifier) || new Set(rawIdentifiers).size !== rawIdentifiers.length) {
|
|
121858
|
+
return { reason: "invalid-correct-response" };
|
|
121859
|
+
}
|
|
121860
|
+
const declarations = new Map;
|
|
121861
|
+
for (const [index2, rawDeclaration] of rawDeclarations.entries()) {
|
|
121862
|
+
const normalized = normalizedDeclarations[index2];
|
|
121863
|
+
if (normalized?.correctValues.length) {
|
|
121864
|
+
const result = declarationResult(rawDeclaration, normalized);
|
|
121865
|
+
if ("reason" in result) {
|
|
121866
|
+
return result;
|
|
121867
|
+
}
|
|
121868
|
+
if (declarations.has(result.declaration.identifier)) {
|
|
121869
|
+
return { reason: "invalid-correct-response" };
|
|
121870
|
+
}
|
|
121871
|
+
declarations.set(result.declaration.identifier, result.declaration);
|
|
121872
|
+
}
|
|
121873
|
+
}
|
|
121874
|
+
return declarations.size > 0 ? { declarations } : { reason: "no-scorable-response" };
|
|
121875
|
+
}
|
|
121876
|
+
function declarationConstructReason(declarations) {
|
|
121877
|
+
if (declarations.some((declaration) => childElements(declaration, "qti-mapping").length > 0)) {
|
|
121878
|
+
return "response-mapping";
|
|
121879
|
+
}
|
|
121880
|
+
return declarations.some((declaration) => childElements(declaration, "qti-area-mapping").length > 0) ? "area-mapping" : null;
|
|
121881
|
+
}
|
|
121882
|
+
function sameScore(left, right) {
|
|
121883
|
+
return Math.abs(left - right) <= Number.EPSILON * Math.max(1, Math.abs(left), Math.abs(right));
|
|
121884
|
+
}
|
|
121885
|
+
function platformResult(itemIdentifier, parsedMaxScore, rules) {
|
|
121886
|
+
const ruleMaximum = rules.reduce((sum, rule) => sum + rule.points, 0);
|
|
121887
|
+
if (!Number.isFinite(ruleMaximum) || ruleMaximum <= 0) {
|
|
121888
|
+
return fallback(itemIdentifier, parsedMaxScore, "no-scorable-response");
|
|
121889
|
+
}
|
|
121890
|
+
if (!sameScore(parsedMaxScore, ruleMaximum)) {
|
|
121891
|
+
return fallback(itemIdentifier, parsedMaxScore, "inconsistent-max-score");
|
|
121892
|
+
}
|
|
121893
|
+
return {
|
|
121894
|
+
strategy: "platform",
|
|
121895
|
+
itemIdentifier,
|
|
121896
|
+
maxScore: ruleMaximum,
|
|
121897
|
+
rules
|
|
121898
|
+
};
|
|
121899
|
+
}
|
|
121900
|
+
function matchRule(declaration, points) {
|
|
121901
|
+
return {
|
|
121902
|
+
responseIdentifier: declaration.identifier,
|
|
121903
|
+
cardinality: declaration.cardinality,
|
|
121904
|
+
baseType: declaration.baseType,
|
|
121905
|
+
points,
|
|
121906
|
+
comparison: {
|
|
121907
|
+
kind: "match",
|
|
121908
|
+
correctValues: [...declaration.correctValues]
|
|
121909
|
+
}
|
|
121910
|
+
};
|
|
121911
|
+
}
|
|
121912
|
+
function operandIdentifiers(comparison) {
|
|
121913
|
+
const operands = childElements(comparison);
|
|
121914
|
+
if (operands.length !== 2 || !onlyWhitespaceText(comparison.children) || operands.some((operand) => !onlyAttributes(operand, ["identifier"]))) {
|
|
121915
|
+
return null;
|
|
121916
|
+
}
|
|
121917
|
+
const variable = operands.find((operand) => operand.localName === "qti-variable");
|
|
121918
|
+
const correct = operands.find((operand) => operand.localName === "qti-correct");
|
|
121919
|
+
if (!variable || !correct || childElements(variable).length > 0 || childElements(correct).length > 0) {
|
|
121920
|
+
return null;
|
|
121921
|
+
}
|
|
121922
|
+
if (!onlyWhitespaceText(variable.children) || !onlyWhitespaceText(correct.children)) {
|
|
121923
|
+
return null;
|
|
121924
|
+
}
|
|
121925
|
+
const variableIdentifier = variable.attributes.identifier?.trim();
|
|
121926
|
+
const correctIdentifier = correct.attributes.identifier?.trim();
|
|
121927
|
+
return variableIdentifier && correctIdentifier ? { variable: variableIdentifier, correct: correctIdentifier } : null;
|
|
121928
|
+
}
|
|
121929
|
+
function comparisonResult(comparison, declaration) {
|
|
121930
|
+
const identifiers = operandIdentifiers(comparison);
|
|
121931
|
+
if (!identifiers || identifiers.variable !== declaration.identifier || identifiers.correct !== declaration.identifier) {
|
|
121932
|
+
return null;
|
|
121933
|
+
}
|
|
121934
|
+
if (comparison.localName === "qti-match" && onlyAttributes(comparison, [])) {
|
|
121935
|
+
return {
|
|
121936
|
+
kind: "match",
|
|
121937
|
+
correctValues: [...declaration.correctValues]
|
|
121938
|
+
};
|
|
121939
|
+
}
|
|
121940
|
+
const correctValue = numericCorrectValue(declaration);
|
|
121941
|
+
if (correctValue === null) {
|
|
121942
|
+
return null;
|
|
121943
|
+
}
|
|
121944
|
+
if (comparison.localName === "qti-equal" && onlyAttributes(comparison, [])) {
|
|
121945
|
+
return { kind: "numeric-equal", correctValue };
|
|
121946
|
+
}
|
|
121947
|
+
if (comparison.localName !== "qti-equal-rounded" || !onlyAttributes(comparison, ["rounding-mode", "figures"])) {
|
|
121948
|
+
return null;
|
|
121949
|
+
}
|
|
121950
|
+
const roundingMode = comparison.attributes["rounding-mode"];
|
|
121951
|
+
const figures = Number(comparison.attributes.figures);
|
|
121952
|
+
const validFigures = Number.isInteger(figures) && (roundingMode === "decimalPlaces" ? figures >= 0 : figures > 0);
|
|
121953
|
+
if (roundingMode !== "decimalPlaces" && roundingMode !== "significantFigures" || !validFigures) {
|
|
121954
|
+
return null;
|
|
121955
|
+
}
|
|
121956
|
+
return {
|
|
121957
|
+
kind: "numeric-equal-rounded",
|
|
121958
|
+
correctValue,
|
|
121959
|
+
roundingMode,
|
|
121960
|
+
figures
|
|
121961
|
+
};
|
|
121962
|
+
}
|
|
121963
|
+
function responseReferences(node, responseIdentifiers) {
|
|
121964
|
+
return new Set(xmlDescendants(node, (candidate) => candidate.localName === "qti-variable" || candidate.localName === "qti-correct").map((candidate) => candidate.attributes.identifier?.trim()).filter((identifier) => Boolean(identifier) && responseIdentifiers.has(identifier)));
|
|
121965
|
+
}
|
|
121966
|
+
function additivePoints(setOutcome) {
|
|
121967
|
+
if (!onlyAttributes(setOutcome, ["identifier"]) || setOutcome.attributes.identifier !== "SCORE" || !onlyWhitespaceText(setOutcome.children)) {
|
|
121968
|
+
return null;
|
|
121969
|
+
}
|
|
121970
|
+
const outcomeChildren = childElements(setOutcome);
|
|
121971
|
+
if (outcomeChildren.length !== 1 || outcomeChildren[0]?.localName !== "qti-sum") {
|
|
121972
|
+
return null;
|
|
121973
|
+
}
|
|
121974
|
+
const sum = outcomeChildren[0];
|
|
121975
|
+
if (!onlyAttributes(sum, []) || !onlyWhitespaceText(sum.children)) {
|
|
121976
|
+
return null;
|
|
121977
|
+
}
|
|
121978
|
+
const operands = childElements(sum);
|
|
121979
|
+
const scoreVariable = operands.find((operand) => operand.localName === "qti-variable" && operand.attributes.identifier === "SCORE");
|
|
121980
|
+
const pointValue = operands.find((operand) => operand.localName === "qti-base-value");
|
|
121981
|
+
if (operands.length !== 2 || !scoreVariable || !pointValue || !onlyAttributes(scoreVariable, ["identifier"]) || !onlyWhitespaceText(scoreVariable.children) || childElements(scoreVariable).length > 0 || !onlyAttributes(pointValue, ["base-type"]) || pointValue.attributes["base-type"] !== "float") {
|
|
121982
|
+
return null;
|
|
121983
|
+
}
|
|
121984
|
+
const rawPoints = directText(pointValue);
|
|
121985
|
+
const points = rawPoints === null ? Number.NaN : Number(rawPoints);
|
|
121986
|
+
return Number.isFinite(points) && points > 0 ? points : null;
|
|
121987
|
+
}
|
|
121988
|
+
function additiveRule(condition, declarations) {
|
|
121989
|
+
const responseIdentifiers = new Set(declarations.keys());
|
|
121990
|
+
const references = responseReferences(condition, responseIdentifiers);
|
|
121991
|
+
if (references.size > 1) {
|
|
121992
|
+
return { reason: "cross-response-processing" };
|
|
121993
|
+
}
|
|
121994
|
+
if (references.size !== 1 || !onlyAttributes(condition, []) || !onlyWhitespaceText(condition.children)) {
|
|
121995
|
+
return { reason: "unsupported-response-processing" };
|
|
121996
|
+
}
|
|
121997
|
+
const conditionChildren = childElements(condition);
|
|
121998
|
+
if (conditionChildren.length !== 1 || conditionChildren[0]?.localName !== "qti-response-if") {
|
|
121999
|
+
return { reason: "unsupported-response-processing" };
|
|
122000
|
+
}
|
|
122001
|
+
const responseIf = conditionChildren[0];
|
|
122002
|
+
if (!onlyAttributes(responseIf, []) || !onlyWhitespaceText(responseIf.children)) {
|
|
122003
|
+
return { reason: "unsupported-response-processing" };
|
|
122004
|
+
}
|
|
122005
|
+
const responseIfChildren = childElements(responseIf);
|
|
122006
|
+
if (responseIfChildren.length !== 2) {
|
|
122007
|
+
return { reason: "unsupported-response-processing" };
|
|
122008
|
+
}
|
|
122009
|
+
const [comparison, setOutcome] = responseIfChildren;
|
|
122010
|
+
if (!comparison || !setOutcome || !["qti-match", "qti-equal", "qti-equal-rounded"].includes(comparison.localName) || setOutcome.localName !== "qti-set-outcome-value") {
|
|
122011
|
+
return { reason: "unsupported-response-processing" };
|
|
122012
|
+
}
|
|
122013
|
+
const responseIdentifier = [...references][0];
|
|
122014
|
+
const declaration = declarations.get(responseIdentifier);
|
|
122015
|
+
const points = additivePoints(setOutcome);
|
|
122016
|
+
const comparisonPayload = declaration ? comparisonResult(comparison, declaration) : null;
|
|
122017
|
+
if (!declaration || points === null || !comparisonPayload) {
|
|
122018
|
+
return { reason: "unsupported-response-processing" };
|
|
122019
|
+
}
|
|
122020
|
+
return {
|
|
122021
|
+
rule: {
|
|
122022
|
+
responseIdentifier,
|
|
122023
|
+
cardinality: declaration.cardinality,
|
|
122024
|
+
baseType: declaration.baseType,
|
|
122025
|
+
points,
|
|
122026
|
+
comparison: comparisonPayload
|
|
122027
|
+
}
|
|
122028
|
+
};
|
|
122029
|
+
}
|
|
122030
|
+
function additiveRules(processingRules, declarations) {
|
|
122031
|
+
const rules = [];
|
|
122032
|
+
const scored = new Set;
|
|
122033
|
+
for (const condition of processingRules) {
|
|
122034
|
+
const result = additiveRule(condition, declarations);
|
|
122035
|
+
if ("reason" in result) {
|
|
122036
|
+
return result;
|
|
122037
|
+
}
|
|
122038
|
+
if (scored.has(result.rule.responseIdentifier)) {
|
|
122039
|
+
return { reason: "unsupported-response-processing" };
|
|
122040
|
+
}
|
|
122041
|
+
scored.add(result.rule.responseIdentifier);
|
|
122042
|
+
rules.push(result.rule);
|
|
122043
|
+
}
|
|
122044
|
+
if (scored.size !== declarations.size || [...declarations.keys()].some((identifier) => !scored.has(identifier))) {
|
|
122045
|
+
return { reason: "unsupported-response-processing" };
|
|
122046
|
+
}
|
|
122047
|
+
return { rules };
|
|
122048
|
+
}
|
|
122049
|
+
function scoreDefaultIsZero(item) {
|
|
122050
|
+
const scoreOutcomes = childElements(item, "qti-outcome-declaration").filter((outcome) => outcome.attributes.identifier === "SCORE");
|
|
122051
|
+
if (scoreOutcomes.length !== 1) {
|
|
122052
|
+
return false;
|
|
122053
|
+
}
|
|
122054
|
+
const scoreOutcome = scoreOutcomes[0];
|
|
122055
|
+
if (!onlyAttributes(scoreOutcome, ["identifier", "cardinality", "base-type"]) || scoreOutcome.attributes.cardinality !== "single" || scoreOutcome.attributes["base-type"] !== "float") {
|
|
122056
|
+
return false;
|
|
122057
|
+
}
|
|
122058
|
+
const defaults = childElements(scoreOutcome, "qti-default-value");
|
|
122059
|
+
if (defaults.length !== 1) {
|
|
122060
|
+
return false;
|
|
122061
|
+
}
|
|
122062
|
+
const values = childElements(defaults[0], "qti-value");
|
|
122063
|
+
const defaultText = values.length === 1 ? directText(values[0]) : null;
|
|
122064
|
+
return defaultText !== null && defaultText !== "" && Number(defaultText) === 0;
|
|
122065
|
+
}
|
|
122066
|
+
function compileResponseProcessing(item, itemIdentifier, maxScore, declarations) {
|
|
122067
|
+
const processingElements = childElements(item, "qti-response-processing");
|
|
122068
|
+
if (processingElements.length === 0) {
|
|
122069
|
+
return platformResult(itemIdentifier, maxScore, [...declarations.values()].map((declaration) => matchRule(declaration, 1)));
|
|
122070
|
+
}
|
|
122071
|
+
if (processingElements.length !== 1) {
|
|
122072
|
+
return fallback(itemIdentifier, maxScore, "unsupported-response-processing");
|
|
122073
|
+
}
|
|
122074
|
+
const processing = processingElements[0];
|
|
122075
|
+
const template = processing.attributes.template;
|
|
122076
|
+
const processingRules = childElements(processing);
|
|
122077
|
+
if (template !== undefined) {
|
|
122078
|
+
if (!MATCH_CORRECT_TEMPLATES2.has(template.trim())) {
|
|
122079
|
+
return fallback(itemIdentifier, maxScore, "unsupported-processing-template");
|
|
122080
|
+
}
|
|
122081
|
+
if (processingRules.length > 0 || !onlyWhitespaceText(processing.children) || !onlyAttributes(processing, ["template"])) {
|
|
122082
|
+
return fallback(itemIdentifier, maxScore, "unsupported-response-processing");
|
|
122083
|
+
}
|
|
122084
|
+
const response = declarations.get("RESPONSE");
|
|
122085
|
+
return response && declarations.size === 1 ? platformResult(itemIdentifier, maxScore, [matchRule(response, 1)]) : fallback(itemIdentifier, maxScore, "unsupported-response-processing");
|
|
122086
|
+
}
|
|
122087
|
+
if (!onlyAttributes(processing, []) || !onlyWhitespaceText(processing.children) || processingRules.length === 0 || processingRules.some((rule) => rule.localName !== "qti-response-condition") || !scoreDefaultIsZero(item)) {
|
|
122088
|
+
return fallback(itemIdentifier, maxScore, "unsupported-response-processing");
|
|
122089
|
+
}
|
|
122090
|
+
const compiled = additiveRules(processingRules, declarations);
|
|
122091
|
+
return "reason" in compiled ? fallback(itemIdentifier, maxScore, compiled.reason) : platformResult(itemIdentifier, maxScore, compiled.rules);
|
|
122092
|
+
}
|
|
122093
|
+
function compileQtiScoringArtifactItem(rawXml) {
|
|
122094
|
+
let root;
|
|
122095
|
+
let parsed;
|
|
122096
|
+
try {
|
|
122097
|
+
root = parseXmlDocument(rawXml);
|
|
122098
|
+
parsed = parseQtiItemXml(rawXml);
|
|
122099
|
+
} catch {
|
|
122100
|
+
throw new QtiScoringArtifactValidationError("QTI assessment item contains invalid XML");
|
|
122101
|
+
}
|
|
122102
|
+
const item = itemElement(root);
|
|
122103
|
+
if (!item) {
|
|
122104
|
+
throw new QtiScoringArtifactValidationError("QTI XML is missing a qti-assessment-item document element");
|
|
122105
|
+
}
|
|
122106
|
+
const itemIdentifier = parsed.identifier.trim();
|
|
122107
|
+
if (!itemIdentifier) {
|
|
122108
|
+
throw new QtiScoringArtifactValidationError("QTI assessment item is missing an identifier");
|
|
122109
|
+
}
|
|
122110
|
+
const maxScore = qtiItemMaxScore(parsed);
|
|
122111
|
+
const rawDeclarations = childElements(item, "qti-response-declaration");
|
|
122112
|
+
const constructReason = declarationConstructReason(rawDeclarations);
|
|
122113
|
+
if (constructReason) {
|
|
122114
|
+
return fallback(itemIdentifier, maxScore, constructReason);
|
|
122115
|
+
}
|
|
122116
|
+
if (childElements(item, "qti-template-processing").length > 0 || childElements(item, "qti-template-declaration").length > 0) {
|
|
122117
|
+
return fallback(itemIdentifier, maxScore, "unsupported-response-processing");
|
|
122118
|
+
}
|
|
122119
|
+
const compiledDeclarations = scoringDeclarations(rawDeclarations, parsed.responseDeclarations);
|
|
122120
|
+
return "reason" in compiledDeclarations ? fallback(itemIdentifier, maxScore, compiledDeclarations.reason) : compileResponseProcessing(item, itemIdentifier, maxScore, compiledDeclarations.declarations);
|
|
122121
|
+
}
|
|
122122
|
+
function compileQtiScoringArtifact(questions) {
|
|
122123
|
+
const identifiers = new Set;
|
|
122124
|
+
const items = questions.questions.map(({ question, reference }) => {
|
|
122125
|
+
if (!question.rawXml) {
|
|
122126
|
+
throw new QtiScoringArtifactValidationError(`QTI question ${reference.identifier} is missing raw XML`);
|
|
122127
|
+
}
|
|
122128
|
+
const item = compileQtiScoringArtifactItem(question.rawXml);
|
|
122129
|
+
if (item.itemIdentifier !== question.identifier) {
|
|
122130
|
+
throw new QtiScoringArtifactValidationError(`Hydrated QTI question ${question.identifier} contains item ${item.itemIdentifier}`);
|
|
122131
|
+
}
|
|
122132
|
+
if (identifiers.has(item.itemIdentifier)) {
|
|
122133
|
+
throw new QtiScoringArtifactValidationError(`QTI questions contain duplicate item ${item.itemIdentifier}`);
|
|
122134
|
+
}
|
|
122135
|
+
identifiers.add(item.itemIdentifier);
|
|
122136
|
+
return item;
|
|
122137
|
+
});
|
|
122138
|
+
return { items };
|
|
122139
|
+
}
|
|
121442
122140
|
function isRecord22(value) {
|
|
121443
122141
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
121444
122142
|
}
|
|
@@ -121638,6 +122336,11 @@ var BLANK_SENTINEL22 = "
|
|
|
121638
122336
|
var PLAYABLE_POINT_VALUE;
|
|
121639
122337
|
var POINT_INTERACTION_TYPES2;
|
|
121640
122338
|
var INDEPENDENT_PROCESSING_TAGS2;
|
|
122339
|
+
var SUPPORTED_BASE_TYPES2;
|
|
122340
|
+
var SUPPORTED_CARDINALITIES2;
|
|
122341
|
+
var MATCH_CORRECT_TEMPLATES2;
|
|
122342
|
+
var DIRECTED_PAIR_VALUE;
|
|
122343
|
+
var QtiScoringArtifactValidationError;
|
|
121641
122344
|
var COMMON_CORE_MATH_FRAMEWORK_ALIASES2;
|
|
121642
122345
|
var COMMON_CORE_ELA_FRAMEWORK_ALIASES2;
|
|
121643
122346
|
var COMMON_CORE_FRAMEWORK_ALIASES2;
|
|
@@ -121890,6 +122593,30 @@ var init_qti = __esm(() => {
|
|
|
121890
122593
|
"qti-sum",
|
|
121891
122594
|
"qti-base-value"
|
|
121892
122595
|
]);
|
|
122596
|
+
SUPPORTED_BASE_TYPES2 = new Set([
|
|
122597
|
+
"string",
|
|
122598
|
+
"identifier",
|
|
122599
|
+
"integer",
|
|
122600
|
+
"float",
|
|
122601
|
+
"directedPair"
|
|
122602
|
+
]);
|
|
122603
|
+
SUPPORTED_CARDINALITIES2 = new Set([
|
|
122604
|
+
"single",
|
|
122605
|
+
"multiple",
|
|
122606
|
+
"ordered"
|
|
122607
|
+
]);
|
|
122608
|
+
MATCH_CORRECT_TEMPLATES2 = new Set([
|
|
122609
|
+
"http://www.imsglobal.org/question/qti_v3p0/rptemplates/match_correct",
|
|
122610
|
+
"https://www.imsglobal.org/question/qti_v3p0/rptemplates/match_correct",
|
|
122611
|
+
"https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/match_correct"
|
|
122612
|
+
].flatMap((template) => [template, `${template}.xml`]));
|
|
122613
|
+
DIRECTED_PAIR_VALUE = /^[A-Za-z_][\w.-]* [A-Za-z_][\w.-]*$/;
|
|
122614
|
+
QtiScoringArtifactValidationError = class QtiScoringArtifactValidationError2 extends Error {
|
|
122615
|
+
constructor(message) {
|
|
122616
|
+
super(message);
|
|
122617
|
+
this.name = "QtiScoringArtifactValidationError";
|
|
122618
|
+
}
|
|
122619
|
+
};
|
|
121893
122620
|
COMMON_CORE_MATH_FRAMEWORK_ALIASES2 = new Set([
|
|
121894
122621
|
"ccssm",
|
|
121895
122622
|
"ccssmath",
|
|
@@ -121913,11 +122640,11 @@ var init_qti = __esm(() => {
|
|
|
121913
122640
|
]);
|
|
121914
122641
|
COMMON_CORE_FRAMEWORK_ALIASES2 = new Set(["ccss", "commoncore", "commoncorestatestandards"]);
|
|
121915
122642
|
});
|
|
121916
|
-
function
|
|
122643
|
+
function isRecord7(value) {
|
|
121917
122644
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
121918
122645
|
}
|
|
121919
122646
|
function metadataOf(record3) {
|
|
121920
|
-
return
|
|
122647
|
+
return isRecord7(record3) && isRecord7(record3.metadata) ? record3.metadata : {};
|
|
121921
122648
|
}
|
|
121922
122649
|
function stringField3(value) {
|
|
121923
122650
|
return typeof value === "string" ? value : "";
|
|
@@ -121929,7 +122656,7 @@ function numberField(value) {
|
|
|
121929
122656
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
121930
122657
|
}
|
|
121931
122658
|
function sourceIdFromRef(value) {
|
|
121932
|
-
return
|
|
122659
|
+
return isRecord7(value) ? stringField3(value.sourcedId) : "";
|
|
121933
122660
|
}
|
|
121934
122661
|
function buildGradeLevelTestResultWhere(options) {
|
|
121935
122662
|
return {
|
|
@@ -122005,7 +122732,7 @@ function mapGradeLevelTestSummary(result, options) {
|
|
|
122005
122732
|
};
|
|
122006
122733
|
}
|
|
122007
122734
|
function readLearningObjectiveSets(record3) {
|
|
122008
|
-
if (!
|
|
122735
|
+
if (!isRecord7(record3)) {
|
|
122009
122736
|
return [];
|
|
122010
122737
|
}
|
|
122011
122738
|
const metadata2 = metadataOf(record3);
|
|
@@ -122035,7 +122762,7 @@ function dedupeStandards3(standards) {
|
|
|
122035
122762
|
function standardsFromLineItem(lineItem) {
|
|
122036
122763
|
const standards = [];
|
|
122037
122764
|
for (const set3 of readLearningObjectiveSets(lineItem)) {
|
|
122038
|
-
if (
|
|
122765
|
+
if (isRecord7(set3)) {
|
|
122039
122766
|
const source = stringField3(set3.source) || "OneRoster";
|
|
122040
122767
|
const ids = Array.isArray(set3.learningObjectiveIds) ? set3.learningObjectiveIds : [];
|
|
122041
122768
|
for (const id of ids) {
|
|
@@ -122051,11 +122778,11 @@ function standardsFromLineItem(lineItem) {
|
|
|
122051
122778
|
function standardsFromResult(result) {
|
|
122052
122779
|
const standards = [];
|
|
122053
122780
|
for (const set3 of readLearningObjectiveSets(result)) {
|
|
122054
|
-
if (
|
|
122781
|
+
if (isRecord7(set3)) {
|
|
122055
122782
|
const source = stringField3(set3.source) || "OneRoster";
|
|
122056
122783
|
const values = Array.isArray(set3.learningObjectiveResults) ? set3.learningObjectiveResults : [];
|
|
122057
122784
|
for (const item of values) {
|
|
122058
|
-
if (
|
|
122785
|
+
if (isRecord7(item)) {
|
|
122059
122786
|
const id = stringField3(item.learningObjectiveId).trim();
|
|
122060
122787
|
if (id) {
|
|
122061
122788
|
standards.push({ source, id });
|
|
@@ -122067,7 +122794,7 @@ function standardsFromResult(result) {
|
|
|
122067
122794
|
return dedupeStandards3(standards);
|
|
122068
122795
|
}
|
|
122069
122796
|
function standardsFromQtiQuestion(questionRef) {
|
|
122070
|
-
const question =
|
|
122797
|
+
const question = isRecord7(questionRef) && isRecord7(questionRef.question) ? questionRef.question : null;
|
|
122071
122798
|
return qtiStandardsFromMetadata(metadataOf(question));
|
|
122072
122799
|
}
|
|
122073
122800
|
function enrichStandardsFromQti(standards, qtiStandards) {
|
|
@@ -122204,8 +122931,8 @@ function mapGradeLevelReviewQuestions(options) {
|
|
|
122204
122931
|
return options.questionLineItems.toSorted((a, b) => naturalTitleSort.compare(a.title, b.title)).map((lineItem) => {
|
|
122205
122932
|
const result = options.latestResultsByLineItem.get(lineItem.sourcedId ?? "");
|
|
122206
122933
|
const resultMetadata = metadataOf(result);
|
|
122207
|
-
const attemptedQuestionData =
|
|
122208
|
-
const attemptedResult =
|
|
122934
|
+
const attemptedQuestionData = isRecord7(resultMetadata.attemptedQuestionData) ? resultMetadata.attemptedQuestionData : null;
|
|
122935
|
+
const attemptedResult = isRecord7(attemptedQuestionData?.result) ? attemptedQuestionData.result : null;
|
|
122209
122936
|
const index2 = parseQuestionIndex(lineItem.title);
|
|
122210
122937
|
const qtiQuestion = index2 ? qtiQuestionByIndex.get(index2) ?? options.qtiQuestions[index2 - 1] : undefined;
|
|
122211
122938
|
const qtiStandards = standardsFromQtiQuestion(qtiQuestion);
|
|
@@ -124245,6 +124972,466 @@ var init_timeback_admin_service = __esm(async () => {
|
|
|
124245
124972
|
init_timeback_mastery_completion_util()
|
|
124246
124973
|
]);
|
|
124247
124974
|
});
|
|
124975
|
+
function normalizedKey(key) {
|
|
124976
|
+
return key.replaceAll(/[^a-z0-9]/gi, "").toLowerCase();
|
|
124977
|
+
}
|
|
124978
|
+
function containsPrivatePlayableKey(value) {
|
|
124979
|
+
if (Array.isArray(value)) {
|
|
124980
|
+
return value.some(containsPrivatePlayableKey);
|
|
124981
|
+
}
|
|
124982
|
+
if (!isRecord(value)) {
|
|
124983
|
+
return false;
|
|
124984
|
+
}
|
|
124985
|
+
return Object.entries(value).some(([key, entry2]) => PRIVATE_PLAYABLE_KEYS.has(normalizedKey(key)) || containsPrivatePlayableKey(entry2));
|
|
124986
|
+
}
|
|
124987
|
+
function playableSchema(identity) {
|
|
124988
|
+
return exports_external.object({
|
|
124989
|
+
contractVersion: exports_external.literal(PLAYCADEMY_ASSESSMENT_CONTRACT_VERSION),
|
|
124990
|
+
identifier: exports_external.literal(identity.qtiTestIdentifier),
|
|
124991
|
+
contentRevision: exports_external.literal(identity.kind === "assessment" ? identity.contentRevision : identity.sourceContentRevision),
|
|
124992
|
+
title: trimmedNonEmptyString,
|
|
124993
|
+
items: exports_external.array(playableItemSchema)
|
|
124994
|
+
}).strict();
|
|
124995
|
+
}
|
|
124996
|
+
function scoringSchema(identity) {
|
|
124997
|
+
if (identity.kind === "assessment") {
|
|
124998
|
+
return exports_external.object({
|
|
124999
|
+
items: uniqueScoringItemsSchema,
|
|
125000
|
+
reviewBank: exports_external.undefined(),
|
|
125001
|
+
diagnostic: diagnosticContextSchema.optional()
|
|
125002
|
+
});
|
|
125003
|
+
}
|
|
125004
|
+
return exports_external.object({
|
|
125005
|
+
items: uniqueScoringItemsSchema,
|
|
125006
|
+
reviewBank: exports_external.object({
|
|
125007
|
+
sourceContentRevision: exports_external.literal(identity.sourceContentRevision),
|
|
125008
|
+
bankRevision: exports_external.literal(identity.bankRevision),
|
|
125009
|
+
items: exports_external.array(exports_external.object({
|
|
125010
|
+
itemIdentifier: trimmedNonEmptyString,
|
|
125011
|
+
standards: exports_external.array(exports_external.unknown())
|
|
125012
|
+
}))
|
|
125013
|
+
}),
|
|
125014
|
+
diagnostic: exports_external.undefined()
|
|
125015
|
+
}).refine((payload) => payload.reviewBank.items.length === payload.items.length && payload.reviewBank.items.every((item, index2) => item.itemIdentifier === payload.items[index2]?.itemIdentifier));
|
|
125016
|
+
}
|
|
125017
|
+
function assertPlayableAssessmentArtifact(payload, identity) {
|
|
125018
|
+
if (!playableSchema(identity).safeParse(payload).success || containsPrivatePlayableKey(payload)) {
|
|
125019
|
+
throw new ValidationError("Playable assessment artifact payload is invalid or private");
|
|
125020
|
+
}
|
|
125021
|
+
}
|
|
125022
|
+
function assertScoringAssessmentArtifact(payload, identity) {
|
|
125023
|
+
if (!scoringSchema(identity).safeParse(payload).success) {
|
|
125024
|
+
throw new ValidationError("Scoring assessment artifact payload is invalid or incompatible");
|
|
125025
|
+
}
|
|
125026
|
+
}
|
|
125027
|
+
function assertScoringArtifactMatchesPlayable(assessment, scoring, identity) {
|
|
125028
|
+
const scoringByIdentifier = new Map(scoring.items.map((item) => [item.itemIdentifier, item.maxScore]));
|
|
125029
|
+
const playableMatches = assessment.items.every((item) => scoringByIdentifier.get(item.identifier) === item.maxScore);
|
|
125030
|
+
const hasExactManagedMembership = identity.kind === "review-bank" || scoring.items.length === assessment.items.length;
|
|
125031
|
+
if (!playableMatches || !hasExactManagedMembership) {
|
|
125032
|
+
throw new ValidationError("Scoring artifact does not match the playable assessment");
|
|
125033
|
+
}
|
|
125034
|
+
}
|
|
125035
|
+
function assertPlayableArtifactContainsItems(assessment, itemIdentifiers) {
|
|
125036
|
+
const available = new Set(assessment.items.map((item) => item.identifier));
|
|
125037
|
+
const missing = [...new Set(itemIdentifiers)].filter((identifier) => !available.has(identifier));
|
|
125038
|
+
if (missing.length > 0) {
|
|
125039
|
+
throw new ValidationError("Playable assessment artifact is missing required items", {
|
|
125040
|
+
missingItemIdentifiers: missing
|
|
125041
|
+
});
|
|
125042
|
+
}
|
|
125043
|
+
}
|
|
125044
|
+
function assessmentArtifactIdentityFromAttempt(metadata2) {
|
|
125045
|
+
return metadata2.purpose === "review" ? {
|
|
125046
|
+
kind: "review-bank",
|
|
125047
|
+
qtiTestIdentifier: metadata2.selectedTest.identifier,
|
|
125048
|
+
sourceContentRevision: metadata2.selectedTest.contentRevision,
|
|
125049
|
+
bankRevision: metadata2.review.bankRevision
|
|
125050
|
+
} : {
|
|
125051
|
+
kind: "assessment",
|
|
125052
|
+
qtiTestIdentifier: metadata2.selectedTest.identifier,
|
|
125053
|
+
contentRevision: metadata2.selectedTest.contentRevision
|
|
125054
|
+
};
|
|
125055
|
+
}
|
|
125056
|
+
function pinnedReviewBankFromAttempt(metadata2, bankIdentifier) {
|
|
125057
|
+
return {
|
|
125058
|
+
bankIdentifier,
|
|
125059
|
+
sourceContentRevision: metadata2.selectedTest.contentRevision,
|
|
125060
|
+
bankRevision: metadata2.review.bankRevision,
|
|
125061
|
+
items: metadata2.review.selections.map((selection) => ({
|
|
125062
|
+
itemIdentifier: selection.itemIdentifier,
|
|
125063
|
+
standards: [selection.standard]
|
|
125064
|
+
}))
|
|
125065
|
+
};
|
|
125066
|
+
}
|
|
125067
|
+
var PRIVATE_PLAYABLE_KEYS;
|
|
125068
|
+
var trimmedNonEmptyString;
|
|
125069
|
+
var finiteNumber2;
|
|
125070
|
+
var nonnegativeInteger;
|
|
125071
|
+
var playableContentNodesSchema;
|
|
125072
|
+
var playableChoiceSchema;
|
|
125073
|
+
var playableMatchChoiceSchema;
|
|
125074
|
+
var playableGraphicSchema;
|
|
125075
|
+
var playableHotspotSchema;
|
|
125076
|
+
var playableGapTextSchema;
|
|
125077
|
+
var playableGapImageSchema;
|
|
125078
|
+
var playableInteractionBase;
|
|
125079
|
+
var playableInteractionSchema;
|
|
125080
|
+
var playableItemSchema;
|
|
125081
|
+
var scoringRuleSchema;
|
|
125082
|
+
var scoringItemSchema;
|
|
125083
|
+
var uniqueScoringItemsSchema;
|
|
125084
|
+
var diagnosticContextSchema;
|
|
125085
|
+
var init_assessment_artifact_runtime_util = __esm(() => {
|
|
125086
|
+
init_esm();
|
|
125087
|
+
init_src();
|
|
125088
|
+
init_errors2();
|
|
125089
|
+
PRIVATE_PLAYABLE_KEYS = new Set([
|
|
125090
|
+
"answer",
|
|
125091
|
+
"answerkey",
|
|
125092
|
+
"correctanswer",
|
|
125093
|
+
"correctresponse",
|
|
125094
|
+
"correctresponses",
|
|
125095
|
+
"correct",
|
|
125096
|
+
"correctvalue",
|
|
125097
|
+
"correctvalues",
|
|
125098
|
+
"iscorrect",
|
|
125099
|
+
"responseprocessing",
|
|
125100
|
+
"rule",
|
|
125101
|
+
"rules",
|
|
125102
|
+
"scoring",
|
|
125103
|
+
"scoringrules",
|
|
125104
|
+
"solution",
|
|
125105
|
+
"expected",
|
|
125106
|
+
"expectedanswer",
|
|
125107
|
+
"expectedvalue",
|
|
125108
|
+
"expectedvalues"
|
|
125109
|
+
]);
|
|
125110
|
+
trimmedNonEmptyString = exports_external.string().min(1).refine((value) => value === value.trim());
|
|
125111
|
+
finiteNumber2 = exports_external.number().finite();
|
|
125112
|
+
nonnegativeInteger = exports_external.number().int().nonnegative();
|
|
125113
|
+
playableContentNodesSchema = exports_external.array(exports_external.unknown());
|
|
125114
|
+
playableChoiceSchema = exports_external.object({
|
|
125115
|
+
identifier: trimmedNonEmptyString,
|
|
125116
|
+
content: exports_external.string(),
|
|
125117
|
+
contentNodes: playableContentNodesSchema.optional()
|
|
125118
|
+
}).strict();
|
|
125119
|
+
playableMatchChoiceSchema = exports_external.object({
|
|
125120
|
+
identifier: trimmedNonEmptyString,
|
|
125121
|
+
content: exports_external.string(),
|
|
125122
|
+
contentNodes: playableContentNodesSchema.optional(),
|
|
125123
|
+
matchMax: nonnegativeInteger
|
|
125124
|
+
}).strict();
|
|
125125
|
+
playableGraphicSchema = exports_external.object({
|
|
125126
|
+
src: trimmedNonEmptyString,
|
|
125127
|
+
width: finiteNumber2.optional(),
|
|
125128
|
+
height: finiteNumber2.optional(),
|
|
125129
|
+
description: exports_external.string().optional()
|
|
125130
|
+
}).strict();
|
|
125131
|
+
playableHotspotSchema = exports_external.object({
|
|
125132
|
+
identifier: trimmedNonEmptyString,
|
|
125133
|
+
shape: exports_external.enum(["circle", "ellipse", "rect", "poly", "default"]),
|
|
125134
|
+
coords: exports_external.array(finiteNumber2),
|
|
125135
|
+
matchMax: nonnegativeInteger.optional(),
|
|
125136
|
+
description: exports_external.string().optional()
|
|
125137
|
+
}).strict();
|
|
125138
|
+
playableGapTextSchema = exports_external.object({
|
|
125139
|
+
identifier: trimmedNonEmptyString,
|
|
125140
|
+
content: exports_external.string(),
|
|
125141
|
+
matchMax: nonnegativeInteger
|
|
125142
|
+
}).strict();
|
|
125143
|
+
playableGapImageSchema = exports_external.object({
|
|
125144
|
+
identifier: trimmedNonEmptyString,
|
|
125145
|
+
image: playableGraphicSchema.optional(),
|
|
125146
|
+
matchMax: nonnegativeInteger,
|
|
125147
|
+
description: exports_external.string().optional()
|
|
125148
|
+
}).strict();
|
|
125149
|
+
playableInteractionBase = {
|
|
125150
|
+
responseIdentifier: trimmedNonEmptyString,
|
|
125151
|
+
prompt: exports_external.string().optional()
|
|
125152
|
+
};
|
|
125153
|
+
playableInteractionSchema = exports_external.discriminatedUnion("type", [
|
|
125154
|
+
exports_external.object({
|
|
125155
|
+
...playableInteractionBase,
|
|
125156
|
+
type: exports_external.literal("choice"),
|
|
125157
|
+
choices: exports_external.array(playableChoiceSchema),
|
|
125158
|
+
minChoices: nonnegativeInteger,
|
|
125159
|
+
maxChoices: nonnegativeInteger
|
|
125160
|
+
}).strict(),
|
|
125161
|
+
exports_external.object({
|
|
125162
|
+
...playableInteractionBase,
|
|
125163
|
+
type: exports_external.literal("inline-choice"),
|
|
125164
|
+
choices: exports_external.array(playableChoiceSchema)
|
|
125165
|
+
}).strict(),
|
|
125166
|
+
exports_external.object({
|
|
125167
|
+
...playableInteractionBase,
|
|
125168
|
+
type: exports_external.literal("text-entry"),
|
|
125169
|
+
expectedLength: nonnegativeInteger.optional(),
|
|
125170
|
+
placeholder: exports_external.string().optional()
|
|
125171
|
+
}).strict(),
|
|
125172
|
+
exports_external.object({
|
|
125173
|
+
...playableInteractionBase,
|
|
125174
|
+
type: exports_external.literal("order"),
|
|
125175
|
+
choices: exports_external.array(playableChoiceSchema),
|
|
125176
|
+
shuffle: exports_external.boolean()
|
|
125177
|
+
}).strict(),
|
|
125178
|
+
exports_external.object({
|
|
125179
|
+
...playableInteractionBase,
|
|
125180
|
+
type: exports_external.literal("match"),
|
|
125181
|
+
sourceChoices: exports_external.array(playableMatchChoiceSchema),
|
|
125182
|
+
targetChoices: exports_external.array(playableMatchChoiceSchema),
|
|
125183
|
+
maxAssociations: nonnegativeInteger,
|
|
125184
|
+
shuffle: exports_external.boolean()
|
|
125185
|
+
}).strict(),
|
|
125186
|
+
exports_external.object({
|
|
125187
|
+
...playableInteractionBase,
|
|
125188
|
+
type: exports_external.literal("hottext"),
|
|
125189
|
+
segments: exports_external.array(exports_external.object({
|
|
125190
|
+
identifier: trimmedNonEmptyString.optional(),
|
|
125191
|
+
content: exports_external.string(),
|
|
125192
|
+
selectable: exports_external.boolean()
|
|
125193
|
+
}).strict()),
|
|
125194
|
+
maxChoices: nonnegativeInteger
|
|
125195
|
+
}).strict(),
|
|
125196
|
+
exports_external.object({
|
|
125197
|
+
...playableInteractionBase,
|
|
125198
|
+
type: exports_external.literal("hotspot"),
|
|
125199
|
+
image: playableGraphicSchema,
|
|
125200
|
+
hotspots: exports_external.array(playableHotspotSchema),
|
|
125201
|
+
minChoices: nonnegativeInteger,
|
|
125202
|
+
maxChoices: nonnegativeInteger
|
|
125203
|
+
}).strict(),
|
|
125204
|
+
exports_external.object({
|
|
125205
|
+
...playableInteractionBase,
|
|
125206
|
+
type: exports_external.literal("gap-match"),
|
|
125207
|
+
content: exports_external.string(),
|
|
125208
|
+
contentNodes: playableContentNodesSchema,
|
|
125209
|
+
gapTexts: exports_external.array(playableGapTextSchema),
|
|
125210
|
+
gaps: exports_external.array(trimmedNonEmptyString),
|
|
125211
|
+
maxAssociations: nonnegativeInteger
|
|
125212
|
+
}).strict(),
|
|
125213
|
+
exports_external.object({
|
|
125214
|
+
...playableInteractionBase,
|
|
125215
|
+
type: exports_external.literal("graphic-gap-match"),
|
|
125216
|
+
image: playableGraphicSchema,
|
|
125217
|
+
gapImages: exports_external.array(playableGapImageSchema),
|
|
125218
|
+
hotspots: exports_external.array(playableHotspotSchema),
|
|
125219
|
+
maxAssociations: nonnegativeInteger
|
|
125220
|
+
}).strict(),
|
|
125221
|
+
exports_external.object({
|
|
125222
|
+
...playableInteractionBase,
|
|
125223
|
+
type: exports_external.literal("select-point"),
|
|
125224
|
+
image: playableGraphicSchema,
|
|
125225
|
+
minChoices: nonnegativeInteger,
|
|
125226
|
+
maxChoices: nonnegativeInteger
|
|
125227
|
+
}).strict(),
|
|
125228
|
+
exports_external.object({
|
|
125229
|
+
...playableInteractionBase,
|
|
125230
|
+
type: exports_external.literal("graphic-associate"),
|
|
125231
|
+
image: playableGraphicSchema,
|
|
125232
|
+
hotspots: exports_external.array(playableHotspotSchema),
|
|
125233
|
+
maxAssociations: nonnegativeInteger
|
|
125234
|
+
}).strict(),
|
|
125235
|
+
exports_external.object({
|
|
125236
|
+
...playableInteractionBase,
|
|
125237
|
+
type: exports_external.literal("position-object"),
|
|
125238
|
+
stage: playableGraphicSchema,
|
|
125239
|
+
object: playableGraphicSchema,
|
|
125240
|
+
maxPlacements: nonnegativeInteger
|
|
125241
|
+
}).strict()
|
|
125242
|
+
]);
|
|
125243
|
+
playableItemSchema = exports_external.object({
|
|
125244
|
+
identifier: trimmedNonEmptyString,
|
|
125245
|
+
title: exports_external.string(),
|
|
125246
|
+
prompt: exports_external.string(),
|
|
125247
|
+
promptContent: playableContentNodesSchema.optional(),
|
|
125248
|
+
maxScore: finiteNumber2,
|
|
125249
|
+
interactions: exports_external.array(playableInteractionSchema)
|
|
125250
|
+
}).strict();
|
|
125251
|
+
scoringRuleSchema = exports_external.object({
|
|
125252
|
+
responseIdentifier: trimmedNonEmptyString,
|
|
125253
|
+
cardinality: exports_external.enum(["single", "multiple", "ordered"]),
|
|
125254
|
+
baseType: exports_external.enum(["string", "identifier", "integer", "float", "directedPair"]),
|
|
125255
|
+
points: finiteNumber2,
|
|
125256
|
+
comparison: exports_external.discriminatedUnion("kind", [
|
|
125257
|
+
exports_external.object({ kind: exports_external.literal("match"), correctValues: exports_external.array(exports_external.string()) }),
|
|
125258
|
+
exports_external.object({ kind: exports_external.literal("numeric-equal"), correctValue: finiteNumber2 }),
|
|
125259
|
+
exports_external.object({
|
|
125260
|
+
kind: exports_external.literal("numeric-equal-rounded"),
|
|
125261
|
+
correctValue: finiteNumber2,
|
|
125262
|
+
roundingMode: exports_external.enum(["decimalPlaces", "significantFigures"]),
|
|
125263
|
+
figures: exports_external.number().int().nonnegative()
|
|
125264
|
+
})
|
|
125265
|
+
])
|
|
125266
|
+
});
|
|
125267
|
+
scoringItemSchema = exports_external.discriminatedUnion("strategy", [
|
|
125268
|
+
exports_external.object({
|
|
125269
|
+
strategy: exports_external.literal("qti"),
|
|
125270
|
+
itemIdentifier: trimmedNonEmptyString,
|
|
125271
|
+
maxScore: finiteNumber2,
|
|
125272
|
+
reason: exports_external.enum([
|
|
125273
|
+
"response-mapping",
|
|
125274
|
+
"area-mapping",
|
|
125275
|
+
"unsupported-base-type",
|
|
125276
|
+
"unsupported-cardinality",
|
|
125277
|
+
"invalid-correct-response",
|
|
125278
|
+
"no-scorable-response",
|
|
125279
|
+
"unsupported-processing-template",
|
|
125280
|
+
"unsupported-response-processing",
|
|
125281
|
+
"cross-response-processing",
|
|
125282
|
+
"inconsistent-max-score"
|
|
125283
|
+
])
|
|
125284
|
+
}),
|
|
125285
|
+
exports_external.object({
|
|
125286
|
+
strategy: exports_external.literal("platform"),
|
|
125287
|
+
itemIdentifier: trimmedNonEmptyString,
|
|
125288
|
+
maxScore: finiteNumber2,
|
|
125289
|
+
rules: exports_external.array(scoringRuleSchema)
|
|
125290
|
+
})
|
|
125291
|
+
]);
|
|
125292
|
+
uniqueScoringItemsSchema = exports_external.array(scoringItemSchema).refine((items) => new Set(items.map((item) => item.itemIdentifier)).size === items.length);
|
|
125293
|
+
diagnosticContextSchema = exports_external.object({
|
|
125294
|
+
routingRevision: trimmedNonEmptyString,
|
|
125295
|
+
routingManifest: exports_external.record(exports_external.unknown())
|
|
125296
|
+
});
|
|
125297
|
+
});
|
|
125298
|
+
|
|
125299
|
+
class AssessmentArtifactRuntimeLoader {
|
|
125300
|
+
artifacts;
|
|
125301
|
+
playableCache;
|
|
125302
|
+
scoringCache;
|
|
125303
|
+
playableLoads = new Map;
|
|
125304
|
+
scoringLoads = new Map;
|
|
125305
|
+
constructor(artifacts, options) {
|
|
125306
|
+
this.artifacts = artifacts;
|
|
125307
|
+
this.playableCache = this.immutableCache(options.maxSize);
|
|
125308
|
+
this.scoringCache = this.immutableCache(options.maxSize);
|
|
125309
|
+
}
|
|
125310
|
+
loadPlayable(identity) {
|
|
125311
|
+
return this.load({
|
|
125312
|
+
artifactKind: "playable",
|
|
125313
|
+
identity,
|
|
125314
|
+
key: this.cacheKey(identity),
|
|
125315
|
+
cache: this.playableCache,
|
|
125316
|
+
loads: this.playableLoads,
|
|
125317
|
+
load: () => this.artifacts.loadPlayable(identity),
|
|
125318
|
+
assert: assertPlayableAssessmentArtifact
|
|
125319
|
+
});
|
|
125320
|
+
}
|
|
125321
|
+
async loadPlayableContainingItems(identity, itemIdentifiers) {
|
|
125322
|
+
const startedAt = Date.now();
|
|
125323
|
+
const assessment = await this.loadPlayable(identity);
|
|
125324
|
+
if (assessment) {
|
|
125325
|
+
try {
|
|
125326
|
+
assertPlayableArtifactContainsItems(assessment, itemIdentifiers);
|
|
125327
|
+
return assessment;
|
|
125328
|
+
} catch (error88) {
|
|
125329
|
+
this.recordLoad("playable", identity.kind, "validation_failure", startedAt, error88);
|
|
125330
|
+
}
|
|
125331
|
+
}
|
|
125332
|
+
this.recordFallback("playable", identity.kind);
|
|
125333
|
+
return null;
|
|
125334
|
+
}
|
|
125335
|
+
loadManagedPlayable(qtiTestIdentifier) {
|
|
125336
|
+
return this.load({
|
|
125337
|
+
artifactKind: "playable",
|
|
125338
|
+
key: `managed\x00${qtiTestIdentifier}`,
|
|
125339
|
+
cache: this.playableCache,
|
|
125340
|
+
loads: this.playableLoads,
|
|
125341
|
+
load: () => this.artifacts.loadManagedPlayable(qtiTestIdentifier),
|
|
125342
|
+
assert: assertPlayableAssessmentArtifact
|
|
125343
|
+
});
|
|
125344
|
+
}
|
|
125345
|
+
loadScoring(identity) {
|
|
125346
|
+
return this.load({
|
|
125347
|
+
artifactKind: "scoring",
|
|
125348
|
+
identity,
|
|
125349
|
+
key: this.cacheKey(identity),
|
|
125350
|
+
cache: this.scoringCache,
|
|
125351
|
+
loads: this.scoringLoads,
|
|
125352
|
+
load: () => this.artifacts.loadScoring(identity),
|
|
125353
|
+
assert: assertScoringAssessmentArtifact
|
|
125354
|
+
});
|
|
125355
|
+
}
|
|
125356
|
+
async loadMatchedScoring(assessment, identity) {
|
|
125357
|
+
const scoring = await this.loadScoring(identity);
|
|
125358
|
+
if (scoring) {
|
|
125359
|
+
try {
|
|
125360
|
+
assertScoringArtifactMatchesPlayable(assessment, scoring, identity);
|
|
125361
|
+
return scoring;
|
|
125362
|
+
} catch (error88) {
|
|
125363
|
+
this.recordLoad("scoring", identity.kind, "validation_failure", Date.now(), error88);
|
|
125364
|
+
}
|
|
125365
|
+
}
|
|
125366
|
+
this.recordFallback("scoring", identity.kind);
|
|
125367
|
+
return null;
|
|
125368
|
+
}
|
|
125369
|
+
recordFallback(artifactKind, identityKind) {
|
|
125370
|
+
addEvent("assessment.artifact_fallback", {
|
|
125371
|
+
"app.assessment.artifact_kind": artifactKind,
|
|
125372
|
+
"app.assessment.artifact_identity_kind": identityKind
|
|
125373
|
+
});
|
|
125374
|
+
}
|
|
125375
|
+
immutableCache(maxSize) {
|
|
125376
|
+
return new TimebackCache({ defaultTTL: Number.POSITIVE_INFINITY, maxSize });
|
|
125377
|
+
}
|
|
125378
|
+
cacheKey(identity) {
|
|
125379
|
+
return canonicalJson2(identity);
|
|
125380
|
+
}
|
|
125381
|
+
recordLoad(artifactKind, identityKind, outcome, startedAt, error88) {
|
|
125382
|
+
addEvent("assessment.artifact_load", {
|
|
125383
|
+
"app.assessment.artifact_kind": artifactKind,
|
|
125384
|
+
"app.assessment.artifact_identity_kind": identityKind,
|
|
125385
|
+
"app.assessment.artifact_outcome": outcome,
|
|
125386
|
+
"app.assessment.artifact_duration_ms": Date.now() - startedAt,
|
|
125387
|
+
...error88 ? { "exception.type": errorType(error88) } : {}
|
|
125388
|
+
});
|
|
125389
|
+
}
|
|
125390
|
+
async load(input) {
|
|
125391
|
+
const startedAt = Date.now();
|
|
125392
|
+
const identityKind = input.identity?.kind ?? "assessment";
|
|
125393
|
+
const record3 = (outcome, error88) => this.recordLoad(input.artifactKind, identityKind, outcome, startedAt, error88);
|
|
125394
|
+
const cached3 = input.cache.get(input.key);
|
|
125395
|
+
if (cached3) {
|
|
125396
|
+
record3("cache_hit");
|
|
125397
|
+
return cached3;
|
|
125398
|
+
}
|
|
125399
|
+
const inFlight = input.loads.get(input.key);
|
|
125400
|
+
if (inFlight) {
|
|
125401
|
+
record3("coalesced");
|
|
125402
|
+
return inFlight;
|
|
125403
|
+
}
|
|
125404
|
+
const pending = (async () => {
|
|
125405
|
+
try {
|
|
125406
|
+
const stored = await input.load();
|
|
125407
|
+
if (!stored) {
|
|
125408
|
+
record3("missing");
|
|
125409
|
+
return null;
|
|
125410
|
+
}
|
|
125411
|
+
input.assert(stored.payload, input.identity ?? stored.identity);
|
|
125412
|
+
input.cache.set(input.key, stored.payload);
|
|
125413
|
+
if (!input.identity) {
|
|
125414
|
+
input.cache.set(this.cacheKey(stored.identity), stored.payload);
|
|
125415
|
+
}
|
|
125416
|
+
record3("loaded");
|
|
125417
|
+
return stored.payload;
|
|
125418
|
+
} catch (error88) {
|
|
125419
|
+
record3(error88 instanceof ValidationError ? "validation_failure" : "load_failure", error88);
|
|
125420
|
+
return null;
|
|
125421
|
+
} finally {
|
|
125422
|
+
input.loads.delete(input.key);
|
|
125423
|
+
}
|
|
125424
|
+
})();
|
|
125425
|
+
input.loads.set(input.key, pending);
|
|
125426
|
+
return pending;
|
|
125427
|
+
}
|
|
125428
|
+
}
|
|
125429
|
+
var init_assessment_artifact_loader_util = __esm(async () => {
|
|
125430
|
+
init_spans();
|
|
125431
|
+
init_errors2();
|
|
125432
|
+
init_assessment_artifact_runtime_util();
|
|
125433
|
+
await init_dist5();
|
|
125434
|
+
});
|
|
124248
125435
|
function createInMemoryDatabaseLock() {
|
|
124249
125436
|
const tails = new Map;
|
|
124250
125437
|
return async (db2, key, operation) => {
|
|
@@ -124364,6 +125551,808 @@ var init_assessment_runtime_lock_util = __esm(() => {
|
|
|
124364
125551
|
}
|
|
124365
125552
|
};
|
|
124366
125553
|
});
|
|
125554
|
+
function assessmentPreparationContentionError(expectedResponseVersion, responseVersion, kind) {
|
|
125555
|
+
if (expectedResponseVersion === responseVersion) {
|
|
125556
|
+
return new ServiceUnavailableError(`The ${kind} response state changed repeatedly while this item was being prepared. Try again shortly.`, {
|
|
125557
|
+
retryable: true,
|
|
125558
|
+
reason: "PREPARATION_CONTENTION",
|
|
125559
|
+
expectedResponseVersion,
|
|
125560
|
+
responseVersion
|
|
125561
|
+
});
|
|
125562
|
+
}
|
|
125563
|
+
return AssessmentRuntimeError.from(assessmentResponseVersionConflict(expectedResponseVersion, responseVersion));
|
|
125564
|
+
}
|
|
125565
|
+
function assessmentTrackableCourse(integration) {
|
|
125566
|
+
if (!isTimebackGrade2(integration.grade) || !isTimebackSubject(integration.subject)) {
|
|
125567
|
+
return null;
|
|
125568
|
+
}
|
|
125569
|
+
return { grade: integration.grade, subject: integration.subject };
|
|
125570
|
+
}
|
|
125571
|
+
function buildAssessmentActivityData(input) {
|
|
125572
|
+
if (!input.course) {
|
|
125573
|
+
return;
|
|
125574
|
+
}
|
|
125575
|
+
return {
|
|
125576
|
+
activityId: input.activityId,
|
|
125577
|
+
activityName: input.activityName,
|
|
125578
|
+
...input.course,
|
|
125579
|
+
courseId: input.courseId
|
|
125580
|
+
};
|
|
125581
|
+
}
|
|
125582
|
+
function buildDiagnosticAssessmentSnapshot(input) {
|
|
125583
|
+
return {
|
|
125584
|
+
attemptId: input.attemptId,
|
|
125585
|
+
responseVersion: input.metadata.responseVersion,
|
|
125586
|
+
status: "in_progress",
|
|
125587
|
+
flow: "platform-routed-item-submit",
|
|
125588
|
+
...input.activityData ? { activityData: input.activityData } : {},
|
|
125589
|
+
assessment: assessmentPresentationForAttempt(input.assessment, input.attemptId),
|
|
125590
|
+
selection: {
|
|
125591
|
+
kind: "platform-routed-diagnostic",
|
|
125592
|
+
purpose: "diagnostic",
|
|
125593
|
+
definitionId: input.metadata.diagnostic.definitionId,
|
|
125594
|
+
diagnosticKey: input.metadata.diagnostic.diagnosticKey,
|
|
125595
|
+
routingRevision: input.metadata.diagnostic.routingRevision
|
|
125596
|
+
},
|
|
125597
|
+
routing: projectDiagnosticRoutingSnapshot(input.metadata.diagnostic.routingRevision, input.state),
|
|
125598
|
+
completion: null
|
|
125599
|
+
};
|
|
125600
|
+
}
|
|
125601
|
+
function buildConventionalAssessmentSnapshot(input) {
|
|
125602
|
+
const metadata2 = input.metadata;
|
|
125603
|
+
let selection;
|
|
125604
|
+
if (metadata2.purpose === "review") {
|
|
125605
|
+
selection = {
|
|
125606
|
+
kind: "standards-review",
|
|
125607
|
+
purpose: metadata2.purpose,
|
|
125608
|
+
standards: [...metadata2.review.standards],
|
|
125609
|
+
candidateItemsPerStandard: metadata2.review.candidateItemsPerStandard,
|
|
125610
|
+
selections: [...metadata2.review.selections],
|
|
125611
|
+
fulfillment: metadata2.review.fulfillment
|
|
125612
|
+
};
|
|
125613
|
+
} else if (metadata2.purpose === "mastery") {
|
|
125614
|
+
selection = {
|
|
125615
|
+
kind: "standard-quiz",
|
|
125616
|
+
purpose: metadata2.purpose,
|
|
125617
|
+
standard: metadata2.mastery.standard
|
|
125618
|
+
};
|
|
125619
|
+
} else {
|
|
125620
|
+
selection = { kind: "fixed-test", purpose: metadata2.purpose };
|
|
125621
|
+
}
|
|
125622
|
+
return {
|
|
125623
|
+
attemptId: input.attemptId,
|
|
125624
|
+
responseVersion: metadata2.responseVersion,
|
|
125625
|
+
status: "in_progress",
|
|
125626
|
+
flow: assessmentFlowForPurpose(metadata2.purpose),
|
|
125627
|
+
...input.activityData ? { activityData: input.activityData } : {},
|
|
125628
|
+
assessment: assessmentPresentationForAttempt(input.assessment, input.attemptId),
|
|
125629
|
+
responses: metadata2.responses,
|
|
125630
|
+
itemSubmissions: metadata2.itemSubmissions,
|
|
125631
|
+
score: null,
|
|
125632
|
+
selection
|
|
125633
|
+
};
|
|
125634
|
+
}
|
|
125635
|
+
function buildAssessmentAwardRecord(input, mastery) {
|
|
125636
|
+
return {
|
|
125637
|
+
xpAwarded: input.xpAwarded,
|
|
125638
|
+
...input.masteredUnits !== undefined ? { masteredUnits: input.masteredUnits } : {},
|
|
125639
|
+
...input.masteredUnitsAbsolute !== undefined ? { masteredUnitsAbsolute: input.masteredUnitsAbsolute } : {},
|
|
125640
|
+
masteredUnitsApplied: mastery.masteredUnitsApplied,
|
|
125641
|
+
...mastery.pctCompleteApp !== undefined ? { pctCompleteApp: mastery.pctCompleteApp } : {},
|
|
125642
|
+
...mastery.warnings ? { warnings: mastery.warnings } : {}
|
|
125643
|
+
};
|
|
125644
|
+
}
|
|
125645
|
+
function stageAssessmentAttemptSupersession(attempt) {
|
|
125646
|
+
Object.assign(attempt.result, ASSESSMENT_ATTEMPT_SUPERSEDED);
|
|
125647
|
+
Object.assign(attempt.selection, ASSESSMENT_ATTEMPT_SUPERSEDED);
|
|
125648
|
+
}
|
|
125649
|
+
function shouldConfirmAssessmentVersionUnavailable(details) {
|
|
125650
|
+
return typeof details === "object" && details !== null && "expectedRevision" in details && typeof details.expectedRevision === "string" && !("currentRevision" in details);
|
|
125651
|
+
}
|
|
125652
|
+
function buildAssessmentAttemptSupersessionUpdate(result) {
|
|
125653
|
+
if (result.status !== "active" || !isAssessmentAttemptOpen({
|
|
125654
|
+
inProgress: result.inProgress ?? "",
|
|
125655
|
+
scoreStatus: result.scoreStatus
|
|
125656
|
+
})) {
|
|
125657
|
+
return null;
|
|
125658
|
+
}
|
|
125659
|
+
return {
|
|
125660
|
+
status: "active",
|
|
125661
|
+
assessmentLineItem: result.assessmentLineItem,
|
|
125662
|
+
student: result.student,
|
|
125663
|
+
score: result.score ?? 0,
|
|
125664
|
+
scoreDate: result.scoreDate,
|
|
125665
|
+
...ASSESSMENT_ATTEMPT_SUPERSEDED,
|
|
125666
|
+
metadata: result.metadata ?? {}
|
|
125667
|
+
};
|
|
125668
|
+
}
|
|
125669
|
+
async function resolveAvailableAssessmentAttempt(initialDecision, select3, resume) {
|
|
125670
|
+
let decision = initialDecision;
|
|
125671
|
+
while (decision?.kind === "resume") {
|
|
125672
|
+
const snapshot = await resume(decision);
|
|
125673
|
+
if (snapshot !== null) {
|
|
125674
|
+
return { kind: "resumed", snapshot };
|
|
125675
|
+
}
|
|
125676
|
+
decision = select3();
|
|
125677
|
+
}
|
|
125678
|
+
return { kind: "continue", decision };
|
|
125679
|
+
}
|
|
125680
|
+
function buildReviewItemResultMetadata(input) {
|
|
125681
|
+
const administration = itemAdministration(input.metadata.itemSubmissions, input.selection.itemIdentifier);
|
|
125682
|
+
if (!administration) {
|
|
125683
|
+
throw new Error(`Cannot create a review result for unadministered candidate ${input.selection.itemIdentifier}`);
|
|
125684
|
+
}
|
|
125685
|
+
return {
|
|
125686
|
+
version: PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_VERSION,
|
|
125687
|
+
parentAttemptId: input.parentAttemptId,
|
|
125688
|
+
activityId: input.metadata.activityId,
|
|
125689
|
+
integrationId: input.metadata.integrationId,
|
|
125690
|
+
enrollmentId: input.metadata.enrollmentId,
|
|
125691
|
+
bankRevision: input.metadata.review.bankRevision,
|
|
125692
|
+
itemIdentifier: input.selection.itemIdentifier,
|
|
125693
|
+
standard: input.selection.standard,
|
|
125694
|
+
deliveredAt: administration.submittedAt,
|
|
125695
|
+
responseVersion: input.responseVersion ?? input.metadata.responseVersion,
|
|
125696
|
+
responses: input.metadata.responses[input.selection.itemIdentifier] ?? {},
|
|
125697
|
+
...input.submissionId ? { submissionId: input.submissionId } : {},
|
|
125698
|
+
...input.isCorrect === undefined ? {} : { isCorrect: input.isCorrect }
|
|
125699
|
+
};
|
|
125700
|
+
}
|
|
125701
|
+
function buildOpenReviewChildResult(input) {
|
|
125702
|
+
return {
|
|
125703
|
+
status: "active",
|
|
125704
|
+
assessmentLineItem: { sourcedId: input.childLineItemId },
|
|
125705
|
+
student: input.student,
|
|
125706
|
+
score: 0,
|
|
125707
|
+
scoreDate: input.metadata.deliveredAt,
|
|
125708
|
+
...ASSESSMENT_ATTEMPT_OPEN,
|
|
125709
|
+
metadata: {
|
|
125710
|
+
[PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY]: input.metadata
|
|
125711
|
+
}
|
|
125712
|
+
};
|
|
125713
|
+
}
|
|
125714
|
+
function diagnosticQuestionLineItemId(parentLineItemId, itemIdentifier) {
|
|
125715
|
+
return deterministicUUID(["playcademy:diagnostic-question-line-item:v1", parentLineItemId, itemIdentifier].join("\x00"));
|
|
125716
|
+
}
|
|
125717
|
+
function diagnosticQuestionResultId(parentAttemptId, childLineItemId) {
|
|
125718
|
+
return deterministicUUID(["playcademy:diagnostic-question-result:v1", parentAttemptId, childLineItemId].join("\x00"));
|
|
125719
|
+
}
|
|
125720
|
+
function diagnosticAdministration(input) {
|
|
125721
|
+
const ledgerIndex = input.metadata.diagnostic.ledger.findIndex((entry2) => entry2.routingNodeKey === input.ledgerEntry.routingNodeKey);
|
|
125722
|
+
const persistedEntry = input.metadata.diagnostic.ledger[ledgerIndex];
|
|
125723
|
+
const administration = input.metadata.itemSubmissions[ledgerIndex];
|
|
125724
|
+
if (ledgerIndex === -1 || !persistedEntry || !administration || persistedEntry.stageKey !== input.ledgerEntry.stageKey || persistedEntry.trackKey !== input.ledgerEntry.trackKey || persistedEntry.itemIdentifier !== input.ledgerEntry.itemIdentifier || persistedEntry.isCorrect !== input.ledgerEntry.isCorrect || administration.itemIdentifier !== input.ledgerEntry.itemIdentifier || administration.isCorrect !== input.ledgerEntry.isCorrect) {
|
|
125725
|
+
throw new Error(`Cannot create a diagnostic result for uncommitted routing node ${input.ledgerEntry.routingNodeKey}`);
|
|
125726
|
+
}
|
|
125727
|
+
return administration;
|
|
125728
|
+
}
|
|
125729
|
+
function buildDiagnosticItemResultMetadata(input) {
|
|
125730
|
+
const administration = diagnosticAdministration(input);
|
|
125731
|
+
return {
|
|
125732
|
+
version: PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_VERSION,
|
|
125733
|
+
parentAttemptId: input.parentAttemptId,
|
|
125734
|
+
activityId: input.metadata.activityId,
|
|
125735
|
+
integrationId: input.metadata.integrationId,
|
|
125736
|
+
enrollmentId: input.metadata.enrollmentId,
|
|
125737
|
+
definitionId: input.metadata.diagnostic.definitionId,
|
|
125738
|
+
diagnosticKey: input.metadata.diagnostic.diagnosticKey,
|
|
125739
|
+
routingRevision: input.metadata.diagnostic.routingRevision,
|
|
125740
|
+
contentRevision: input.metadata.selectedTest.contentRevision,
|
|
125741
|
+
stageKey: input.ledgerEntry.stageKey,
|
|
125742
|
+
trackKey: input.ledgerEntry.trackKey,
|
|
125743
|
+
routingNodeKey: input.ledgerEntry.routingNodeKey,
|
|
125744
|
+
itemIdentifier: input.ledgerEntry.itemIdentifier,
|
|
125745
|
+
deliveredAt: administration.submittedAt,
|
|
125746
|
+
responseVersion: administration.responseVersion,
|
|
125747
|
+
responses: input.metadata.responses[input.ledgerEntry.itemIdentifier] ?? {},
|
|
125748
|
+
submissionId: administration.submissionId,
|
|
125749
|
+
isCorrect: input.ledgerEntry.isCorrect
|
|
125750
|
+
};
|
|
125751
|
+
}
|
|
125752
|
+
function resolveDiagnosticChildResult(input) {
|
|
125753
|
+
if (!input.childResult) {
|
|
125754
|
+
return { action: "create" };
|
|
125755
|
+
}
|
|
125756
|
+
const childMetadata = playcademyDiagnosticAssessmentItemResultMetadata(input.childResult.metadata);
|
|
125757
|
+
const expectedMetadata = buildDiagnosticItemResultMetadata({
|
|
125758
|
+
metadata: input.parentMetadata,
|
|
125759
|
+
ledgerEntry: input.ledgerEntry,
|
|
125760
|
+
parentAttemptId: input.parentResult.sourcedId
|
|
125761
|
+
});
|
|
125762
|
+
if (input.childResult.assessmentLineItem.sourcedId !== input.childLineItemId || input.childResult.student.sourcedId !== input.parentResult.student.sourcedId || !childMetadata || canonicalJson2(childMetadata) !== canonicalJson2(expectedMetadata)) {
|
|
125763
|
+
return { action: "restore", reason: "incompatible" };
|
|
125764
|
+
}
|
|
125765
|
+
return input.childResult.status === "active" ? { action: "reuse", metadata: expectedMetadata } : { action: "restore", reason: "inactive" };
|
|
125766
|
+
}
|
|
125767
|
+
function buildFinalizedDiagnosticChildResult(input) {
|
|
125768
|
+
const administration = diagnosticAdministration(input);
|
|
125769
|
+
const metadata2 = buildDiagnosticItemResultMetadata(input);
|
|
125770
|
+
return {
|
|
125771
|
+
resultUpdate: {
|
|
125772
|
+
status: "active",
|
|
125773
|
+
assessmentLineItem: { sourcedId: input.childLineItemId },
|
|
125774
|
+
student: input.student,
|
|
125775
|
+
score: administration.score.earned,
|
|
125776
|
+
scoreDate: administration.submittedAt,
|
|
125777
|
+
...ASSESSMENT_ATTEMPT_COMPLETED,
|
|
125778
|
+
metadata: {
|
|
125779
|
+
[PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY]: metadata2
|
|
125780
|
+
}
|
|
125781
|
+
}
|
|
125782
|
+
};
|
|
125783
|
+
}
|
|
125784
|
+
function buildFinalizedReviewChildResult(input) {
|
|
125785
|
+
const metadata2 = buildReviewItemResultMetadata({
|
|
125786
|
+
metadata: input.metadata,
|
|
125787
|
+
selection: input.selection,
|
|
125788
|
+
parentAttemptId: input.parentAttemptId,
|
|
125789
|
+
submissionId: input.submissionId,
|
|
125790
|
+
isCorrect: input.itemResult.isCorrect
|
|
125791
|
+
});
|
|
125792
|
+
return {
|
|
125793
|
+
resultUpdate: {
|
|
125794
|
+
status: "active",
|
|
125795
|
+
assessmentLineItem: { sourcedId: input.childLineItemId },
|
|
125796
|
+
student: input.student,
|
|
125797
|
+
score: input.itemResult.score.earned,
|
|
125798
|
+
scoreDate: input.timestamp,
|
|
125799
|
+
...ASSESSMENT_ATTEMPT_COMPLETED,
|
|
125800
|
+
metadata: {
|
|
125801
|
+
[PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY]: metadata2
|
|
125802
|
+
}
|
|
125803
|
+
},
|
|
125804
|
+
outcome: {
|
|
125805
|
+
itemIdentifier: input.selection.itemIdentifier,
|
|
125806
|
+
standard: input.selection.standard,
|
|
125807
|
+
answered: Object.keys(input.metadata.responses[input.selection.itemIdentifier] ?? {}).length > 0,
|
|
125808
|
+
score: input.itemResult.score,
|
|
125809
|
+
isCorrect: input.itemResult.isCorrect
|
|
125810
|
+
}
|
|
125811
|
+
};
|
|
125812
|
+
}
|
|
125813
|
+
function resolveReviewChildResult(input) {
|
|
125814
|
+
if (!input.childResult) {
|
|
125815
|
+
return { action: "create" };
|
|
125816
|
+
}
|
|
125817
|
+
const childMetadata = playcademyAssessmentItemResultMetadata(input.childResult.metadata);
|
|
125818
|
+
const expectedResponses = input.parentMetadata.responses[input.selection.itemIdentifier] ?? {};
|
|
125819
|
+
const administration = itemAdministration(input.parentMetadata.itemSubmissions, input.selection.itemIdentifier);
|
|
125820
|
+
if (input.childResult.assessmentLineItem.sourcedId !== input.childLineItemId || input.childResult.student.sourcedId !== input.parentResult.student.sourcedId || !childMetadata || childMetadata.parentAttemptId !== input.parentResult.sourcedId || childMetadata.activityId !== input.parentMetadata.activityId || childMetadata.integrationId !== input.parentMetadata.integrationId || childMetadata.enrollmentId !== input.parentMetadata.enrollmentId || childMetadata.bankRevision !== input.parentMetadata.review.bankRevision || childMetadata.itemIdentifier !== input.selection.itemIdentifier || childMetadata.standard.framework !== input.selection.standard.framework || childMetadata.standard.identifier !== input.selection.standard.identifier || childMetadata.deliveredAt !== administration?.submittedAt || childMetadata.responseVersion > input.parentMetadata.responseVersion || canonicalJson2(childMetadata.responses) !== canonicalJson2(expectedResponses)) {
|
|
125821
|
+
return { action: "restore", reason: "incompatible" };
|
|
125822
|
+
}
|
|
125823
|
+
return input.childResult.status === "active" ? { action: "reuse", metadata: childMetadata } : { action: "restore", reason: "inactive" };
|
|
125824
|
+
}
|
|
125825
|
+
function buildAssessmentSubmitResult(input) {
|
|
125826
|
+
if (!input.metadata.score || !input.metadata.submissionId) {
|
|
125827
|
+
return null;
|
|
125828
|
+
}
|
|
125829
|
+
if (input.metadata.purpose === "diagnostic" && input.metadata.diagnostic) {
|
|
125830
|
+
const tracks = input.metadata.completion?.diagnosticTracks;
|
|
125831
|
+
return tracks ? {
|
|
125832
|
+
purpose: input.metadata.purpose,
|
|
125833
|
+
attemptId: input.attemptId,
|
|
125834
|
+
submissionId: input.metadata.submissionId,
|
|
125835
|
+
diagnosticKey: input.metadata.diagnostic.diagnosticKey,
|
|
125836
|
+
responseVersion: input.metadata.responseVersion,
|
|
125837
|
+
status: "awaiting_award",
|
|
125838
|
+
tracks
|
|
125839
|
+
} : null;
|
|
125840
|
+
}
|
|
125841
|
+
const base = {
|
|
125842
|
+
attemptId: input.attemptId,
|
|
125843
|
+
submissionId: input.metadata.submissionId,
|
|
125844
|
+
responseVersion: input.metadata.responseVersion,
|
|
125845
|
+
status: "awaiting_award",
|
|
125846
|
+
responses: input.metadata.responses,
|
|
125847
|
+
score: input.metadata.score
|
|
125848
|
+
};
|
|
125849
|
+
if (input.metadata.purpose === "review") {
|
|
125850
|
+
const itemOutcomes = input.metadata.completion?.itemOutcomes;
|
|
125851
|
+
return itemOutcomes ? { ...base, purpose: input.metadata.purpose, itemOutcomes } : null;
|
|
125852
|
+
}
|
|
125853
|
+
return { ...base, purpose: input.metadata.purpose };
|
|
125854
|
+
}
|
|
125855
|
+
function assessmentAwardFromResult(result, metadata2) {
|
|
125856
|
+
if (metadata2.award) {
|
|
125857
|
+
return metadata2.award;
|
|
125858
|
+
}
|
|
125859
|
+
const xp = result.metadata?.xp;
|
|
125860
|
+
return typeof xp === "number" && Number.isFinite(xp) && xp >= 0 ? { xpAwarded: xp, masteredUnitsApplied: 0 } : null;
|
|
125861
|
+
}
|
|
125862
|
+
function buildCompletedAssessmentSubmitResult(input) {
|
|
125863
|
+
const attemptId = input.result.sourcedId;
|
|
125864
|
+
const award = assessmentAwardFromResult(input.result, input.metadata);
|
|
125865
|
+
if (award === null) {
|
|
125866
|
+
throw new GoneError("This completed assessment is missing its recorded XP.", {
|
|
125867
|
+
attemptId
|
|
125868
|
+
});
|
|
125869
|
+
}
|
|
125870
|
+
const submitted = buildAssessmentSubmitResult({ attemptId, metadata: input.metadata });
|
|
125871
|
+
if (!submitted) {
|
|
125872
|
+
throw new GoneError("This completed assessment has incomplete persisted results.", {
|
|
125873
|
+
attemptId
|
|
125874
|
+
});
|
|
125875
|
+
}
|
|
125876
|
+
return {
|
|
125877
|
+
...submitted,
|
|
125878
|
+
status: "completed",
|
|
125879
|
+
xpAwarded: award.xpAwarded,
|
|
125880
|
+
masteredUnitsApplied: award.masteredUnitsApplied,
|
|
125881
|
+
...award.pctCompleteApp !== undefined ? { pctCompleteApp: award.pctCompleteApp } : {},
|
|
125882
|
+
...award.warnings ? { warnings: award.warnings } : {}
|
|
125883
|
+
};
|
|
125884
|
+
}
|
|
125885
|
+
function assessmentAttemptId(input) {
|
|
125886
|
+
return deterministicUUID([
|
|
125887
|
+
"playcademy:assessment-attempt",
|
|
125888
|
+
input.gameId,
|
|
125889
|
+
input.studentId,
|
|
125890
|
+
input.courseId,
|
|
125891
|
+
input.activityId,
|
|
125892
|
+
input.purpose,
|
|
125893
|
+
String(input.attemptNumber)
|
|
125894
|
+
].join("\x00"));
|
|
125895
|
+
}
|
|
125896
|
+
async function findLatestAssessmentResult(results, match) {
|
|
125897
|
+
let latest = null;
|
|
125898
|
+
let latestTimestamp = 0;
|
|
125899
|
+
for await (const result of results) {
|
|
125900
|
+
const resultTimestamp = assessmentAttemptTimestamp(result.scoreDate);
|
|
125901
|
+
if (latest && resultTimestamp < latestTimestamp) {
|
|
125902
|
+
break;
|
|
125903
|
+
}
|
|
125904
|
+
const matched = match(result);
|
|
125905
|
+
if (matched !== null && (!latest || compareNewestCompletedAssessmentAttempt({ attemptId: result.sourcedId, scoreDate: result.scoreDate }, {
|
|
125906
|
+
attemptId: latest.result.sourcedId,
|
|
125907
|
+
scoreDate: latest.result.scoreDate
|
|
125908
|
+
}) < 0)) {
|
|
125909
|
+
latest = { result, match: matched };
|
|
125910
|
+
latestTimestamp = resultTimestamp;
|
|
125911
|
+
}
|
|
125912
|
+
}
|
|
125913
|
+
return latest;
|
|
125914
|
+
}
|
|
125915
|
+
function playableChoice(choice) {
|
|
125916
|
+
return {
|
|
125917
|
+
identifier: choice.identifier,
|
|
125918
|
+
content: choice.content,
|
|
125919
|
+
...choice.contentNodes ? { contentNodes: choice.contentNodes } : {}
|
|
125920
|
+
};
|
|
125921
|
+
}
|
|
125922
|
+
function playableAssociableChoice(choice) {
|
|
125923
|
+
const matchMax = qtiAttributeCapacity(choice.attributes, "match-max", 0);
|
|
125924
|
+
if (matchMax === null) {
|
|
125925
|
+
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", "The selected assessment contains an invalid Match capacity.");
|
|
125926
|
+
}
|
|
125927
|
+
return {
|
|
125928
|
+
...playableChoice(choice),
|
|
125929
|
+
matchMax
|
|
125930
|
+
};
|
|
125931
|
+
}
|
|
125932
|
+
function requireChoices2(choices) {
|
|
125933
|
+
if (choices.length === 0 || choices.some((candidate) => !candidate.identifier)) {
|
|
125934
|
+
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", "The selected assessment contains an interaction Playcademy cannot render.");
|
|
125935
|
+
}
|
|
125936
|
+
return choices;
|
|
125937
|
+
}
|
|
125938
|
+
function isPlayableAssessmentShape(value) {
|
|
125939
|
+
return PLAYABLE_SHAPE_SET.has(value);
|
|
125940
|
+
}
|
|
125941
|
+
function playableGraphic(graphic) {
|
|
125942
|
+
return {
|
|
125943
|
+
src: graphic.src,
|
|
125944
|
+
...graphic.width === undefined ? {} : { width: graphic.width },
|
|
125945
|
+
...graphic.height === undefined ? {} : { height: graphic.height },
|
|
125946
|
+
...graphic.description ? { description: graphic.description } : {}
|
|
125947
|
+
};
|
|
125948
|
+
}
|
|
125949
|
+
function requireGraphic(questionIdentifier, graphic) {
|
|
125950
|
+
if (!graphic) {
|
|
125951
|
+
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${questionIdentifier} is missing a required graphic.`);
|
|
125952
|
+
}
|
|
125953
|
+
return playableGraphic(graphic);
|
|
125954
|
+
}
|
|
125955
|
+
function requireHotspots(questionIdentifier, hotspots) {
|
|
125956
|
+
if (hotspots.length === 0) {
|
|
125957
|
+
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${questionIdentifier} declares no graphic regions.`);
|
|
125958
|
+
}
|
|
125959
|
+
return hotspots.map((hotspot) => ({
|
|
125960
|
+
identifier: hotspot.identifier,
|
|
125961
|
+
shape: isPlayableAssessmentShape(hotspot.shape) ? hotspot.shape : "default",
|
|
125962
|
+
coords: [...hotspot.coords],
|
|
125963
|
+
...hotspot.matchMax === undefined ? {} : { matchMax: hotspot.matchMax },
|
|
125964
|
+
...hotspot.description ? { description: hotspot.description } : {}
|
|
125965
|
+
}));
|
|
125966
|
+
}
|
|
125967
|
+
function parseInteraction2(questionIdentifier, interaction) {
|
|
125968
|
+
const { type } = interaction;
|
|
125969
|
+
const responseIdentifier = interaction.attributes["response-identifier"];
|
|
125970
|
+
if (!responseIdentifier) {
|
|
125971
|
+
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${questionIdentifier} has no response identifier.`);
|
|
125972
|
+
}
|
|
125973
|
+
const prompt = interaction.embeddedPrompt ? { prompt: interaction.embeddedPrompt } : {};
|
|
125974
|
+
if (type === "choice") {
|
|
125975
|
+
const choices = requireChoices2(interaction.choices.map(playableChoice));
|
|
125976
|
+
return {
|
|
125977
|
+
type,
|
|
125978
|
+
responseIdentifier,
|
|
125979
|
+
...prompt,
|
|
125980
|
+
choices,
|
|
125981
|
+
minChoices: qtiAttributeNumber(interaction.attributes, "min-choices", 1),
|
|
125982
|
+
maxChoices: qtiAttributeMaximum(interaction.attributes, "max-choices", 1, choices.length)
|
|
125983
|
+
};
|
|
125984
|
+
}
|
|
125985
|
+
if (type === "inline-choice") {
|
|
125986
|
+
return {
|
|
125987
|
+
type,
|
|
125988
|
+
responseIdentifier,
|
|
125989
|
+
...prompt,
|
|
125990
|
+
choices: requireChoices2(interaction.choices.map(playableChoice))
|
|
125991
|
+
};
|
|
125992
|
+
}
|
|
125993
|
+
if (type === "text-entry") {
|
|
125994
|
+
const expectedLength = qtiAttributeNumber(interaction.attributes, "expected-length", 0);
|
|
125995
|
+
return {
|
|
125996
|
+
type,
|
|
125997
|
+
responseIdentifier,
|
|
125998
|
+
...prompt,
|
|
125999
|
+
...Number.isInteger(expectedLength) && expectedLength > 0 ? { expectedLength } : {},
|
|
126000
|
+
...interaction.attributes.placeholder ? { placeholder: interaction.attributes.placeholder } : {}
|
|
126001
|
+
};
|
|
126002
|
+
}
|
|
126003
|
+
if (type === "order") {
|
|
126004
|
+
return {
|
|
126005
|
+
type,
|
|
126006
|
+
responseIdentifier,
|
|
126007
|
+
...prompt,
|
|
126008
|
+
choices: requireChoices2(interaction.choices.map(playableChoice)),
|
|
126009
|
+
shuffle: qtiAttributeBoolean(interaction.attributes, "shuffle")
|
|
126010
|
+
};
|
|
126011
|
+
}
|
|
126012
|
+
if (type === "match") {
|
|
126013
|
+
const sets = interaction.choiceSets;
|
|
126014
|
+
if (sets.length !== 2) {
|
|
126015
|
+
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", "Match interactions require exactly two choice sets.");
|
|
126016
|
+
}
|
|
126017
|
+
const sourceChoices = requireChoices2(sets[0].map(playableAssociableChoice));
|
|
126018
|
+
const targetChoices = requireChoices2(sets[1].map(playableAssociableChoice));
|
|
126019
|
+
return {
|
|
126020
|
+
type,
|
|
126021
|
+
responseIdentifier,
|
|
126022
|
+
...prompt,
|
|
126023
|
+
sourceChoices,
|
|
126024
|
+
targetChoices,
|
|
126025
|
+
maxAssociations: qtiAttributeMaximum(interaction.attributes, "max-associations", 1, sourceChoices.length * targetChoices.length),
|
|
126026
|
+
shuffle: qtiAttributeBoolean(interaction.attributes, "shuffle")
|
|
126027
|
+
};
|
|
126028
|
+
}
|
|
126029
|
+
if (type === "hotspot") {
|
|
126030
|
+
const image = requireGraphic(questionIdentifier, interaction.graphic);
|
|
126031
|
+
const hotspots = requireHotspots(questionIdentifier, interaction.hotspots);
|
|
126032
|
+
return {
|
|
126033
|
+
type,
|
|
126034
|
+
responseIdentifier,
|
|
126035
|
+
...prompt,
|
|
126036
|
+
image,
|
|
126037
|
+
hotspots,
|
|
126038
|
+
minChoices: qtiAttributeNumber(interaction.attributes, "min-choices", 1),
|
|
126039
|
+
maxChoices: qtiAttributeMaximum(interaction.attributes, "max-choices", 1, hotspots.length)
|
|
126040
|
+
};
|
|
126041
|
+
}
|
|
126042
|
+
if (type === "gap-match") {
|
|
126043
|
+
if (interaction.gapTexts.length === 0 || interaction.gaps.length === 0 || interaction.textWithGaps === undefined || !interaction.gapContent || !qtiGapContentMatchesGaps(interaction.gapContent, interaction.gaps)) {
|
|
126044
|
+
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${questionIdentifier} has an incomplete gap-match interaction.`);
|
|
126045
|
+
}
|
|
126046
|
+
return {
|
|
126047
|
+
type,
|
|
126048
|
+
responseIdentifier,
|
|
126049
|
+
...prompt,
|
|
126050
|
+
content: interaction.textWithGaps,
|
|
126051
|
+
contentNodes: interaction.gapContent,
|
|
126052
|
+
gapTexts: interaction.gapTexts.map((gapText) => ({
|
|
126053
|
+
identifier: gapText.identifier,
|
|
126054
|
+
content: gapText.content,
|
|
126055
|
+
matchMax: gapText.matchMax
|
|
126056
|
+
})),
|
|
126057
|
+
gaps: [...interaction.gaps],
|
|
126058
|
+
maxAssociations: qtiAttributeNumber(interaction.attributes, "max-associations", 0)
|
|
126059
|
+
};
|
|
126060
|
+
}
|
|
126061
|
+
if (type === "graphic-gap-match") {
|
|
126062
|
+
const image = requireGraphic(questionIdentifier, interaction.graphic);
|
|
126063
|
+
const hotspots = requireHotspots(questionIdentifier, interaction.hotspots);
|
|
126064
|
+
if (interaction.gapImages.length === 0) {
|
|
126065
|
+
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${questionIdentifier} has no draggable gap images.`);
|
|
126066
|
+
}
|
|
126067
|
+
return {
|
|
126068
|
+
type,
|
|
126069
|
+
responseIdentifier,
|
|
126070
|
+
...prompt,
|
|
126071
|
+
image,
|
|
126072
|
+
gapImages: interaction.gapImages.map((gapImage) => ({
|
|
126073
|
+
identifier: gapImage.identifier,
|
|
126074
|
+
...gapImage.graphic ? { image: playableGraphic(gapImage.graphic) } : {},
|
|
126075
|
+
matchMax: gapImage.matchMax,
|
|
126076
|
+
...gapImage.description ? { description: gapImage.description } : {}
|
|
126077
|
+
})),
|
|
126078
|
+
hotspots,
|
|
126079
|
+
maxAssociations: qtiAttributeNumber(interaction.attributes, "max-associations", 0)
|
|
126080
|
+
};
|
|
126081
|
+
}
|
|
126082
|
+
if (type === "select-point") {
|
|
126083
|
+
return {
|
|
126084
|
+
type,
|
|
126085
|
+
responseIdentifier,
|
|
126086
|
+
...prompt,
|
|
126087
|
+
image: requireGraphic(questionIdentifier, interaction.graphic),
|
|
126088
|
+
minChoices: qtiAttributeNumber(interaction.attributes, "min-choices", 0),
|
|
126089
|
+
maxChoices: qtiAttributeNumber(interaction.attributes, "max-choices", 0)
|
|
126090
|
+
};
|
|
126091
|
+
}
|
|
126092
|
+
if (type === "graphic-associate") {
|
|
126093
|
+
return {
|
|
126094
|
+
type,
|
|
126095
|
+
responseIdentifier,
|
|
126096
|
+
...prompt,
|
|
126097
|
+
image: requireGraphic(questionIdentifier, interaction.graphic),
|
|
126098
|
+
hotspots: requireHotspots(questionIdentifier, interaction.hotspots),
|
|
126099
|
+
maxAssociations: qtiAttributeNumber(interaction.attributes, "max-associations", 1)
|
|
126100
|
+
};
|
|
126101
|
+
}
|
|
126102
|
+
if (type === "position-object") {
|
|
126103
|
+
return {
|
|
126104
|
+
type,
|
|
126105
|
+
responseIdentifier,
|
|
126106
|
+
...prompt,
|
|
126107
|
+
stage: requireGraphic(questionIdentifier, interaction.graphic),
|
|
126108
|
+
object: requireGraphic(questionIdentifier, interaction.objectGraphic),
|
|
126109
|
+
maxPlacements: qtiAttributeNumber(interaction.attributes, "max-choices", 1)
|
|
126110
|
+
};
|
|
126111
|
+
}
|
|
126112
|
+
if (type === "hottext") {
|
|
126113
|
+
const segments = interaction.textSegments;
|
|
126114
|
+
if (!segments.some((segment) => segment.selectable) || segments.some((segment) => segment.selectable && (!segment.identifier || !segment.content))) {
|
|
126115
|
+
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", "Hottext interactions require identified selectable content.");
|
|
126116
|
+
}
|
|
126117
|
+
return {
|
|
126118
|
+
type,
|
|
126119
|
+
responseIdentifier,
|
|
126120
|
+
...prompt,
|
|
126121
|
+
segments,
|
|
126122
|
+
maxChoices: qtiAttributeMaximum(interaction.attributes, "max-choices", 1, segments.filter((segment) => segment.selectable).length)
|
|
126123
|
+
};
|
|
126124
|
+
}
|
|
126125
|
+
const unsupported = type;
|
|
126126
|
+
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${questionIdentifier} uses unsupported interaction ${unsupported}.`);
|
|
126127
|
+
}
|
|
126128
|
+
function normalizedQtiItem(question) {
|
|
126129
|
+
if (!question.rawXml) {
|
|
126130
|
+
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${question.identifier} has no authoritative QTI XML.`);
|
|
126131
|
+
}
|
|
126132
|
+
try {
|
|
126133
|
+
return parseQtiItemXml(question.rawXml);
|
|
126134
|
+
} catch (error88) {
|
|
126135
|
+
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${question.identifier} contains invalid QTI XML.`, { reason: errorMessage2(error88) });
|
|
126136
|
+
}
|
|
126137
|
+
}
|
|
126138
|
+
function parsePlayableItem(question) {
|
|
126139
|
+
const parsed = normalizedQtiItem(question);
|
|
126140
|
+
const interactions = playableQtiInteractions(parsed);
|
|
126141
|
+
if (!interactions) {
|
|
126142
|
+
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${question.identifier} must contain only supported interactions with unique response identifiers.`);
|
|
126143
|
+
}
|
|
126144
|
+
return {
|
|
126145
|
+
identifier: question.identifier,
|
|
126146
|
+
title: question.title,
|
|
126147
|
+
prompt: parsed.bodyTextWithBlanks,
|
|
126148
|
+
...parsed.bodyContent ? { promptContent: parsed.bodyContent } : {},
|
|
126149
|
+
maxScore: qtiItemMaxScore(parsed),
|
|
126150
|
+
interactions: interactions.map((interaction) => parseInteraction2(question.identifier, interaction))
|
|
126151
|
+
};
|
|
126152
|
+
}
|
|
126153
|
+
function assessmentContentRevision(test, questions) {
|
|
126154
|
+
return sha256Hex(canonicalJson2({
|
|
126155
|
+
identifier: test.identifier,
|
|
126156
|
+
title: test.title,
|
|
126157
|
+
qtiVersion: test.qtiVersion,
|
|
126158
|
+
parts: test["qti-test-part"],
|
|
126159
|
+
questions: questions.questions.map(({ reference, question }) => ({
|
|
126160
|
+
identifier: reference.identifier,
|
|
126161
|
+
rawXml: question.rawXml
|
|
126162
|
+
}))
|
|
126163
|
+
}));
|
|
126164
|
+
}
|
|
126165
|
+
async function buildPlayableAssessment(test, questions) {
|
|
126166
|
+
return {
|
|
126167
|
+
identifier: test.identifier,
|
|
126168
|
+
contractVersion: PLAYCADEMY_ASSESSMENT_CONTRACT_VERSION,
|
|
126169
|
+
contentRevision: await assessmentContentRevision(test, questions),
|
|
126170
|
+
title: test.title,
|
|
126171
|
+
items: questions.questions.map(({ question }) => parsePlayableItem(question))
|
|
126172
|
+
};
|
|
126173
|
+
}
|
|
126174
|
+
function assessmentFixtureScoringKeys(assessment, questions) {
|
|
126175
|
+
const questionByIdentifier = new Map(questions.questions.map(({ question }) => [question.identifier, question]));
|
|
126176
|
+
const correctResponses = {};
|
|
126177
|
+
const responseAreas = {};
|
|
126178
|
+
for (const item of assessment.items) {
|
|
126179
|
+
const question = questionByIdentifier.get(item.identifier);
|
|
126180
|
+
const normalizedDeclarations = question ? normalizedQtiItem(question).responseDeclarations : [];
|
|
126181
|
+
const itemResponses = {};
|
|
126182
|
+
for (const interaction of item.interactions) {
|
|
126183
|
+
const responseIdentifier = interaction.responseIdentifier;
|
|
126184
|
+
const declaration = question?.responseDeclarations?.find((candidate) => candidate.identifier === responseIdentifier);
|
|
126185
|
+
const declaredValues = declaration?.correctResponse?.value ?? [];
|
|
126186
|
+
const normalizedDeclaration = normalizedDeclarations.find((candidate) => candidate.identifier === responseIdentifier);
|
|
126187
|
+
const values = declaredValues.length > 0 ? declaredValues : normalizedDeclaration?.correctValues ?? [];
|
|
126188
|
+
const areas = interaction.type === "select-point" || interaction.type === "position-object" ? normalizedDeclaration?.areaMapping?.entries.filter((entry2) => entry2.value > 0 && isPlayableAssessmentShape(entry2.shape)) : undefined;
|
|
126189
|
+
if (areas?.length) {
|
|
126190
|
+
(responseAreas[item.identifier] ??= {})[responseIdentifier] = areas.map((entry2) => ({
|
|
126191
|
+
shape: entry2.shape,
|
|
126192
|
+
coords: [...entry2.coords],
|
|
126193
|
+
value: entry2.value
|
|
126194
|
+
}));
|
|
126195
|
+
}
|
|
126196
|
+
if (values.length > 0) {
|
|
126197
|
+
const cardinality = declaration?.cardinality ?? normalizedDeclaration?.cardinality;
|
|
126198
|
+
itemResponses[responseIdentifier] = cardinality === "single" && values.length === 1 ? values[0] : values;
|
|
126199
|
+
} else if (!areas?.length) {
|
|
126200
|
+
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${item.identifier} has no fixture scoring key.`);
|
|
126201
|
+
}
|
|
126202
|
+
}
|
|
126203
|
+
correctResponses[item.identifier] = itemResponses;
|
|
126204
|
+
}
|
|
126205
|
+
return {
|
|
126206
|
+
correctResponses,
|
|
126207
|
+
...Object.keys(responseAreas).length > 0 ? { responseAreas } : {}
|
|
126208
|
+
};
|
|
126209
|
+
}
|
|
126210
|
+
function prepareDiagnosticAssessmentResponses(assessment, current, input) {
|
|
126211
|
+
const update2 = { [input.itemIdentifier]: input.responses };
|
|
126212
|
+
validateAssessmentResponseUpdate(assessment, update2);
|
|
126213
|
+
const responses = applyAssessmentResponseUpdate(current, update2);
|
|
126214
|
+
const item = assessment.items.find((candidate) => candidate.identifier === input.itemIdentifier);
|
|
126215
|
+
const itemResponses = responses[input.itemIdentifier];
|
|
126216
|
+
const missingResponse = item?.interactions.find((interaction) => itemResponses?.[interaction.responseIdentifier] === undefined);
|
|
126217
|
+
if (!item || !itemResponses || missingResponse) {
|
|
126218
|
+
throw new AssessmentRuntimeError("INVALID_RESPONSE", `Diagnostic item ${input.itemIdentifier} requires a response for every interaction.`, {
|
|
126219
|
+
itemIdentifier: input.itemIdentifier,
|
|
126220
|
+
...missingResponse ? { responseIdentifier: missingResponse.responseIdentifier } : {}
|
|
126221
|
+
});
|
|
126222
|
+
}
|
|
126223
|
+
validateAssessmentResponses(assessment, { [input.itemIdentifier]: itemResponses });
|
|
126224
|
+
return responses;
|
|
126225
|
+
}
|
|
126226
|
+
function validateAssessmentResponseUpdate(assessment, update2) {
|
|
126227
|
+
const message = assessmentResponseUpdateValidationMessage(assessment, update2);
|
|
126228
|
+
if (message) {
|
|
126229
|
+
throw new AssessmentRuntimeError("INVALID_RESPONSE", message);
|
|
126230
|
+
}
|
|
126231
|
+
}
|
|
126232
|
+
function validateAssessmentResponses(assessment, responses) {
|
|
126233
|
+
const message = assessmentResponseValidationMessage(assessment, responses);
|
|
126234
|
+
if (message) {
|
|
126235
|
+
throw new AssessmentRuntimeError("INVALID_RESPONSE", message);
|
|
126236
|
+
}
|
|
126237
|
+
}
|
|
126238
|
+
function validateAssessmentDraftResponses(assessment, responses) {
|
|
126239
|
+
const message = assessmentDraftResponseValidationMessage(assessment, responses);
|
|
126240
|
+
if (message) {
|
|
126241
|
+
throw new AssessmentRuntimeError("INVALID_RESPONSE", message);
|
|
126242
|
+
}
|
|
126243
|
+
}
|
|
126244
|
+
function assessmentScoringRequests(assessment, responses) {
|
|
126245
|
+
return assessment.items.map((item) => ({
|
|
126246
|
+
itemIdentifier: item.identifier,
|
|
126247
|
+
maxScore: item.maxScore,
|
|
126248
|
+
requests: item.interactions.map((interaction) => ({
|
|
126249
|
+
identifier: interaction.responseIdentifier,
|
|
126250
|
+
response: responses[item.identifier]?.[interaction.responseIdentifier] ?? ""
|
|
126251
|
+
}))
|
|
126252
|
+
}));
|
|
126253
|
+
}
|
|
126254
|
+
function buildAssessmentResultMetadata(input) {
|
|
126255
|
+
return {
|
|
126256
|
+
[PLAYCADEMY_ASSESSMENT_RESULT_METADATA_KEY]: input.metadata,
|
|
126257
|
+
assessmentPurpose: input.metadata.purpose,
|
|
126258
|
+
...assessmentPurposeClassificationFields2(input.metadata.purpose, input)
|
|
126259
|
+
};
|
|
126260
|
+
}
|
|
126261
|
+
function mergeAssessmentRuntimeMetadata(resultMetadata, metadata2) {
|
|
126262
|
+
return {
|
|
126263
|
+
...resultMetadata,
|
|
126264
|
+
[PLAYCADEMY_ASSESSMENT_RESULT_METADATA_KEY]: metadata2
|
|
126265
|
+
};
|
|
126266
|
+
}
|
|
126267
|
+
function aggregateAssessmentScore(itemScores, possible) {
|
|
126268
|
+
const earned = itemScores.reduce((sum, itemScore) => sum + (Number.isFinite(itemScore) ? itemScore : 0), 0);
|
|
126269
|
+
return {
|
|
126270
|
+
earned,
|
|
126271
|
+
possible,
|
|
126272
|
+
normalized: possible === 0 ? 0 : Math.min(1, Math.max(0, earned / possible))
|
|
126273
|
+
};
|
|
126274
|
+
}
|
|
126275
|
+
function countCorrectQuestions(verdicts) {
|
|
126276
|
+
return verdicts.filter((verdict) => verdict === true).length;
|
|
126277
|
+
}
|
|
126278
|
+
function assessmentScoreFromPercentage(percentage, possible) {
|
|
126279
|
+
const normalized = Math.min(1, Math.max(0, (percentage ?? 0) / 100));
|
|
126280
|
+
const earned = Math.round(normalized * possible * 1e6) / 1e6;
|
|
126281
|
+
return { earned, possible, normalized };
|
|
126282
|
+
}
|
|
126283
|
+
function buildAssessmentResultSubmission(input) {
|
|
126284
|
+
const metadata2 = {
|
|
126285
|
+
...input.metadata,
|
|
126286
|
+
submissionId: input.submissionId,
|
|
126287
|
+
score: input.score,
|
|
126288
|
+
completion: {
|
|
126289
|
+
testName: input.testName,
|
|
126290
|
+
courseName: input.courseName,
|
|
126291
|
+
totalQuestions: input.totalQuestions,
|
|
126292
|
+
correctQuestions: input.correctQuestions,
|
|
126293
|
+
...input.session ? { session: input.session } : {},
|
|
126294
|
+
...input.itemOutcomes ? { itemOutcomes: input.itemOutcomes } : {},
|
|
126295
|
+
...input.diagnosticTracks ? { diagnosticTracks: input.diagnosticTracks } : {}
|
|
126296
|
+
},
|
|
126297
|
+
updatedAt: input.timestamp
|
|
126298
|
+
};
|
|
126299
|
+
return {
|
|
126300
|
+
metadata: metadata2,
|
|
126301
|
+
resultUpdate: {
|
|
126302
|
+
status: input.result.status,
|
|
126303
|
+
assessmentLineItem: input.result.assessmentLineItem,
|
|
126304
|
+
student: input.result.student,
|
|
126305
|
+
score: input.score.normalized * 100,
|
|
126306
|
+
scoreDate: input.timestamp,
|
|
126307
|
+
...ASSESSMENT_ATTEMPT_AWAITING_AWARD,
|
|
126308
|
+
metadata: mergeAssessmentRuntimeMetadata(input.result.metadata, metadata2)
|
|
126309
|
+
}
|
|
126310
|
+
};
|
|
126311
|
+
}
|
|
126312
|
+
function buildAssessmentResultAwardFinalization(input) {
|
|
126313
|
+
const completion = input.metadata.completion;
|
|
126314
|
+
if (!completion || completion.totalQuestions === undefined) {
|
|
126315
|
+
throw new Error("A scored assessment must retain its completion facts before finalization");
|
|
126316
|
+
}
|
|
126317
|
+
const metadata2 = {
|
|
126318
|
+
...input.metadata,
|
|
126319
|
+
finalizedAt: input.timestamp,
|
|
126320
|
+
updatedAt: input.timestamp,
|
|
126321
|
+
award: input.award
|
|
126322
|
+
};
|
|
126323
|
+
const masteryRequested = input.award.masteredUnits !== undefined || input.award.masteredUnitsAbsolute !== undefined;
|
|
126324
|
+
return {
|
|
126325
|
+
metadata: metadata2,
|
|
126326
|
+
resultUpdate: {
|
|
126327
|
+
status: input.result.status,
|
|
126328
|
+
assessmentLineItem: input.result.assessmentLineItem,
|
|
126329
|
+
student: input.result.student,
|
|
126330
|
+
score: input.result.score ?? 0,
|
|
126331
|
+
scoreDate: input.result.scoreDate,
|
|
126332
|
+
...ASSESSMENT_ATTEMPT_COMPLETED,
|
|
126333
|
+
metadata: {
|
|
126334
|
+
...mergeAssessmentRuntimeMetadata(input.result.metadata, metadata2),
|
|
126335
|
+
xp: input.award.xpAwarded,
|
|
126336
|
+
...masteryRequested ? { masteredUnits: input.award.masteredUnitsApplied } : {},
|
|
126337
|
+
totalQuestions: completion.totalQuestions,
|
|
126338
|
+
correctQuestions: completion.correctQuestions,
|
|
126339
|
+
appName: input.appName
|
|
126340
|
+
}
|
|
126341
|
+
}
|
|
126342
|
+
};
|
|
126343
|
+
}
|
|
126344
|
+
var PLAYABLE_SHAPE_SET;
|
|
126345
|
+
var init_timeback_assessment_runtime_util = __esm(() => {
|
|
126346
|
+
init_src();
|
|
126347
|
+
init_assessment_runtime2();
|
|
126348
|
+
init_qti();
|
|
126349
|
+
init_types2();
|
|
126350
|
+
init_utils6();
|
|
126351
|
+
init_timeback4();
|
|
126352
|
+
init_uuid();
|
|
126353
|
+
init_errors2();
|
|
126354
|
+
PLAYABLE_SHAPE_SET = new Set(PLAYABLE_ASSESSMENT_SHAPES);
|
|
126355
|
+
});
|
|
124367
126356
|
function recordValue(value) {
|
|
124368
126357
|
return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
124369
126358
|
}
|
|
@@ -125306,808 +127295,6 @@ var init_timeback_qti_authoring_util = __esm(() => {
|
|
|
125306
127295
|
buildMatchQuestionXml
|
|
125307
127296
|
];
|
|
125308
127297
|
});
|
|
125309
|
-
function assessmentPreparationContentionError(expectedResponseVersion, responseVersion, kind) {
|
|
125310
|
-
if (expectedResponseVersion === responseVersion) {
|
|
125311
|
-
return new ServiceUnavailableError(`The ${kind} response state changed repeatedly while this item was being prepared. Try again shortly.`, {
|
|
125312
|
-
retryable: true,
|
|
125313
|
-
reason: "PREPARATION_CONTENTION",
|
|
125314
|
-
expectedResponseVersion,
|
|
125315
|
-
responseVersion
|
|
125316
|
-
});
|
|
125317
|
-
}
|
|
125318
|
-
return AssessmentRuntimeError.from(assessmentResponseVersionConflict(expectedResponseVersion, responseVersion));
|
|
125319
|
-
}
|
|
125320
|
-
function assessmentTrackableCourse(integration) {
|
|
125321
|
-
if (!isTimebackGrade2(integration.grade) || !isTimebackSubject(integration.subject)) {
|
|
125322
|
-
return null;
|
|
125323
|
-
}
|
|
125324
|
-
return { grade: integration.grade, subject: integration.subject };
|
|
125325
|
-
}
|
|
125326
|
-
function buildAssessmentActivityData(input) {
|
|
125327
|
-
if (!input.course) {
|
|
125328
|
-
return;
|
|
125329
|
-
}
|
|
125330
|
-
return {
|
|
125331
|
-
activityId: input.activityId,
|
|
125332
|
-
activityName: input.activityName,
|
|
125333
|
-
...input.course,
|
|
125334
|
-
courseId: input.courseId
|
|
125335
|
-
};
|
|
125336
|
-
}
|
|
125337
|
-
function buildDiagnosticAssessmentSnapshot(input) {
|
|
125338
|
-
return {
|
|
125339
|
-
attemptId: input.attemptId,
|
|
125340
|
-
responseVersion: input.metadata.responseVersion,
|
|
125341
|
-
status: "in_progress",
|
|
125342
|
-
flow: "platform-routed-item-submit",
|
|
125343
|
-
...input.activityData ? { activityData: input.activityData } : {},
|
|
125344
|
-
assessment: assessmentPresentationForAttempt(input.assessment, input.attemptId),
|
|
125345
|
-
selection: {
|
|
125346
|
-
kind: "platform-routed-diagnostic",
|
|
125347
|
-
purpose: "diagnostic",
|
|
125348
|
-
definitionId: input.metadata.diagnostic.definitionId,
|
|
125349
|
-
diagnosticKey: input.metadata.diagnostic.diagnosticKey,
|
|
125350
|
-
routingRevision: input.metadata.diagnostic.routingRevision
|
|
125351
|
-
},
|
|
125352
|
-
routing: projectDiagnosticRoutingSnapshot(input.metadata.diagnostic.routingRevision, input.state),
|
|
125353
|
-
completion: null
|
|
125354
|
-
};
|
|
125355
|
-
}
|
|
125356
|
-
function buildConventionalAssessmentSnapshot(input) {
|
|
125357
|
-
const metadata2 = input.metadata;
|
|
125358
|
-
let selection;
|
|
125359
|
-
if (metadata2.purpose === "review") {
|
|
125360
|
-
selection = {
|
|
125361
|
-
kind: "standards-review",
|
|
125362
|
-
purpose: metadata2.purpose,
|
|
125363
|
-
standards: [...metadata2.review.standards],
|
|
125364
|
-
candidateItemsPerStandard: metadata2.review.candidateItemsPerStandard,
|
|
125365
|
-
selections: [...metadata2.review.selections],
|
|
125366
|
-
fulfillment: metadata2.review.fulfillment
|
|
125367
|
-
};
|
|
125368
|
-
} else if (metadata2.purpose === "mastery") {
|
|
125369
|
-
selection = {
|
|
125370
|
-
kind: "standard-quiz",
|
|
125371
|
-
purpose: metadata2.purpose,
|
|
125372
|
-
standard: metadata2.mastery.standard
|
|
125373
|
-
};
|
|
125374
|
-
} else {
|
|
125375
|
-
selection = { kind: "fixed-test", purpose: metadata2.purpose };
|
|
125376
|
-
}
|
|
125377
|
-
return {
|
|
125378
|
-
attemptId: input.attemptId,
|
|
125379
|
-
responseVersion: metadata2.responseVersion,
|
|
125380
|
-
status: "in_progress",
|
|
125381
|
-
flow: assessmentFlowForPurpose(metadata2.purpose),
|
|
125382
|
-
...input.activityData ? { activityData: input.activityData } : {},
|
|
125383
|
-
assessment: assessmentPresentationForAttempt(input.assessment, input.attemptId),
|
|
125384
|
-
responses: metadata2.responses,
|
|
125385
|
-
itemSubmissions: metadata2.itemSubmissions,
|
|
125386
|
-
score: null,
|
|
125387
|
-
selection
|
|
125388
|
-
};
|
|
125389
|
-
}
|
|
125390
|
-
function buildAssessmentAwardRecord(input, mastery) {
|
|
125391
|
-
return {
|
|
125392
|
-
xpAwarded: input.xpAwarded,
|
|
125393
|
-
...input.masteredUnits !== undefined ? { masteredUnits: input.masteredUnits } : {},
|
|
125394
|
-
...input.masteredUnitsAbsolute !== undefined ? { masteredUnitsAbsolute: input.masteredUnitsAbsolute } : {},
|
|
125395
|
-
masteredUnitsApplied: mastery.masteredUnitsApplied,
|
|
125396
|
-
...mastery.pctCompleteApp !== undefined ? { pctCompleteApp: mastery.pctCompleteApp } : {},
|
|
125397
|
-
...mastery.warnings ? { warnings: mastery.warnings } : {}
|
|
125398
|
-
};
|
|
125399
|
-
}
|
|
125400
|
-
function stageAssessmentAttemptSupersession(attempt) {
|
|
125401
|
-
Object.assign(attempt.result, ASSESSMENT_ATTEMPT_SUPERSEDED);
|
|
125402
|
-
Object.assign(attempt.selection, ASSESSMENT_ATTEMPT_SUPERSEDED);
|
|
125403
|
-
}
|
|
125404
|
-
function shouldConfirmAssessmentVersionUnavailable(details) {
|
|
125405
|
-
return typeof details === "object" && details !== null && "expectedRevision" in details && typeof details.expectedRevision === "string" && !("currentRevision" in details);
|
|
125406
|
-
}
|
|
125407
|
-
function buildAssessmentAttemptSupersessionUpdate(result) {
|
|
125408
|
-
if (result.status !== "active" || !isAssessmentAttemptOpen({
|
|
125409
|
-
inProgress: result.inProgress ?? "",
|
|
125410
|
-
scoreStatus: result.scoreStatus
|
|
125411
|
-
})) {
|
|
125412
|
-
return null;
|
|
125413
|
-
}
|
|
125414
|
-
return {
|
|
125415
|
-
status: "active",
|
|
125416
|
-
assessmentLineItem: result.assessmentLineItem,
|
|
125417
|
-
student: result.student,
|
|
125418
|
-
score: result.score ?? 0,
|
|
125419
|
-
scoreDate: result.scoreDate,
|
|
125420
|
-
...ASSESSMENT_ATTEMPT_SUPERSEDED,
|
|
125421
|
-
metadata: result.metadata ?? {}
|
|
125422
|
-
};
|
|
125423
|
-
}
|
|
125424
|
-
async function resolveAvailableAssessmentAttempt(initialDecision, select3, resume) {
|
|
125425
|
-
let decision = initialDecision;
|
|
125426
|
-
while (decision?.kind === "resume") {
|
|
125427
|
-
const snapshot = await resume(decision);
|
|
125428
|
-
if (snapshot !== null) {
|
|
125429
|
-
return { kind: "resumed", snapshot };
|
|
125430
|
-
}
|
|
125431
|
-
decision = select3();
|
|
125432
|
-
}
|
|
125433
|
-
return { kind: "continue", decision };
|
|
125434
|
-
}
|
|
125435
|
-
function buildReviewItemResultMetadata(input) {
|
|
125436
|
-
const administration = itemAdministration(input.metadata.itemSubmissions, input.selection.itemIdentifier);
|
|
125437
|
-
if (!administration) {
|
|
125438
|
-
throw new Error(`Cannot create a review result for unadministered candidate ${input.selection.itemIdentifier}`);
|
|
125439
|
-
}
|
|
125440
|
-
return {
|
|
125441
|
-
version: PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_VERSION,
|
|
125442
|
-
parentAttemptId: input.parentAttemptId,
|
|
125443
|
-
activityId: input.metadata.activityId,
|
|
125444
|
-
integrationId: input.metadata.integrationId,
|
|
125445
|
-
enrollmentId: input.metadata.enrollmentId,
|
|
125446
|
-
bankRevision: input.metadata.review.bankRevision,
|
|
125447
|
-
itemIdentifier: input.selection.itemIdentifier,
|
|
125448
|
-
standard: input.selection.standard,
|
|
125449
|
-
deliveredAt: administration.submittedAt,
|
|
125450
|
-
responseVersion: input.responseVersion ?? input.metadata.responseVersion,
|
|
125451
|
-
responses: input.metadata.responses[input.selection.itemIdentifier] ?? {},
|
|
125452
|
-
...input.submissionId ? { submissionId: input.submissionId } : {},
|
|
125453
|
-
...input.isCorrect === undefined ? {} : { isCorrect: input.isCorrect }
|
|
125454
|
-
};
|
|
125455
|
-
}
|
|
125456
|
-
function buildOpenReviewChildResult(input) {
|
|
125457
|
-
return {
|
|
125458
|
-
status: "active",
|
|
125459
|
-
assessmentLineItem: { sourcedId: input.childLineItemId },
|
|
125460
|
-
student: input.student,
|
|
125461
|
-
score: 0,
|
|
125462
|
-
scoreDate: input.metadata.deliveredAt,
|
|
125463
|
-
...ASSESSMENT_ATTEMPT_OPEN,
|
|
125464
|
-
metadata: {
|
|
125465
|
-
[PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY]: input.metadata
|
|
125466
|
-
}
|
|
125467
|
-
};
|
|
125468
|
-
}
|
|
125469
|
-
function diagnosticQuestionLineItemId(parentLineItemId, itemIdentifier) {
|
|
125470
|
-
return deterministicUUID(["playcademy:diagnostic-question-line-item:v1", parentLineItemId, itemIdentifier].join("\x00"));
|
|
125471
|
-
}
|
|
125472
|
-
function diagnosticQuestionResultId(parentAttemptId, childLineItemId) {
|
|
125473
|
-
return deterministicUUID(["playcademy:diagnostic-question-result:v1", parentAttemptId, childLineItemId].join("\x00"));
|
|
125474
|
-
}
|
|
125475
|
-
function diagnosticAdministration(input) {
|
|
125476
|
-
const ledgerIndex = input.metadata.diagnostic.ledger.findIndex((entry2) => entry2.routingNodeKey === input.ledgerEntry.routingNodeKey);
|
|
125477
|
-
const persistedEntry = input.metadata.diagnostic.ledger[ledgerIndex];
|
|
125478
|
-
const administration = input.metadata.itemSubmissions[ledgerIndex];
|
|
125479
|
-
if (ledgerIndex === -1 || !persistedEntry || !administration || persistedEntry.stageKey !== input.ledgerEntry.stageKey || persistedEntry.trackKey !== input.ledgerEntry.trackKey || persistedEntry.itemIdentifier !== input.ledgerEntry.itemIdentifier || persistedEntry.isCorrect !== input.ledgerEntry.isCorrect || administration.itemIdentifier !== input.ledgerEntry.itemIdentifier || administration.isCorrect !== input.ledgerEntry.isCorrect) {
|
|
125480
|
-
throw new Error(`Cannot create a diagnostic result for uncommitted routing node ${input.ledgerEntry.routingNodeKey}`);
|
|
125481
|
-
}
|
|
125482
|
-
return administration;
|
|
125483
|
-
}
|
|
125484
|
-
function buildDiagnosticItemResultMetadata(input) {
|
|
125485
|
-
const administration = diagnosticAdministration(input);
|
|
125486
|
-
return {
|
|
125487
|
-
version: PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_VERSION,
|
|
125488
|
-
parentAttemptId: input.parentAttemptId,
|
|
125489
|
-
activityId: input.metadata.activityId,
|
|
125490
|
-
integrationId: input.metadata.integrationId,
|
|
125491
|
-
enrollmentId: input.metadata.enrollmentId,
|
|
125492
|
-
definitionId: input.metadata.diagnostic.definitionId,
|
|
125493
|
-
diagnosticKey: input.metadata.diagnostic.diagnosticKey,
|
|
125494
|
-
routingRevision: input.metadata.diagnostic.routingRevision,
|
|
125495
|
-
contentRevision: input.metadata.selectedTest.contentRevision,
|
|
125496
|
-
stageKey: input.ledgerEntry.stageKey,
|
|
125497
|
-
trackKey: input.ledgerEntry.trackKey,
|
|
125498
|
-
routingNodeKey: input.ledgerEntry.routingNodeKey,
|
|
125499
|
-
itemIdentifier: input.ledgerEntry.itemIdentifier,
|
|
125500
|
-
deliveredAt: administration.submittedAt,
|
|
125501
|
-
responseVersion: administration.responseVersion,
|
|
125502
|
-
responses: input.metadata.responses[input.ledgerEntry.itemIdentifier] ?? {},
|
|
125503
|
-
submissionId: administration.submissionId,
|
|
125504
|
-
isCorrect: input.ledgerEntry.isCorrect
|
|
125505
|
-
};
|
|
125506
|
-
}
|
|
125507
|
-
function resolveDiagnosticChildResult(input) {
|
|
125508
|
-
if (!input.childResult) {
|
|
125509
|
-
return { action: "create" };
|
|
125510
|
-
}
|
|
125511
|
-
const childMetadata = playcademyDiagnosticAssessmentItemResultMetadata(input.childResult.metadata);
|
|
125512
|
-
const expectedMetadata = buildDiagnosticItemResultMetadata({
|
|
125513
|
-
metadata: input.parentMetadata,
|
|
125514
|
-
ledgerEntry: input.ledgerEntry,
|
|
125515
|
-
parentAttemptId: input.parentResult.sourcedId
|
|
125516
|
-
});
|
|
125517
|
-
if (input.childResult.assessmentLineItem.sourcedId !== input.childLineItemId || input.childResult.student.sourcedId !== input.parentResult.student.sourcedId || !childMetadata || canonicalJson2(childMetadata) !== canonicalJson2(expectedMetadata)) {
|
|
125518
|
-
return { action: "restore", reason: "incompatible" };
|
|
125519
|
-
}
|
|
125520
|
-
return input.childResult.status === "active" ? { action: "reuse", metadata: expectedMetadata } : { action: "restore", reason: "inactive" };
|
|
125521
|
-
}
|
|
125522
|
-
function buildFinalizedDiagnosticChildResult(input) {
|
|
125523
|
-
const administration = diagnosticAdministration(input);
|
|
125524
|
-
const metadata2 = buildDiagnosticItemResultMetadata(input);
|
|
125525
|
-
return {
|
|
125526
|
-
resultUpdate: {
|
|
125527
|
-
status: "active",
|
|
125528
|
-
assessmentLineItem: { sourcedId: input.childLineItemId },
|
|
125529
|
-
student: input.student,
|
|
125530
|
-
score: administration.score.earned,
|
|
125531
|
-
scoreDate: administration.submittedAt,
|
|
125532
|
-
...ASSESSMENT_ATTEMPT_COMPLETED,
|
|
125533
|
-
metadata: {
|
|
125534
|
-
[PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY]: metadata2
|
|
125535
|
-
}
|
|
125536
|
-
}
|
|
125537
|
-
};
|
|
125538
|
-
}
|
|
125539
|
-
function buildFinalizedReviewChildResult(input) {
|
|
125540
|
-
const metadata2 = buildReviewItemResultMetadata({
|
|
125541
|
-
metadata: input.metadata,
|
|
125542
|
-
selection: input.selection,
|
|
125543
|
-
parentAttemptId: input.parentAttemptId,
|
|
125544
|
-
submissionId: input.submissionId,
|
|
125545
|
-
isCorrect: input.itemResult.isCorrect
|
|
125546
|
-
});
|
|
125547
|
-
return {
|
|
125548
|
-
resultUpdate: {
|
|
125549
|
-
status: "active",
|
|
125550
|
-
assessmentLineItem: { sourcedId: input.childLineItemId },
|
|
125551
|
-
student: input.student,
|
|
125552
|
-
score: input.itemResult.score.earned,
|
|
125553
|
-
scoreDate: input.timestamp,
|
|
125554
|
-
...ASSESSMENT_ATTEMPT_COMPLETED,
|
|
125555
|
-
metadata: {
|
|
125556
|
-
[PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY]: metadata2
|
|
125557
|
-
}
|
|
125558
|
-
},
|
|
125559
|
-
outcome: {
|
|
125560
|
-
itemIdentifier: input.selection.itemIdentifier,
|
|
125561
|
-
standard: input.selection.standard,
|
|
125562
|
-
answered: Object.keys(input.metadata.responses[input.selection.itemIdentifier] ?? {}).length > 0,
|
|
125563
|
-
score: input.itemResult.score,
|
|
125564
|
-
isCorrect: input.itemResult.isCorrect
|
|
125565
|
-
}
|
|
125566
|
-
};
|
|
125567
|
-
}
|
|
125568
|
-
function resolveReviewChildResult(input) {
|
|
125569
|
-
if (!input.childResult) {
|
|
125570
|
-
return { action: "create" };
|
|
125571
|
-
}
|
|
125572
|
-
const childMetadata = playcademyAssessmentItemResultMetadata(input.childResult.metadata);
|
|
125573
|
-
const expectedResponses = input.parentMetadata.responses[input.selection.itemIdentifier] ?? {};
|
|
125574
|
-
const administration = itemAdministration(input.parentMetadata.itemSubmissions, input.selection.itemIdentifier);
|
|
125575
|
-
if (input.childResult.assessmentLineItem.sourcedId !== input.childLineItemId || input.childResult.student.sourcedId !== input.parentResult.student.sourcedId || !childMetadata || childMetadata.parentAttemptId !== input.parentResult.sourcedId || childMetadata.activityId !== input.parentMetadata.activityId || childMetadata.integrationId !== input.parentMetadata.integrationId || childMetadata.enrollmentId !== input.parentMetadata.enrollmentId || childMetadata.bankRevision !== input.parentMetadata.review.bankRevision || childMetadata.itemIdentifier !== input.selection.itemIdentifier || childMetadata.standard.framework !== input.selection.standard.framework || childMetadata.standard.identifier !== input.selection.standard.identifier || childMetadata.deliveredAt !== administration?.submittedAt || childMetadata.responseVersion > input.parentMetadata.responseVersion || canonicalJson2(childMetadata.responses) !== canonicalJson2(expectedResponses)) {
|
|
125576
|
-
return { action: "restore", reason: "incompatible" };
|
|
125577
|
-
}
|
|
125578
|
-
return input.childResult.status === "active" ? { action: "reuse", metadata: childMetadata } : { action: "restore", reason: "inactive" };
|
|
125579
|
-
}
|
|
125580
|
-
function buildAssessmentSubmitResult(input) {
|
|
125581
|
-
if (!input.metadata.score || !input.metadata.submissionId) {
|
|
125582
|
-
return null;
|
|
125583
|
-
}
|
|
125584
|
-
if (input.metadata.purpose === "diagnostic" && input.metadata.diagnostic) {
|
|
125585
|
-
const tracks = input.metadata.completion?.diagnosticTracks;
|
|
125586
|
-
return tracks ? {
|
|
125587
|
-
purpose: input.metadata.purpose,
|
|
125588
|
-
attemptId: input.attemptId,
|
|
125589
|
-
submissionId: input.metadata.submissionId,
|
|
125590
|
-
diagnosticKey: input.metadata.diagnostic.diagnosticKey,
|
|
125591
|
-
responseVersion: input.metadata.responseVersion,
|
|
125592
|
-
status: "awaiting_award",
|
|
125593
|
-
tracks
|
|
125594
|
-
} : null;
|
|
125595
|
-
}
|
|
125596
|
-
const base = {
|
|
125597
|
-
attemptId: input.attemptId,
|
|
125598
|
-
submissionId: input.metadata.submissionId,
|
|
125599
|
-
responseVersion: input.metadata.responseVersion,
|
|
125600
|
-
status: "awaiting_award",
|
|
125601
|
-
responses: input.metadata.responses,
|
|
125602
|
-
score: input.metadata.score
|
|
125603
|
-
};
|
|
125604
|
-
if (input.metadata.purpose === "review") {
|
|
125605
|
-
const itemOutcomes = input.metadata.completion?.itemOutcomes;
|
|
125606
|
-
return itemOutcomes ? { ...base, purpose: input.metadata.purpose, itemOutcomes } : null;
|
|
125607
|
-
}
|
|
125608
|
-
return { ...base, purpose: input.metadata.purpose };
|
|
125609
|
-
}
|
|
125610
|
-
function assessmentAwardFromResult(result, metadata2) {
|
|
125611
|
-
if (metadata2.award) {
|
|
125612
|
-
return metadata2.award;
|
|
125613
|
-
}
|
|
125614
|
-
const xp = result.metadata?.xp;
|
|
125615
|
-
return typeof xp === "number" && Number.isFinite(xp) && xp >= 0 ? { xpAwarded: xp, masteredUnitsApplied: 0 } : null;
|
|
125616
|
-
}
|
|
125617
|
-
function buildCompletedAssessmentSubmitResult(input) {
|
|
125618
|
-
const attemptId = input.result.sourcedId;
|
|
125619
|
-
const award = assessmentAwardFromResult(input.result, input.metadata);
|
|
125620
|
-
if (award === null) {
|
|
125621
|
-
throw new GoneError("This completed assessment is missing its recorded XP.", {
|
|
125622
|
-
attemptId
|
|
125623
|
-
});
|
|
125624
|
-
}
|
|
125625
|
-
const submitted = buildAssessmentSubmitResult({ attemptId, metadata: input.metadata });
|
|
125626
|
-
if (!submitted) {
|
|
125627
|
-
throw new GoneError("This completed assessment has incomplete persisted results.", {
|
|
125628
|
-
attemptId
|
|
125629
|
-
});
|
|
125630
|
-
}
|
|
125631
|
-
return {
|
|
125632
|
-
...submitted,
|
|
125633
|
-
status: "completed",
|
|
125634
|
-
xpAwarded: award.xpAwarded,
|
|
125635
|
-
masteredUnitsApplied: award.masteredUnitsApplied,
|
|
125636
|
-
...award.pctCompleteApp !== undefined ? { pctCompleteApp: award.pctCompleteApp } : {},
|
|
125637
|
-
...award.warnings ? { warnings: award.warnings } : {}
|
|
125638
|
-
};
|
|
125639
|
-
}
|
|
125640
|
-
function assessmentAttemptId(input) {
|
|
125641
|
-
return deterministicUUID([
|
|
125642
|
-
"playcademy:assessment-attempt",
|
|
125643
|
-
input.gameId,
|
|
125644
|
-
input.studentId,
|
|
125645
|
-
input.courseId,
|
|
125646
|
-
input.activityId,
|
|
125647
|
-
input.purpose,
|
|
125648
|
-
String(input.attemptNumber)
|
|
125649
|
-
].join("\x00"));
|
|
125650
|
-
}
|
|
125651
|
-
async function findLatestAssessmentResult(results, match) {
|
|
125652
|
-
let latest = null;
|
|
125653
|
-
let latestTimestamp = 0;
|
|
125654
|
-
for await (const result of results) {
|
|
125655
|
-
const resultTimestamp = assessmentAttemptTimestamp(result.scoreDate);
|
|
125656
|
-
if (latest && resultTimestamp < latestTimestamp) {
|
|
125657
|
-
break;
|
|
125658
|
-
}
|
|
125659
|
-
const matched = match(result);
|
|
125660
|
-
if (matched !== null && (!latest || compareNewestCompletedAssessmentAttempt({ attemptId: result.sourcedId, scoreDate: result.scoreDate }, {
|
|
125661
|
-
attemptId: latest.result.sourcedId,
|
|
125662
|
-
scoreDate: latest.result.scoreDate
|
|
125663
|
-
}) < 0)) {
|
|
125664
|
-
latest = { result, match: matched };
|
|
125665
|
-
latestTimestamp = resultTimestamp;
|
|
125666
|
-
}
|
|
125667
|
-
}
|
|
125668
|
-
return latest;
|
|
125669
|
-
}
|
|
125670
|
-
function playableChoice(choice) {
|
|
125671
|
-
return {
|
|
125672
|
-
identifier: choice.identifier,
|
|
125673
|
-
content: choice.content,
|
|
125674
|
-
...choice.contentNodes ? { contentNodes: choice.contentNodes } : {}
|
|
125675
|
-
};
|
|
125676
|
-
}
|
|
125677
|
-
function playableAssociableChoice(choice) {
|
|
125678
|
-
const matchMax = qtiAttributeCapacity(choice.attributes, "match-max", 0);
|
|
125679
|
-
if (matchMax === null) {
|
|
125680
|
-
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", "The selected assessment contains an invalid Match capacity.");
|
|
125681
|
-
}
|
|
125682
|
-
return {
|
|
125683
|
-
...playableChoice(choice),
|
|
125684
|
-
matchMax
|
|
125685
|
-
};
|
|
125686
|
-
}
|
|
125687
|
-
function requireChoices2(choices) {
|
|
125688
|
-
if (choices.length === 0 || choices.some((candidate) => !candidate.identifier)) {
|
|
125689
|
-
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", "The selected assessment contains an interaction Playcademy cannot render.");
|
|
125690
|
-
}
|
|
125691
|
-
return choices;
|
|
125692
|
-
}
|
|
125693
|
-
function isPlayableAssessmentShape(value) {
|
|
125694
|
-
return PLAYABLE_SHAPE_SET.has(value);
|
|
125695
|
-
}
|
|
125696
|
-
function playableGraphic(graphic) {
|
|
125697
|
-
return {
|
|
125698
|
-
src: graphic.src,
|
|
125699
|
-
...graphic.width === undefined ? {} : { width: graphic.width },
|
|
125700
|
-
...graphic.height === undefined ? {} : { height: graphic.height },
|
|
125701
|
-
...graphic.description ? { description: graphic.description } : {}
|
|
125702
|
-
};
|
|
125703
|
-
}
|
|
125704
|
-
function requireGraphic(questionIdentifier, graphic) {
|
|
125705
|
-
if (!graphic) {
|
|
125706
|
-
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${questionIdentifier} is missing a required graphic.`);
|
|
125707
|
-
}
|
|
125708
|
-
return playableGraphic(graphic);
|
|
125709
|
-
}
|
|
125710
|
-
function requireHotspots(questionIdentifier, hotspots) {
|
|
125711
|
-
if (hotspots.length === 0) {
|
|
125712
|
-
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${questionIdentifier} declares no graphic regions.`);
|
|
125713
|
-
}
|
|
125714
|
-
return hotspots.map((hotspot) => ({
|
|
125715
|
-
identifier: hotspot.identifier,
|
|
125716
|
-
shape: isPlayableAssessmentShape(hotspot.shape) ? hotspot.shape : "default",
|
|
125717
|
-
coords: [...hotspot.coords],
|
|
125718
|
-
...hotspot.matchMax === undefined ? {} : { matchMax: hotspot.matchMax },
|
|
125719
|
-
...hotspot.description ? { description: hotspot.description } : {}
|
|
125720
|
-
}));
|
|
125721
|
-
}
|
|
125722
|
-
function parseInteraction2(questionIdentifier, interaction) {
|
|
125723
|
-
const { type } = interaction;
|
|
125724
|
-
const responseIdentifier = interaction.attributes["response-identifier"];
|
|
125725
|
-
if (!responseIdentifier) {
|
|
125726
|
-
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${questionIdentifier} has no response identifier.`);
|
|
125727
|
-
}
|
|
125728
|
-
const prompt = interaction.embeddedPrompt ? { prompt: interaction.embeddedPrompt } : {};
|
|
125729
|
-
if (type === "choice") {
|
|
125730
|
-
const choices = requireChoices2(interaction.choices.map(playableChoice));
|
|
125731
|
-
return {
|
|
125732
|
-
type,
|
|
125733
|
-
responseIdentifier,
|
|
125734
|
-
...prompt,
|
|
125735
|
-
choices,
|
|
125736
|
-
minChoices: qtiAttributeNumber(interaction.attributes, "min-choices", 1),
|
|
125737
|
-
maxChoices: qtiAttributeMaximum(interaction.attributes, "max-choices", 1, choices.length)
|
|
125738
|
-
};
|
|
125739
|
-
}
|
|
125740
|
-
if (type === "inline-choice") {
|
|
125741
|
-
return {
|
|
125742
|
-
type,
|
|
125743
|
-
responseIdentifier,
|
|
125744
|
-
...prompt,
|
|
125745
|
-
choices: requireChoices2(interaction.choices.map(playableChoice))
|
|
125746
|
-
};
|
|
125747
|
-
}
|
|
125748
|
-
if (type === "text-entry") {
|
|
125749
|
-
const expectedLength = qtiAttributeNumber(interaction.attributes, "expected-length", 0);
|
|
125750
|
-
return {
|
|
125751
|
-
type,
|
|
125752
|
-
responseIdentifier,
|
|
125753
|
-
...prompt,
|
|
125754
|
-
...Number.isInteger(expectedLength) && expectedLength > 0 ? { expectedLength } : {},
|
|
125755
|
-
...interaction.attributes.placeholder ? { placeholder: interaction.attributes.placeholder } : {}
|
|
125756
|
-
};
|
|
125757
|
-
}
|
|
125758
|
-
if (type === "order") {
|
|
125759
|
-
return {
|
|
125760
|
-
type,
|
|
125761
|
-
responseIdentifier,
|
|
125762
|
-
...prompt,
|
|
125763
|
-
choices: requireChoices2(interaction.choices.map(playableChoice)),
|
|
125764
|
-
shuffle: qtiAttributeBoolean(interaction.attributes, "shuffle")
|
|
125765
|
-
};
|
|
125766
|
-
}
|
|
125767
|
-
if (type === "match") {
|
|
125768
|
-
const sets = interaction.choiceSets;
|
|
125769
|
-
if (sets.length !== 2) {
|
|
125770
|
-
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", "Match interactions require exactly two choice sets.");
|
|
125771
|
-
}
|
|
125772
|
-
const sourceChoices = requireChoices2(sets[0].map(playableAssociableChoice));
|
|
125773
|
-
const targetChoices = requireChoices2(sets[1].map(playableAssociableChoice));
|
|
125774
|
-
return {
|
|
125775
|
-
type,
|
|
125776
|
-
responseIdentifier,
|
|
125777
|
-
...prompt,
|
|
125778
|
-
sourceChoices,
|
|
125779
|
-
targetChoices,
|
|
125780
|
-
maxAssociations: qtiAttributeMaximum(interaction.attributes, "max-associations", 1, sourceChoices.length * targetChoices.length),
|
|
125781
|
-
shuffle: qtiAttributeBoolean(interaction.attributes, "shuffle")
|
|
125782
|
-
};
|
|
125783
|
-
}
|
|
125784
|
-
if (type === "hotspot") {
|
|
125785
|
-
const image = requireGraphic(questionIdentifier, interaction.graphic);
|
|
125786
|
-
const hotspots = requireHotspots(questionIdentifier, interaction.hotspots);
|
|
125787
|
-
return {
|
|
125788
|
-
type,
|
|
125789
|
-
responseIdentifier,
|
|
125790
|
-
...prompt,
|
|
125791
|
-
image,
|
|
125792
|
-
hotspots,
|
|
125793
|
-
minChoices: qtiAttributeNumber(interaction.attributes, "min-choices", 1),
|
|
125794
|
-
maxChoices: qtiAttributeMaximum(interaction.attributes, "max-choices", 1, hotspots.length)
|
|
125795
|
-
};
|
|
125796
|
-
}
|
|
125797
|
-
if (type === "gap-match") {
|
|
125798
|
-
if (interaction.gapTexts.length === 0 || interaction.gaps.length === 0 || interaction.textWithGaps === undefined || !interaction.gapContent || !qtiGapContentMatchesGaps(interaction.gapContent, interaction.gaps)) {
|
|
125799
|
-
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${questionIdentifier} has an incomplete gap-match interaction.`);
|
|
125800
|
-
}
|
|
125801
|
-
return {
|
|
125802
|
-
type,
|
|
125803
|
-
responseIdentifier,
|
|
125804
|
-
...prompt,
|
|
125805
|
-
content: interaction.textWithGaps,
|
|
125806
|
-
contentNodes: interaction.gapContent,
|
|
125807
|
-
gapTexts: interaction.gapTexts.map((gapText) => ({
|
|
125808
|
-
identifier: gapText.identifier,
|
|
125809
|
-
content: gapText.content,
|
|
125810
|
-
matchMax: gapText.matchMax
|
|
125811
|
-
})),
|
|
125812
|
-
gaps: [...interaction.gaps],
|
|
125813
|
-
maxAssociations: qtiAttributeNumber(interaction.attributes, "max-associations", 0)
|
|
125814
|
-
};
|
|
125815
|
-
}
|
|
125816
|
-
if (type === "graphic-gap-match") {
|
|
125817
|
-
const image = requireGraphic(questionIdentifier, interaction.graphic);
|
|
125818
|
-
const hotspots = requireHotspots(questionIdentifier, interaction.hotspots);
|
|
125819
|
-
if (interaction.gapImages.length === 0) {
|
|
125820
|
-
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${questionIdentifier} has no draggable gap images.`);
|
|
125821
|
-
}
|
|
125822
|
-
return {
|
|
125823
|
-
type,
|
|
125824
|
-
responseIdentifier,
|
|
125825
|
-
...prompt,
|
|
125826
|
-
image,
|
|
125827
|
-
gapImages: interaction.gapImages.map((gapImage) => ({
|
|
125828
|
-
identifier: gapImage.identifier,
|
|
125829
|
-
...gapImage.graphic ? { image: playableGraphic(gapImage.graphic) } : {},
|
|
125830
|
-
matchMax: gapImage.matchMax,
|
|
125831
|
-
...gapImage.description ? { description: gapImage.description } : {}
|
|
125832
|
-
})),
|
|
125833
|
-
hotspots,
|
|
125834
|
-
maxAssociations: qtiAttributeNumber(interaction.attributes, "max-associations", 0)
|
|
125835
|
-
};
|
|
125836
|
-
}
|
|
125837
|
-
if (type === "select-point") {
|
|
125838
|
-
return {
|
|
125839
|
-
type,
|
|
125840
|
-
responseIdentifier,
|
|
125841
|
-
...prompt,
|
|
125842
|
-
image: requireGraphic(questionIdentifier, interaction.graphic),
|
|
125843
|
-
minChoices: qtiAttributeNumber(interaction.attributes, "min-choices", 0),
|
|
125844
|
-
maxChoices: qtiAttributeNumber(interaction.attributes, "max-choices", 0)
|
|
125845
|
-
};
|
|
125846
|
-
}
|
|
125847
|
-
if (type === "graphic-associate") {
|
|
125848
|
-
return {
|
|
125849
|
-
type,
|
|
125850
|
-
responseIdentifier,
|
|
125851
|
-
...prompt,
|
|
125852
|
-
image: requireGraphic(questionIdentifier, interaction.graphic),
|
|
125853
|
-
hotspots: requireHotspots(questionIdentifier, interaction.hotspots),
|
|
125854
|
-
maxAssociations: qtiAttributeNumber(interaction.attributes, "max-associations", 1)
|
|
125855
|
-
};
|
|
125856
|
-
}
|
|
125857
|
-
if (type === "position-object") {
|
|
125858
|
-
return {
|
|
125859
|
-
type,
|
|
125860
|
-
responseIdentifier,
|
|
125861
|
-
...prompt,
|
|
125862
|
-
stage: requireGraphic(questionIdentifier, interaction.graphic),
|
|
125863
|
-
object: requireGraphic(questionIdentifier, interaction.objectGraphic),
|
|
125864
|
-
maxPlacements: qtiAttributeNumber(interaction.attributes, "max-choices", 1)
|
|
125865
|
-
};
|
|
125866
|
-
}
|
|
125867
|
-
if (type === "hottext") {
|
|
125868
|
-
const segments = interaction.textSegments;
|
|
125869
|
-
if (!segments.some((segment) => segment.selectable) || segments.some((segment) => segment.selectable && (!segment.identifier || !segment.content))) {
|
|
125870
|
-
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", "Hottext interactions require identified selectable content.");
|
|
125871
|
-
}
|
|
125872
|
-
return {
|
|
125873
|
-
type,
|
|
125874
|
-
responseIdentifier,
|
|
125875
|
-
...prompt,
|
|
125876
|
-
segments,
|
|
125877
|
-
maxChoices: qtiAttributeMaximum(interaction.attributes, "max-choices", 1, segments.filter((segment) => segment.selectable).length)
|
|
125878
|
-
};
|
|
125879
|
-
}
|
|
125880
|
-
const unsupported = type;
|
|
125881
|
-
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${questionIdentifier} uses unsupported interaction ${unsupported}.`);
|
|
125882
|
-
}
|
|
125883
|
-
function normalizedQtiItem(question) {
|
|
125884
|
-
if (!question.rawXml) {
|
|
125885
|
-
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${question.identifier} has no authoritative QTI XML.`);
|
|
125886
|
-
}
|
|
125887
|
-
try {
|
|
125888
|
-
return parseQtiItemXml(question.rawXml);
|
|
125889
|
-
} catch (error88) {
|
|
125890
|
-
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${question.identifier} contains invalid QTI XML.`, { reason: errorMessage2(error88) });
|
|
125891
|
-
}
|
|
125892
|
-
}
|
|
125893
|
-
function parsePlayableItem(question) {
|
|
125894
|
-
const parsed = normalizedQtiItem(question);
|
|
125895
|
-
const interactions = playableQtiInteractions(parsed);
|
|
125896
|
-
if (!interactions) {
|
|
125897
|
-
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${question.identifier} must contain only supported interactions with unique response identifiers.`);
|
|
125898
|
-
}
|
|
125899
|
-
return {
|
|
125900
|
-
identifier: question.identifier,
|
|
125901
|
-
title: question.title,
|
|
125902
|
-
prompt: parsed.bodyTextWithBlanks,
|
|
125903
|
-
...parsed.bodyContent ? { promptContent: parsed.bodyContent } : {},
|
|
125904
|
-
maxScore: qtiItemMaxScore(parsed),
|
|
125905
|
-
interactions: interactions.map((interaction) => parseInteraction2(question.identifier, interaction))
|
|
125906
|
-
};
|
|
125907
|
-
}
|
|
125908
|
-
function assessmentContentRevision(test, questions) {
|
|
125909
|
-
return sha256Hex(canonicalJson2({
|
|
125910
|
-
identifier: test.identifier,
|
|
125911
|
-
title: test.title,
|
|
125912
|
-
qtiVersion: test.qtiVersion,
|
|
125913
|
-
parts: test["qti-test-part"],
|
|
125914
|
-
questions: questions.questions.map(({ reference, question }) => ({
|
|
125915
|
-
identifier: reference.identifier,
|
|
125916
|
-
rawXml: question.rawXml
|
|
125917
|
-
}))
|
|
125918
|
-
}));
|
|
125919
|
-
}
|
|
125920
|
-
async function buildPlayableAssessment(test, questions) {
|
|
125921
|
-
return {
|
|
125922
|
-
identifier: test.identifier,
|
|
125923
|
-
contractVersion: PLAYCADEMY_ASSESSMENT_CONTRACT_VERSION,
|
|
125924
|
-
contentRevision: await assessmentContentRevision(test, questions),
|
|
125925
|
-
title: test.title,
|
|
125926
|
-
items: questions.questions.map(({ question }) => parsePlayableItem(question))
|
|
125927
|
-
};
|
|
125928
|
-
}
|
|
125929
|
-
function assessmentFixtureScoringKeys(assessment, questions) {
|
|
125930
|
-
const questionByIdentifier = new Map(questions.questions.map(({ question }) => [question.identifier, question]));
|
|
125931
|
-
const correctResponses = {};
|
|
125932
|
-
const responseAreas = {};
|
|
125933
|
-
for (const item of assessment.items) {
|
|
125934
|
-
const question = questionByIdentifier.get(item.identifier);
|
|
125935
|
-
const normalizedDeclarations = question ? normalizedQtiItem(question).responseDeclarations : [];
|
|
125936
|
-
const itemResponses = {};
|
|
125937
|
-
for (const interaction of item.interactions) {
|
|
125938
|
-
const responseIdentifier = interaction.responseIdentifier;
|
|
125939
|
-
const declaration = question?.responseDeclarations?.find((candidate) => candidate.identifier === responseIdentifier);
|
|
125940
|
-
const declaredValues = declaration?.correctResponse?.value ?? [];
|
|
125941
|
-
const normalizedDeclaration = normalizedDeclarations.find((candidate) => candidate.identifier === responseIdentifier);
|
|
125942
|
-
const values = declaredValues.length > 0 ? declaredValues : normalizedDeclaration?.correctValues ?? [];
|
|
125943
|
-
const areas = interaction.type === "select-point" || interaction.type === "position-object" ? normalizedDeclaration?.areaMapping?.entries.filter((entry2) => entry2.value > 0 && isPlayableAssessmentShape(entry2.shape)) : undefined;
|
|
125944
|
-
if (areas?.length) {
|
|
125945
|
-
(responseAreas[item.identifier] ??= {})[responseIdentifier] = areas.map((entry2) => ({
|
|
125946
|
-
shape: entry2.shape,
|
|
125947
|
-
coords: [...entry2.coords],
|
|
125948
|
-
value: entry2.value
|
|
125949
|
-
}));
|
|
125950
|
-
}
|
|
125951
|
-
if (values.length > 0) {
|
|
125952
|
-
const cardinality = declaration?.cardinality ?? normalizedDeclaration?.cardinality;
|
|
125953
|
-
itemResponses[responseIdentifier] = cardinality === "single" && values.length === 1 ? values[0] : values;
|
|
125954
|
-
} else if (!areas?.length) {
|
|
125955
|
-
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${item.identifier} has no fixture scoring key.`);
|
|
125956
|
-
}
|
|
125957
|
-
}
|
|
125958
|
-
correctResponses[item.identifier] = itemResponses;
|
|
125959
|
-
}
|
|
125960
|
-
return {
|
|
125961
|
-
correctResponses,
|
|
125962
|
-
...Object.keys(responseAreas).length > 0 ? { responseAreas } : {}
|
|
125963
|
-
};
|
|
125964
|
-
}
|
|
125965
|
-
function prepareDiagnosticAssessmentResponses(assessment, current, input) {
|
|
125966
|
-
const update2 = { [input.itemIdentifier]: input.responses };
|
|
125967
|
-
validateAssessmentResponseUpdate(assessment, update2);
|
|
125968
|
-
const responses = applyAssessmentResponseUpdate(current, update2);
|
|
125969
|
-
const item = assessment.items.find((candidate) => candidate.identifier === input.itemIdentifier);
|
|
125970
|
-
const itemResponses = responses[input.itemIdentifier];
|
|
125971
|
-
const missingResponse = item?.interactions.find((interaction) => itemResponses?.[interaction.responseIdentifier] === undefined);
|
|
125972
|
-
if (!item || !itemResponses || missingResponse) {
|
|
125973
|
-
throw new AssessmentRuntimeError("INVALID_RESPONSE", `Diagnostic item ${input.itemIdentifier} requires a response for every interaction.`, {
|
|
125974
|
-
itemIdentifier: input.itemIdentifier,
|
|
125975
|
-
...missingResponse ? { responseIdentifier: missingResponse.responseIdentifier } : {}
|
|
125976
|
-
});
|
|
125977
|
-
}
|
|
125978
|
-
validateAssessmentResponses(assessment, { [input.itemIdentifier]: itemResponses });
|
|
125979
|
-
return responses;
|
|
125980
|
-
}
|
|
125981
|
-
function validateAssessmentResponseUpdate(assessment, update2) {
|
|
125982
|
-
const message = assessmentResponseUpdateValidationMessage(assessment, update2);
|
|
125983
|
-
if (message) {
|
|
125984
|
-
throw new AssessmentRuntimeError("INVALID_RESPONSE", message);
|
|
125985
|
-
}
|
|
125986
|
-
}
|
|
125987
|
-
function validateAssessmentResponses(assessment, responses) {
|
|
125988
|
-
const message = assessmentResponseValidationMessage(assessment, responses);
|
|
125989
|
-
if (message) {
|
|
125990
|
-
throw new AssessmentRuntimeError("INVALID_RESPONSE", message);
|
|
125991
|
-
}
|
|
125992
|
-
}
|
|
125993
|
-
function validateAssessmentDraftResponses(assessment, responses) {
|
|
125994
|
-
const message = assessmentDraftResponseValidationMessage(assessment, responses);
|
|
125995
|
-
if (message) {
|
|
125996
|
-
throw new AssessmentRuntimeError("INVALID_RESPONSE", message);
|
|
125997
|
-
}
|
|
125998
|
-
}
|
|
125999
|
-
function assessmentScoringRequests(assessment, responses) {
|
|
126000
|
-
return assessment.items.map((item) => ({
|
|
126001
|
-
itemIdentifier: item.identifier,
|
|
126002
|
-
maxScore: item.maxScore,
|
|
126003
|
-
requests: item.interactions.map((interaction) => ({
|
|
126004
|
-
identifier: interaction.responseIdentifier,
|
|
126005
|
-
response: responses[item.identifier]?.[interaction.responseIdentifier] ?? ""
|
|
126006
|
-
}))
|
|
126007
|
-
}));
|
|
126008
|
-
}
|
|
126009
|
-
function buildAssessmentResultMetadata(input) {
|
|
126010
|
-
return {
|
|
126011
|
-
[PLAYCADEMY_ASSESSMENT_RESULT_METADATA_KEY]: input.metadata,
|
|
126012
|
-
assessmentPurpose: input.metadata.purpose,
|
|
126013
|
-
...assessmentPurposeClassificationFields2(input.metadata.purpose, input)
|
|
126014
|
-
};
|
|
126015
|
-
}
|
|
126016
|
-
function mergeAssessmentRuntimeMetadata(resultMetadata, metadata2) {
|
|
126017
|
-
return {
|
|
126018
|
-
...resultMetadata,
|
|
126019
|
-
[PLAYCADEMY_ASSESSMENT_RESULT_METADATA_KEY]: metadata2
|
|
126020
|
-
};
|
|
126021
|
-
}
|
|
126022
|
-
function aggregateAssessmentScore(itemScores, possible) {
|
|
126023
|
-
const earned = itemScores.reduce((sum, itemScore) => sum + (Number.isFinite(itemScore) ? itemScore : 0), 0);
|
|
126024
|
-
return {
|
|
126025
|
-
earned,
|
|
126026
|
-
possible,
|
|
126027
|
-
normalized: possible === 0 ? 0 : Math.min(1, Math.max(0, earned / possible))
|
|
126028
|
-
};
|
|
126029
|
-
}
|
|
126030
|
-
function countCorrectQuestions(verdicts) {
|
|
126031
|
-
return verdicts.filter((verdict) => verdict === true).length;
|
|
126032
|
-
}
|
|
126033
|
-
function assessmentScoreFromPercentage(percentage, possible) {
|
|
126034
|
-
const normalized = Math.min(1, Math.max(0, (percentage ?? 0) / 100));
|
|
126035
|
-
const earned = Math.round(normalized * possible * 1e6) / 1e6;
|
|
126036
|
-
return { earned, possible, normalized };
|
|
126037
|
-
}
|
|
126038
|
-
function buildAssessmentResultSubmission(input) {
|
|
126039
|
-
const metadata2 = {
|
|
126040
|
-
...input.metadata,
|
|
126041
|
-
submissionId: input.submissionId,
|
|
126042
|
-
score: input.score,
|
|
126043
|
-
completion: {
|
|
126044
|
-
testName: input.testName,
|
|
126045
|
-
courseName: input.courseName,
|
|
126046
|
-
totalQuestions: input.totalQuestions,
|
|
126047
|
-
correctQuestions: input.correctQuestions,
|
|
126048
|
-
...input.session ? { session: input.session } : {},
|
|
126049
|
-
...input.itemOutcomes ? { itemOutcomes: input.itemOutcomes } : {},
|
|
126050
|
-
...input.diagnosticTracks ? { diagnosticTracks: input.diagnosticTracks } : {}
|
|
126051
|
-
},
|
|
126052
|
-
updatedAt: input.timestamp
|
|
126053
|
-
};
|
|
126054
|
-
return {
|
|
126055
|
-
metadata: metadata2,
|
|
126056
|
-
resultUpdate: {
|
|
126057
|
-
status: input.result.status,
|
|
126058
|
-
assessmentLineItem: input.result.assessmentLineItem,
|
|
126059
|
-
student: input.result.student,
|
|
126060
|
-
score: input.score.normalized * 100,
|
|
126061
|
-
scoreDate: input.timestamp,
|
|
126062
|
-
...ASSESSMENT_ATTEMPT_AWAITING_AWARD,
|
|
126063
|
-
metadata: mergeAssessmentRuntimeMetadata(input.result.metadata, metadata2)
|
|
126064
|
-
}
|
|
126065
|
-
};
|
|
126066
|
-
}
|
|
126067
|
-
function buildAssessmentResultAwardFinalization(input) {
|
|
126068
|
-
const completion = input.metadata.completion;
|
|
126069
|
-
if (!completion || completion.totalQuestions === undefined) {
|
|
126070
|
-
throw new Error("A scored assessment must retain its completion facts before finalization");
|
|
126071
|
-
}
|
|
126072
|
-
const metadata2 = {
|
|
126073
|
-
...input.metadata,
|
|
126074
|
-
finalizedAt: input.timestamp,
|
|
126075
|
-
updatedAt: input.timestamp,
|
|
126076
|
-
award: input.award
|
|
126077
|
-
};
|
|
126078
|
-
const masteryRequested = input.award.masteredUnits !== undefined || input.award.masteredUnitsAbsolute !== undefined;
|
|
126079
|
-
return {
|
|
126080
|
-
metadata: metadata2,
|
|
126081
|
-
resultUpdate: {
|
|
126082
|
-
status: input.result.status,
|
|
126083
|
-
assessmentLineItem: input.result.assessmentLineItem,
|
|
126084
|
-
student: input.result.student,
|
|
126085
|
-
score: input.result.score ?? 0,
|
|
126086
|
-
scoreDate: input.result.scoreDate,
|
|
126087
|
-
...ASSESSMENT_ATTEMPT_COMPLETED,
|
|
126088
|
-
metadata: {
|
|
126089
|
-
...mergeAssessmentRuntimeMetadata(input.result.metadata, metadata2),
|
|
126090
|
-
xp: input.award.xpAwarded,
|
|
126091
|
-
...masteryRequested ? { masteredUnits: input.award.masteredUnitsApplied } : {},
|
|
126092
|
-
totalQuestions: completion.totalQuestions,
|
|
126093
|
-
correctQuestions: completion.correctQuestions,
|
|
126094
|
-
appName: input.appName
|
|
126095
|
-
}
|
|
126096
|
-
}
|
|
126097
|
-
};
|
|
126098
|
-
}
|
|
126099
|
-
var PLAYABLE_SHAPE_SET;
|
|
126100
|
-
var init_timeback_assessment_runtime_util = __esm(() => {
|
|
126101
|
-
init_src();
|
|
126102
|
-
init_assessment_runtime2();
|
|
126103
|
-
init_qti();
|
|
126104
|
-
init_types2();
|
|
126105
|
-
init_utils6();
|
|
126106
|
-
init_timeback4();
|
|
126107
|
-
init_uuid();
|
|
126108
|
-
init_errors2();
|
|
126109
|
-
PLAYABLE_SHAPE_SET = new Set(PLAYABLE_ASSESSMENT_SHAPES);
|
|
126110
|
-
});
|
|
126111
127298
|
function reviewBankTestItemIdentifiers(test) {
|
|
126112
127299
|
return qtiTestParts(test).flatMap((part) => part["qti-assessment-section"].flatMap((section) => (section["qti-assessment-item-ref"] ?? []).map((reference) => reference.identifier)));
|
|
126113
127300
|
}
|
|
@@ -126360,6 +127547,7 @@ async function prepareReviewMappingUpdate(test, questions) {
|
|
|
126360
127547
|
}
|
|
126361
127548
|
};
|
|
126362
127549
|
}
|
|
127550
|
+
var PLAYCADEMY_REVIEW_BANK_TITLE = "Review Bank";
|
|
126363
127551
|
var PLAYCADEMY_REVIEW_BANK_SINK_METADATA_KEY = "playcademyReviewBankSink";
|
|
126364
127552
|
var PLAYCADEMY_REVIEW_BANK_SINK_VERSION = 1;
|
|
126365
127553
|
var StoredSinkWarningSchema;
|
|
@@ -126405,6 +127593,256 @@ var init_timeback_review_mapping_util = __esm(() => {
|
|
|
126405
127593
|
return { ...marker, warnings };
|
|
126406
127594
|
});
|
|
126407
127595
|
});
|
|
127596
|
+
async function storeAndVerifyCompiledAssessmentArtifacts(input) {
|
|
127597
|
+
await input.store(input.compiled);
|
|
127598
|
+
const assessment = await input.loadPlayable(input.compiled.identity);
|
|
127599
|
+
const scoring = assessment ? await input.loadMatchedScoring(assessment, input.compiled.identity) : null;
|
|
127600
|
+
if (!assessment || !scoring) {
|
|
127601
|
+
throw new ServiceUnavailableError(`Prepared ${input.label} could not be verified.`);
|
|
127602
|
+
}
|
|
127603
|
+
return assessment;
|
|
127604
|
+
}
|
|
127605
|
+
function assertSourceIdentity(source) {
|
|
127606
|
+
if (source.test.identifier !== source.questions.assessmentTest) {
|
|
127607
|
+
throw new ValidationError(`QTI questions for ${source.questions.assessmentTest} do not describe ${source.test.identifier}`);
|
|
127608
|
+
}
|
|
127609
|
+
}
|
|
127610
|
+
function sourceItemIdentifiers(source) {
|
|
127611
|
+
return source.questions.questions.map(({ question }) => question.identifier);
|
|
127612
|
+
}
|
|
127613
|
+
function compileScoringArtifact(questions) {
|
|
127614
|
+
try {
|
|
127615
|
+
return compileQtiScoringArtifact(questions);
|
|
127616
|
+
} catch (error88) {
|
|
127617
|
+
if (error88 instanceof QtiScoringArtifactValidationError) {
|
|
127618
|
+
throw new ValidationError(error88.message);
|
|
127619
|
+
}
|
|
127620
|
+
throw error88;
|
|
127621
|
+
}
|
|
127622
|
+
}
|
|
127623
|
+
function qtiFallbackItemCount(compiled) {
|
|
127624
|
+
return compiled.scoringPayload.items.filter((item) => item.strategy === "qti").length;
|
|
127625
|
+
}
|
|
127626
|
+
async function compileManagedAssessmentArtifacts(input) {
|
|
127627
|
+
assertSourceIdentity(input.source);
|
|
127628
|
+
const playablePayload = await buildPlayableAssessment(input.source.test, input.source.questions);
|
|
127629
|
+
const scoring = compileScoringArtifact(input.source.questions);
|
|
127630
|
+
const diagnostic = input.diagnosticRoutingManifest ? {
|
|
127631
|
+
routingRevision: await diagnosticRoutingRevision(input.diagnosticRoutingManifest),
|
|
127632
|
+
routingManifest: input.diagnosticRoutingManifest
|
|
127633
|
+
} : undefined;
|
|
127634
|
+
return {
|
|
127635
|
+
identity: {
|
|
127636
|
+
kind: "assessment",
|
|
127637
|
+
qtiTestIdentifier: input.source.test.identifier,
|
|
127638
|
+
contentRevision: playablePayload.contentRevision
|
|
127639
|
+
},
|
|
127640
|
+
playablePayload,
|
|
127641
|
+
scoringPayload: {
|
|
127642
|
+
...scoring,
|
|
127643
|
+
...diagnostic ? { diagnostic } : {}
|
|
127644
|
+
}
|
|
127645
|
+
};
|
|
127646
|
+
}
|
|
127647
|
+
async function compileReviewBankArtifacts(input) {
|
|
127648
|
+
assertSourceIdentity(input.source);
|
|
127649
|
+
if (input.source.test.identifier !== input.bank.bankIdentifier) {
|
|
127650
|
+
throw new ValidationError(`Review bank ${input.bank.bankIdentifier} does not describe ${input.source.test.identifier}`);
|
|
127651
|
+
}
|
|
127652
|
+
const itemIdentifiers = sourceItemIdentifiers(input.source);
|
|
127653
|
+
const indexedItemIdentifiers = input.bank.items.map((item) => item.itemIdentifier);
|
|
127654
|
+
if (itemIdentifiers.length !== indexedItemIdentifiers.length || itemIdentifiers.some((identifier, index2) => identifier !== indexedItemIdentifiers[index2])) {
|
|
127655
|
+
throw new ValidationError("Review artifact content does not match its revisioned index");
|
|
127656
|
+
}
|
|
127657
|
+
const compiledPlayable = await buildPlayableAssessment({ ...input.source.test, title: PLAYCADEMY_REVIEW_BANK_TITLE }, input.source.questions);
|
|
127658
|
+
const playablePayload = {
|
|
127659
|
+
...compiledPlayable,
|
|
127660
|
+
contentRevision: input.bank.sourceContentRevision
|
|
127661
|
+
};
|
|
127662
|
+
return {
|
|
127663
|
+
identity: {
|
|
127664
|
+
kind: "review-bank",
|
|
127665
|
+
qtiTestIdentifier: input.bank.bankIdentifier,
|
|
127666
|
+
sourceContentRevision: input.bank.sourceContentRevision,
|
|
127667
|
+
bankRevision: input.bank.bankRevision
|
|
127668
|
+
},
|
|
127669
|
+
playablePayload,
|
|
127670
|
+
scoringPayload: {
|
|
127671
|
+
...compileScoringArtifact(input.source.questions),
|
|
127672
|
+
reviewBank: {
|
|
127673
|
+
sourceContentRevision: input.bank.sourceContentRevision,
|
|
127674
|
+
bankRevision: input.bank.bankRevision,
|
|
127675
|
+
items: input.bank.items
|
|
127676
|
+
}
|
|
127677
|
+
}
|
|
127678
|
+
};
|
|
127679
|
+
}
|
|
127680
|
+
var init_timeback_assessment_artifacts_util = __esm(() => {
|
|
127681
|
+
init_assessment_runtime2();
|
|
127682
|
+
init_qti();
|
|
127683
|
+
init_errors2();
|
|
127684
|
+
init_timeback_assessment_runtime_util();
|
|
127685
|
+
init_timeback_review_mapping_util();
|
|
127686
|
+
});
|
|
127687
|
+
|
|
127688
|
+
class AssessmentPreparationFlights {
|
|
127689
|
+
limit;
|
|
127690
|
+
pending = new Map;
|
|
127691
|
+
constructor(limit) {
|
|
127692
|
+
if (!Number.isInteger(limit) || limit <= 0) {
|
|
127693
|
+
throw new Error("Assessment preparation flight limit must be a positive integer");
|
|
127694
|
+
}
|
|
127695
|
+
this.limit = limit;
|
|
127696
|
+
}
|
|
127697
|
+
claim(key, operation) {
|
|
127698
|
+
const existing = this.pending.get(key);
|
|
127699
|
+
if (existing) {
|
|
127700
|
+
return { status: "coalesced", promise: existing };
|
|
127701
|
+
}
|
|
127702
|
+
if (this.pending.size >= this.limit) {
|
|
127703
|
+
return { status: "at_capacity" };
|
|
127704
|
+
}
|
|
127705
|
+
const pending = Promise.resolve().then(operation).finally(() => {
|
|
127706
|
+
if (this.pending.get(key) === pending) {
|
|
127707
|
+
this.pending.delete(key);
|
|
127708
|
+
}
|
|
127709
|
+
});
|
|
127710
|
+
this.pending.set(key, pending);
|
|
127711
|
+
return { status: "started", promise: pending };
|
|
127712
|
+
}
|
|
127713
|
+
}
|
|
127714
|
+
function assessmentPreparationRequestHash(input) {
|
|
127715
|
+
return sha256Hex(canonicalJson2(input));
|
|
127716
|
+
}
|
|
127717
|
+
function assessmentPreparationFlightKey(input) {
|
|
127718
|
+
return canonicalJson2(input);
|
|
127719
|
+
}
|
|
127720
|
+
async function assessmentPreparationReceiptMatches(plan, request) {
|
|
127721
|
+
return plan.gameId === request.gameId && plan.studentId === request.studentId && plan.activityId === request.input.activityId && plan.purpose === request.input.purpose && plan.requestHash === await assessmentPreparationRequestHash(request.input);
|
|
127722
|
+
}
|
|
127723
|
+
function resolveAssessmentPreparationSelection(decision, candidateCourseCount) {
|
|
127724
|
+
if (decision?.kind === "resume") {
|
|
127725
|
+
return {
|
|
127726
|
+
status: "blocked",
|
|
127727
|
+
result: { status: "canonical_required", reason: "resumable_attempt" }
|
|
127728
|
+
};
|
|
127729
|
+
}
|
|
127730
|
+
if (candidateCourseCount !== 1) {
|
|
127731
|
+
return {
|
|
127732
|
+
status: "blocked",
|
|
127733
|
+
result: { status: "unprepared", reason: "prerequisite_unavailable" }
|
|
127734
|
+
};
|
|
127735
|
+
}
|
|
127736
|
+
return decision ? { status: "ready", decision } : { status: "unavailable" };
|
|
127737
|
+
}
|
|
127738
|
+
function preparedAssessmentIntegration(plan, candidates) {
|
|
127739
|
+
if (candidates.rows.length !== 1) {
|
|
127740
|
+
return null;
|
|
127741
|
+
}
|
|
127742
|
+
const integration = candidates.rows[0];
|
|
127743
|
+
const enrollment = candidates.enrollmentByCourse.get(integration.courseId);
|
|
127744
|
+
if (integration.id !== plan.integrationId || integration.courseId !== plan.courseId || enrollment?.id !== plan.enrollmentId) {
|
|
127745
|
+
return null;
|
|
127746
|
+
}
|
|
127747
|
+
return integration;
|
|
127748
|
+
}
|
|
127749
|
+
function preparedAssessmentTestIsLive(plan, tests) {
|
|
127750
|
+
if (plan.purpose === "review" && tests.length !== 1) {
|
|
127751
|
+
return false;
|
|
127752
|
+
}
|
|
127753
|
+
return tests.some((test) => test.assessmentKey === plan.selectedTest.assessmentKey && test.qtiTestIdentifier === plan.selectedTest.identifier && (plan.purpose !== "diagnostic" || test.id === plan.diagnostic.definitionId));
|
|
127754
|
+
}
|
|
127755
|
+
function preparedAssessmentArtifactIdentity(plan) {
|
|
127756
|
+
return plan.purpose === "review" ? {
|
|
127757
|
+
kind: "review-bank",
|
|
127758
|
+
qtiTestIdentifier: plan.selectedTest.identifier,
|
|
127759
|
+
sourceContentRevision: plan.selectedTest.contentRevision,
|
|
127760
|
+
bankRevision: plan.review.bankRevision
|
|
127761
|
+
} : {
|
|
127762
|
+
kind: "assessment",
|
|
127763
|
+
qtiTestIdentifier: plan.selectedTest.identifier,
|
|
127764
|
+
contentRevision: plan.selectedTest.contentRevision
|
|
127765
|
+
};
|
|
127766
|
+
}
|
|
127767
|
+
function preparedAssessmentScoringIsCompatible(plan, scoring) {
|
|
127768
|
+
return Boolean(scoring && (plan.purpose !== "diagnostic" || scoring.diagnostic?.routingRevision === plan.diagnostic.routingRevision));
|
|
127769
|
+
}
|
|
127770
|
+
function preparedReviewBankIndex(plan) {
|
|
127771
|
+
return {
|
|
127772
|
+
bankIdentifier: plan.selectedTest.identifier,
|
|
127773
|
+
sourceContentRevision: plan.selectedTest.contentRevision,
|
|
127774
|
+
bankRevision: plan.review.bankRevision,
|
|
127775
|
+
items: plan.review.selections.map((selection) => ({
|
|
127776
|
+
itemIdentifier: selection.itemIdentifier,
|
|
127777
|
+
standards: [selection.standard]
|
|
127778
|
+
}))
|
|
127779
|
+
};
|
|
127780
|
+
}
|
|
127781
|
+
function nextAssessmentAttemptNumber(attempts) {
|
|
127782
|
+
return Math.max(0, ...[...attempts].map((entry2) => entry2.metadata.attemptNumber)) + 1;
|
|
127783
|
+
}
|
|
127784
|
+
function buildPreparedAssessmentMetadata(plan, timestamp6, testName) {
|
|
127785
|
+
const base = {
|
|
127786
|
+
version: PLAYCADEMY_ASSESSMENT_RESULT_METADATA_VERSION,
|
|
127787
|
+
activityId: plan.activityId,
|
|
127788
|
+
courseId: plan.courseId,
|
|
127789
|
+
integrationId: plan.integrationId,
|
|
127790
|
+
enrollmentId: plan.enrollmentId,
|
|
127791
|
+
selectedTest: plan.selectedTest,
|
|
127792
|
+
attemptNumber: plan.attemptNumber,
|
|
127793
|
+
responseVersion: 0,
|
|
127794
|
+
responses: {},
|
|
127795
|
+
itemSubmissions: [],
|
|
127796
|
+
startedAt: timestamp6,
|
|
127797
|
+
updatedAt: timestamp6
|
|
127798
|
+
};
|
|
127799
|
+
if (plan.purpose === "mastery") {
|
|
127800
|
+
return {
|
|
127801
|
+
...base,
|
|
127802
|
+
purpose: plan.purpose,
|
|
127803
|
+
mastery: {
|
|
127804
|
+
requestFingerprint: masteryRequestFingerprint(plan.mastery),
|
|
127805
|
+
standard: plan.mastery.standard
|
|
127806
|
+
}
|
|
127807
|
+
};
|
|
127808
|
+
}
|
|
127809
|
+
if (plan.purpose === "diagnostic") {
|
|
127810
|
+
return {
|
|
127811
|
+
...base,
|
|
127812
|
+
purpose: plan.purpose,
|
|
127813
|
+
diagnostic: {
|
|
127814
|
+
definitionId: plan.diagnostic.definitionId,
|
|
127815
|
+
diagnosticKey: plan.diagnostic.diagnosticKey,
|
|
127816
|
+
testName,
|
|
127817
|
+
routingRevision: plan.diagnostic.routingRevision,
|
|
127818
|
+
ledger: [],
|
|
127819
|
+
state: plan.diagnostic.initialState
|
|
127820
|
+
}
|
|
127821
|
+
};
|
|
127822
|
+
}
|
|
127823
|
+
if (plan.purpose === "review") {
|
|
127824
|
+
return {
|
|
127825
|
+
...base,
|
|
127826
|
+
purpose: plan.purpose,
|
|
127827
|
+
review: {
|
|
127828
|
+
requestFingerprint: reviewRequestFingerprint({
|
|
127829
|
+
standards: plan.review.standards,
|
|
127830
|
+
candidateItemsPerStandard: plan.review.candidateItemsPerStandard
|
|
127831
|
+
}, DEFAULT_REVIEW_SELECTION_POLICY),
|
|
127832
|
+
bankRevision: plan.review.bankRevision,
|
|
127833
|
+
standards: plan.review.standards,
|
|
127834
|
+
candidateItemsPerStandard: plan.review.candidateItemsPerStandard,
|
|
127835
|
+
selections: plan.review.selections,
|
|
127836
|
+
fulfillment: plan.review.fulfillment
|
|
127837
|
+
}
|
|
127838
|
+
};
|
|
127839
|
+
}
|
|
127840
|
+
return { ...base, purpose: plan.purpose };
|
|
127841
|
+
}
|
|
127842
|
+
var init_timeback_assessment_preparation_util = __esm(() => {
|
|
127843
|
+
init_src();
|
|
127844
|
+
init_assessment_runtime2();
|
|
127845
|
+
});
|
|
126408
127846
|
function assessmentKeyFromManagedQtiIdentifier(identifier) {
|
|
126409
127847
|
const assessmentKey = /^playcademy-test\.(.+)\.[a-f0-9]{64}$/.exec(identifier)?.[1];
|
|
126410
127848
|
return assessmentKey && AssessmentKeySchema.safeParse(assessmentKey).success ? assessmentKey : null;
|
|
@@ -126525,7 +127963,7 @@ async function prepareManagedAssessmentPublication(input) {
|
|
|
126525
127963
|
sourceItemIdentifier: question.identifier,
|
|
126526
127964
|
qtiItemIdentifier,
|
|
126527
127965
|
contentHash,
|
|
126528
|
-
xml: canonicalManagedAssetUrls(rewriteQtiItemIdentity(declarationNeutralQtiXml(question.rawXml), qtiItemIdentifier, title)),
|
|
127966
|
+
xml: canonicalManagedAssetUrls(rewriteQtiItemIdentity(declarationNeutralQtiXml(question.rawXml).trim(), qtiItemIdentifier, title)),
|
|
126529
127967
|
metadata: {
|
|
126530
127968
|
...metadata2,
|
|
126531
127969
|
ownerSystem: PLAYCADEMY_QTI_OWNER_SYSTEM,
|
|
@@ -126614,7 +128052,11 @@ async function prepareManagedAssessmentPublication(input) {
|
|
|
126614
128052
|
qtiTestIdentifier,
|
|
126615
128053
|
testInput,
|
|
126616
128054
|
items,
|
|
126617
|
-
diagnosticRoutingManifest: compiledRouting
|
|
128055
|
+
diagnosticRoutingManifest: compiledRouting,
|
|
128056
|
+
artifactSource: {
|
|
128057
|
+
test: testInput,
|
|
128058
|
+
questions: physicalQuestions
|
|
128059
|
+
}
|
|
126618
128060
|
};
|
|
126619
128061
|
}
|
|
126620
128062
|
function assertManagedAssessmentPublication(test, expected) {
|
|
@@ -126810,6 +128252,7 @@ var QTI_HYDRATION_CONCURRENCY = 8;
|
|
|
126810
128252
|
var init_timeback_qti_hydration_util = __esm(() => {
|
|
126811
128253
|
init_timeback_qti_authoring_util();
|
|
126812
128254
|
});
|
|
128255
|
+
var AssessmentPreparationPrerequisiteError;
|
|
126813
128256
|
var TimebackAssessmentRuntimeService;
|
|
126814
128257
|
var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
126815
128258
|
init_drizzle_orm();
|
|
@@ -126822,17 +128265,30 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
126822
128265
|
init_types2();
|
|
126823
128266
|
init_uuid();
|
|
126824
128267
|
init_errors2();
|
|
128268
|
+
init_assessment_artifact_runtime_util();
|
|
126825
128269
|
init_assessment_runtime_lock_util();
|
|
128270
|
+
init_timeback_assessment_artifacts_util();
|
|
128271
|
+
init_timeback_assessment_preparation_util();
|
|
126826
128272
|
init_timeback_assessment_publication_util();
|
|
126827
128273
|
init_timeback_assessment_rules_util();
|
|
126828
128274
|
init_timeback_assessment_runtime_util();
|
|
126829
128275
|
init_timeback_qti_hydration_util();
|
|
126830
128276
|
await __promiseAll([
|
|
126831
128277
|
init_dist5(),
|
|
126832
|
-
init_errors8()
|
|
128278
|
+
init_errors8(),
|
|
128279
|
+
init_assessment_artifact_loader_util()
|
|
126833
128280
|
]);
|
|
128281
|
+
AssessmentPreparationPrerequisiteError = class AssessmentPreparationPrerequisiteError2 extends Error {
|
|
128282
|
+
original;
|
|
128283
|
+
constructor(original) {
|
|
128284
|
+
super("Assessment preparation prerequisite provisioning failed");
|
|
128285
|
+
this.name = "AssessmentPreparationPrerequisiteError";
|
|
128286
|
+
this.original = original;
|
|
128287
|
+
}
|
|
128288
|
+
};
|
|
126834
128289
|
TimebackAssessmentRuntimeService = class TimebackAssessmentRuntimeService2 {
|
|
126835
128290
|
static ASSESSMENT_CACHE_LIMIT = 32;
|
|
128291
|
+
static PREPARATION_IN_FLIGHT_LIMIT = 32;
|
|
126836
128292
|
static ASSESSMENT_CACHE_TTL_MS = 60000;
|
|
126837
128293
|
static EXPORT_CONCURRENCY = 4;
|
|
126838
128294
|
static ITEM_SUBMISSION_MAX_PREPARATION_ATTEMPTS = 3;
|
|
@@ -126852,16 +128308,65 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
126852
128308
|
defaultTTL: TimebackAssessmentRuntimeService2.ASSESSMENT_CACHE_TTL_MS,
|
|
126853
128309
|
maxSize: TimebackAssessmentRuntimeService2.ASSESSMENT_CACHE_LIMIT
|
|
126854
128310
|
});
|
|
128311
|
+
artifactLoader;
|
|
128312
|
+
preparations = new AssessmentPreparationFlights(TimebackAssessmentRuntimeService2.PREPARATION_IN_FLIGHT_LIMIT);
|
|
126855
128313
|
constructor(deps) {
|
|
126856
128314
|
this.deps = deps;
|
|
128315
|
+
this.artifactLoader = new AssessmentArtifactRuntimeLoader(deps.assessmentArtifacts, {
|
|
128316
|
+
maxSize: TimebackAssessmentRuntimeService2.ASSESSMENT_CACHE_LIMIT
|
|
128317
|
+
});
|
|
128318
|
+
}
|
|
128319
|
+
async prepare(params) {
|
|
128320
|
+
await this.deps.validateDeveloperAccess(params.user, params.gameId);
|
|
128321
|
+
const startedAt = Date.now();
|
|
128322
|
+
const requestHash = await assessmentPreparationRequestHash(params.input);
|
|
128323
|
+
const key = assessmentPreparationFlightKey({
|
|
128324
|
+
gameId: params.gameId,
|
|
128325
|
+
studentId: params.studentId,
|
|
128326
|
+
requestHash
|
|
128327
|
+
});
|
|
128328
|
+
const flight = this.preparations.claim(key, () => this.prepareNewAttempt(params, requestHash).catch((error88) => {
|
|
128329
|
+
if (error88 instanceof ValidationError || error88 instanceof AssessmentRuntimeError) {
|
|
128330
|
+
throw error88;
|
|
128331
|
+
}
|
|
128332
|
+
this.recordPreparation("unprepared", params.input.purpose, startedAt, {
|
|
128333
|
+
reason: "prerequisite_unavailable",
|
|
128334
|
+
error: error88
|
|
128335
|
+
});
|
|
128336
|
+
return {
|
|
128337
|
+
status: "unprepared",
|
|
128338
|
+
reason: "prerequisite_unavailable"
|
|
128339
|
+
};
|
|
128340
|
+
}));
|
|
128341
|
+
if (flight.status === "coalesced") {
|
|
128342
|
+
addEvent("assessment.preparation_coalesced", {
|
|
128343
|
+
"app.assessment.purpose": params.input.purpose
|
|
128344
|
+
});
|
|
128345
|
+
}
|
|
128346
|
+
if (flight.status === "at_capacity") {
|
|
128347
|
+
this.recordPreparation("unprepared", params.input.purpose, Date.now(), {
|
|
128348
|
+
reason: "local_capacity"
|
|
128349
|
+
});
|
|
128350
|
+
return { status: "unprepared", reason: "prerequisite_unavailable" };
|
|
128351
|
+
}
|
|
128352
|
+
return flight.promise;
|
|
126857
128353
|
}
|
|
126858
|
-
async start({
|
|
128354
|
+
async start(params) {
|
|
128355
|
+
await this.deps.validateDeveloperAccess(params.user, params.gameId);
|
|
128356
|
+
if (params.preparationReceipt) {
|
|
128357
|
+
const prepared = await this.tryPreparedStart(params, params.preparationReceipt);
|
|
128358
|
+
if (prepared) {
|
|
128359
|
+
return prepared;
|
|
128360
|
+
}
|
|
128361
|
+
}
|
|
128362
|
+
return this.startCanonical(params);
|
|
128363
|
+
}
|
|
128364
|
+
async startCanonical({
|
|
126859
128365
|
gameId,
|
|
126860
128366
|
studentId,
|
|
126861
128367
|
input,
|
|
126862
128368
|
user
|
|
126863
128369
|
}) {
|
|
126864
|
-
await this.deps.validateDeveloperAccess(user, gameId);
|
|
126865
128370
|
if (input.purpose === "diagnostic") {
|
|
126866
128371
|
return this.startDiagnostic({ gameId, studentId, input, user });
|
|
126867
128372
|
}
|
|
@@ -126970,6 +128475,285 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
126970
128475
|
return this.snapshot(result, metadata2, assessment, context2.integration);
|
|
126971
128476
|
});
|
|
126972
128477
|
}
|
|
128478
|
+
async prepareNewAttempt(params, requestHash) {
|
|
128479
|
+
const startedAt = Date.now();
|
|
128480
|
+
const [candidates, results] = await Promise.all([
|
|
128481
|
+
this.candidateIntegrations(params.gameId, params.studentId, params.input),
|
|
128482
|
+
this.studentAssessmentResults(params.studentId)
|
|
128483
|
+
]);
|
|
128484
|
+
const context2 = await this.selectIntegration(candidates, params.studentId, params.input, results);
|
|
128485
|
+
const attemptScope = {
|
|
128486
|
+
studentId: params.studentId,
|
|
128487
|
+
activityId: params.input.activityId,
|
|
128488
|
+
purpose: params.input.purpose,
|
|
128489
|
+
courseId: context2.integration.courseId,
|
|
128490
|
+
integrationId: context2.integration.id,
|
|
128491
|
+
enrollmentId: context2.enrollment.id
|
|
128492
|
+
};
|
|
128493
|
+
const tests = await this.liveTests(context2.integration.id, params.input.purpose, this.deps.db, params.input.purpose === "mastery" ? params.input.standard : undefined, params.input.purpose === "diagnostic" ? params.input.diagnosticKey : undefined);
|
|
128494
|
+
const listing = this.attemptListing(results, attemptScope);
|
|
128495
|
+
const decision = selectRuntimeAssessment(tests, [
|
|
128496
|
+
...this.attemptsMatchingRequest(listing.attempts, this.startRequestFingerprint(params.input)).values()
|
|
128497
|
+
].map((entry2) => entry2.selection));
|
|
128498
|
+
const selection = resolveAssessmentPreparationSelection(decision, candidates.rows.length);
|
|
128499
|
+
if (selection.status === "blocked") {
|
|
128500
|
+
this.recordPreparation(selection.result.status, params.input.purpose, startedAt, {
|
|
128501
|
+
reason: selection.result.status === "canonical_required" ? selection.result.reason : "multiple_candidate_courses"
|
|
128502
|
+
});
|
|
128503
|
+
return selection.result;
|
|
128504
|
+
}
|
|
128505
|
+
if (selection.status === "unavailable") {
|
|
128506
|
+
throw AssessmentRuntimeError.from(assessmentNoEligibleTests(params.input.purpose));
|
|
128507
|
+
}
|
|
128508
|
+
const preparation = {
|
|
128509
|
+
context: context2,
|
|
128510
|
+
tests,
|
|
128511
|
+
listing,
|
|
128512
|
+
decision: selection.decision
|
|
128513
|
+
};
|
|
128514
|
+
let prepared;
|
|
128515
|
+
try {
|
|
128516
|
+
prepared = await this.buildPreparationPlan(params, requestHash, preparation);
|
|
128517
|
+
} catch (error88) {
|
|
128518
|
+
if (error88 instanceof ValidationError || error88 instanceof AssessmentRuntimeError) {
|
|
128519
|
+
throw error88;
|
|
128520
|
+
}
|
|
128521
|
+
const prerequisiteFailure = error88 instanceof AssessmentPreparationPrerequisiteError;
|
|
128522
|
+
const reason = prerequisiteFailure ? "prerequisite_unavailable" : "artifact_unavailable";
|
|
128523
|
+
this.recordPreparation("unprepared", params.input.purpose, startedAt, {
|
|
128524
|
+
reason,
|
|
128525
|
+
error: prerequisiteFailure ? error88.original : error88
|
|
128526
|
+
});
|
|
128527
|
+
return { status: "unprepared", reason };
|
|
128528
|
+
}
|
|
128529
|
+
const minted = await this.deps.mintAssessmentPreparationReceipt(prepared.plan);
|
|
128530
|
+
this.recordPreparation("prepared", params.input.purpose, startedAt, {
|
|
128531
|
+
artifactSource: prepared.artifactSource
|
|
128532
|
+
});
|
|
128533
|
+
return { status: "prepared", ...minted };
|
|
128534
|
+
}
|
|
128535
|
+
async buildPreparationPlan(params, requestHash, preparation) {
|
|
128536
|
+
if (params.input.purpose === "review") {
|
|
128537
|
+
return this.buildReviewPreparationPlan({ ...params, input: params.input }, requestHash, preparation);
|
|
128538
|
+
}
|
|
128539
|
+
const { context: context2, decision, listing } = preparation;
|
|
128540
|
+
let artifact;
|
|
128541
|
+
let diagnostic;
|
|
128542
|
+
if (params.input.purpose === "diagnostic") {
|
|
128543
|
+
const definition = await this.requireDiagnosticDefinition(decision.test.id, context2.integration.id, this.deps.db, true);
|
|
128544
|
+
artifact = await this.loadOrCompileManagedArtifact(definition.qtiTestIdentifier, definition.diagnosticRoutingManifest);
|
|
128545
|
+
diagnostic = {
|
|
128546
|
+
definition,
|
|
128547
|
+
routing: await this.initializeHostedDiagnostic(definition, artifact.assessment)
|
|
128548
|
+
};
|
|
128549
|
+
} else {
|
|
128550
|
+
artifact = await this.loadOrCompileManagedArtifact(decision.test.qtiTestIdentifier);
|
|
128551
|
+
}
|
|
128552
|
+
const plan = await this.provisionPreparationPrerequisites(() => this.finishManagedPreparationPlan({
|
|
128553
|
+
params,
|
|
128554
|
+
requestHash,
|
|
128555
|
+
context: context2,
|
|
128556
|
+
listing,
|
|
128557
|
+
decision,
|
|
128558
|
+
assessment: artifact.assessment,
|
|
128559
|
+
diagnostic
|
|
128560
|
+
}));
|
|
128561
|
+
return { plan, artifactSource: artifact.source };
|
|
128562
|
+
}
|
|
128563
|
+
async provisionPreparationPrerequisites(operation) {
|
|
128564
|
+
try {
|
|
128565
|
+
return await operation();
|
|
128566
|
+
} catch (error88) {
|
|
128567
|
+
throw new AssessmentPreparationPrerequisiteError(error88);
|
|
128568
|
+
}
|
|
128569
|
+
}
|
|
128570
|
+
async finishManagedPreparationPlan(input) {
|
|
128571
|
+
const { params, context: context2, listing, decision, assessment, diagnostic } = input;
|
|
128572
|
+
const diagnosticIdentity = diagnostic && {
|
|
128573
|
+
definitionId: diagnostic.definition.id,
|
|
128574
|
+
diagnosticKey: diagnostic.definition.diagnosticKey,
|
|
128575
|
+
routingRevision: diagnostic.routing.routingRevision
|
|
128576
|
+
};
|
|
128577
|
+
const classId = await this.resolveClassId(context2.integration.courseId, context2.enrollment, params.studentId);
|
|
128578
|
+
const parentLineItemId = await this.ensureLineItem({
|
|
128579
|
+
classId,
|
|
128580
|
+
integration: context2.integration,
|
|
128581
|
+
purpose: params.input.purpose,
|
|
128582
|
+
assessment,
|
|
128583
|
+
...diagnosticIdentity ? { diagnostic: diagnosticIdentity } : {}
|
|
128584
|
+
});
|
|
128585
|
+
const attempt = await this.claimAttemptId({
|
|
128586
|
+
gameId: params.gameId,
|
|
128587
|
+
studentId: params.studentId,
|
|
128588
|
+
courseId: context2.integration.courseId,
|
|
128589
|
+
activityId: params.input.activityId,
|
|
128590
|
+
purpose: params.input.purpose
|
|
128591
|
+
}, nextAssessmentAttemptNumber(listing.attempts.values()));
|
|
128592
|
+
const common2 = {
|
|
128593
|
+
version: 1,
|
|
128594
|
+
gameId: params.gameId,
|
|
128595
|
+
studentId: params.studentId,
|
|
128596
|
+
activityId: params.input.activityId,
|
|
128597
|
+
requestHash: input.requestHash,
|
|
128598
|
+
preparedAt: Date.now(),
|
|
128599
|
+
integrationId: context2.integration.id,
|
|
128600
|
+
enrollmentId: context2.enrollment.id,
|
|
128601
|
+
courseId: context2.integration.courseId,
|
|
128602
|
+
...attempt,
|
|
128603
|
+
parentLineItemId,
|
|
128604
|
+
selectedTest: {
|
|
128605
|
+
assessmentKey: decision.test.assessmentKey,
|
|
128606
|
+
identifier: assessment.identifier,
|
|
128607
|
+
contentRevision: assessment.contentRevision
|
|
128608
|
+
}
|
|
128609
|
+
};
|
|
128610
|
+
if (params.input.purpose === "mastery") {
|
|
128611
|
+
return {
|
|
128612
|
+
...common2,
|
|
128613
|
+
purpose: params.input.purpose,
|
|
128614
|
+
mastery: { standard: normalizeMasteryStandard(params.input.standard) }
|
|
128615
|
+
};
|
|
128616
|
+
}
|
|
128617
|
+
if (params.input.purpose === "diagnostic") {
|
|
128618
|
+
if (!diagnostic || !diagnosticIdentity) {
|
|
128619
|
+
throw new Error("Diagnostic preparation is missing its routing plan");
|
|
128620
|
+
}
|
|
128621
|
+
return {
|
|
128622
|
+
...common2,
|
|
128623
|
+
purpose: params.input.purpose,
|
|
128624
|
+
diagnostic: { ...diagnosticIdentity, initialState: diagnostic.routing.state }
|
|
128625
|
+
};
|
|
128626
|
+
}
|
|
128627
|
+
if (params.input.purpose !== "end_of_course") {
|
|
128628
|
+
throw new Error(`Unsupported prepared assessment purpose ${params.input.purpose}`);
|
|
128629
|
+
}
|
|
128630
|
+
return { ...common2, purpose: "end_of_course" };
|
|
128631
|
+
}
|
|
128632
|
+
async buildReviewPreparationPlan(params, requestHash, preparation) {
|
|
128633
|
+
const { context: context2, decision, listing, tests } = preparation;
|
|
128634
|
+
if (tests.length !== 1) {
|
|
128635
|
+
throw new ValidationError("Standards review requires exactly one live review-bank assessment.");
|
|
128636
|
+
}
|
|
128637
|
+
const normalized = normalizeReviewRequest(params.input);
|
|
128638
|
+
const catalog = await this.loadReviewBankCatalog(decision.test.qtiTestIdentifier);
|
|
128639
|
+
const selected = selectReviewItems({
|
|
128640
|
+
request: params.input,
|
|
128641
|
+
bank: catalog.bank,
|
|
128642
|
+
exposures: this.reviewExposures(listing.reviewChildren.values(), {
|
|
128643
|
+
bankRevision: catalog.bank.bankRevision
|
|
128644
|
+
})
|
|
128645
|
+
});
|
|
128646
|
+
if (selected.selections.length === 0) {
|
|
128647
|
+
if (selected.fulfillment.status !== "partial") {
|
|
128648
|
+
throw new Error("A complete review selection cannot be empty");
|
|
128649
|
+
}
|
|
128650
|
+
throw AssessmentRuntimeError.from(assessmentReviewBankShortage(selected.fulfillment));
|
|
128651
|
+
}
|
|
128652
|
+
const artifact = await this.loadOrCompileReviewBankArtifact(catalog);
|
|
128653
|
+
const assessment = projectReviewAssessment(artifact.assessment, catalog.bank, selected.selections);
|
|
128654
|
+
const source = { assessment: artifact.assessment, bank: catalog.bank };
|
|
128655
|
+
const parentLineItemId = await this.provisionPreparationPrerequisites(() => this.ensureReviewBankLineItem({
|
|
128656
|
+
integration: context2.integration,
|
|
128657
|
+
activityId: params.input.activityId,
|
|
128658
|
+
source
|
|
128659
|
+
}));
|
|
128660
|
+
await this.provisionPreparationPrerequisites(() => runWithConcurrency(assessment.items, TimebackAssessmentRuntimeService2.SCORING_CONCURRENCY, (item) => this.ensureReviewQuestionLineItem({
|
|
128661
|
+
parentLineItemId,
|
|
128662
|
+
integration: context2.integration,
|
|
128663
|
+
bank: catalog.bank,
|
|
128664
|
+
item
|
|
128665
|
+
})));
|
|
128666
|
+
const attempt = await this.provisionPreparationPrerequisites(() => this.claimAttemptId({
|
|
128667
|
+
gameId: params.gameId,
|
|
128668
|
+
studentId: params.studentId,
|
|
128669
|
+
courseId: context2.integration.courseId,
|
|
128670
|
+
activityId: params.input.activityId,
|
|
128671
|
+
purpose: params.input.purpose
|
|
128672
|
+
}, nextAssessmentAttemptNumber(listing.attempts.values())));
|
|
128673
|
+
const plan = {
|
|
128674
|
+
version: 1,
|
|
128675
|
+
gameId: params.gameId,
|
|
128676
|
+
studentId: params.studentId,
|
|
128677
|
+
activityId: params.input.activityId,
|
|
128678
|
+
purpose: params.input.purpose,
|
|
128679
|
+
requestHash,
|
|
128680
|
+
preparedAt: Date.now(),
|
|
128681
|
+
integrationId: context2.integration.id,
|
|
128682
|
+
enrollmentId: context2.enrollment.id,
|
|
128683
|
+
courseId: context2.integration.courseId,
|
|
128684
|
+
...attempt,
|
|
128685
|
+
parentLineItemId,
|
|
128686
|
+
selectedTest: {
|
|
128687
|
+
assessmentKey: decision.test.assessmentKey,
|
|
128688
|
+
identifier: artifact.assessment.identifier,
|
|
128689
|
+
contentRevision: artifact.assessment.contentRevision
|
|
128690
|
+
},
|
|
128691
|
+
review: {
|
|
128692
|
+
bankRevision: catalog.bank.bankRevision,
|
|
128693
|
+
standards: [...normalized.standards],
|
|
128694
|
+
candidateItemsPerStandard: normalized.candidateItemsPerStandard,
|
|
128695
|
+
selections: [...selected.selections],
|
|
128696
|
+
fulfillment: selected.fulfillment
|
|
128697
|
+
}
|
|
128698
|
+
};
|
|
128699
|
+
return { plan, artifactSource: artifact.source };
|
|
128700
|
+
}
|
|
128701
|
+
async tryPreparedStart(params, receipt) {
|
|
128702
|
+
const startedAt = Date.now();
|
|
128703
|
+
let plan;
|
|
128704
|
+
try {
|
|
128705
|
+
const verified = await this.deps.verifyAssessmentPreparationReceipt(receipt);
|
|
128706
|
+
if (!verified || !await assessmentPreparationReceiptMatches(verified, params)) {
|
|
128707
|
+
this.recordPreparedStart("fallback", params.input.purpose, startedAt, {
|
|
128708
|
+
reason: "receipt_mismatch"
|
|
128709
|
+
});
|
|
128710
|
+
return null;
|
|
128711
|
+
}
|
|
128712
|
+
plan = verified;
|
|
128713
|
+
const assessment = await this.loadPreparedPlanAssessment(verified);
|
|
128714
|
+
if (!assessment) {
|
|
128715
|
+
this.recordPreparedStart("fallback", params.input.purpose, startedAt, {
|
|
128716
|
+
reason: "artifact_unavailable",
|
|
128717
|
+
plan
|
|
128718
|
+
});
|
|
128719
|
+
return null;
|
|
128720
|
+
}
|
|
128721
|
+
const outcome = await this.deps.assessmentRuntimeLock(this.deps.db, [
|
|
128722
|
+
assessmentRuntimeStartLockKey({
|
|
128723
|
+
studentId: params.studentId,
|
|
128724
|
+
activityId: params.input.activityId,
|
|
128725
|
+
purpose: params.input.purpose,
|
|
128726
|
+
courseId: verified.courseId
|
|
128727
|
+
})
|
|
128728
|
+
], async (db2) => {
|
|
128729
|
+
const candidates = await this.candidateIntegrations(verified.gameId, verified.studentId, params.input, db2);
|
|
128730
|
+
const integration = preparedAssessmentIntegration(verified, candidates);
|
|
128731
|
+
if (!integration || !await this.preparedPlanTestIsLive(verified, db2)) {
|
|
128732
|
+
return null;
|
|
128733
|
+
}
|
|
128734
|
+
if (await this.attemptIdOccupied(verified.attemptId)) {
|
|
128735
|
+
return null;
|
|
128736
|
+
}
|
|
128737
|
+
return this.createPreparedAttempt(verified, assessment, integration);
|
|
128738
|
+
});
|
|
128739
|
+
if (!outcome) {
|
|
128740
|
+
this.recordPreparedStart("fallback", params.input.purpose, startedAt, {
|
|
128741
|
+
reason: "stale_plan",
|
|
128742
|
+
plan
|
|
128743
|
+
});
|
|
128744
|
+
return null;
|
|
128745
|
+
}
|
|
128746
|
+
this.recordPreparedStart("started", params.input.purpose, startedAt, { plan });
|
|
128747
|
+
return outcome;
|
|
128748
|
+
} catch (error88) {
|
|
128749
|
+
this.recordPreparedStart("fallback", params.input.purpose, startedAt, {
|
|
128750
|
+
reason: "prepared_start_failed",
|
|
128751
|
+
error: error88,
|
|
128752
|
+
...plan ? { plan } : {}
|
|
128753
|
+
});
|
|
128754
|
+
return null;
|
|
128755
|
+
}
|
|
128756
|
+
}
|
|
126973
128757
|
async startDiagnostic({
|
|
126974
128758
|
gameId,
|
|
126975
128759
|
studentId,
|
|
@@ -127624,7 +129408,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
127624
129408
|
let scoring = null;
|
|
127625
129409
|
if (loaded.state.status === "in-progress" && expected?.nodeKey === input.routingNodeKey && expected.itemIdentifier === input.itemIdentifier && attempt.metadata.responseVersion === input.expectedResponseVersion) {
|
|
127626
129410
|
const responses = prepareDiagnosticAssessmentResponses(assessment, attempt.metadata.responses, input);
|
|
127627
|
-
scoring = await this.scoreItem(assessment, responses, input.itemIdentifier);
|
|
129411
|
+
scoring = await this.scoreItem(assessment, responses, input.itemIdentifier, assessmentArtifactIdentityFromAttempt(attempt.metadata));
|
|
127628
129412
|
}
|
|
127629
129413
|
return {
|
|
127630
129414
|
responseVersion: attempt.metadata.responseVersion,
|
|
@@ -127635,7 +129419,8 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
127635
129419
|
scoring
|
|
127636
129420
|
};
|
|
127637
129421
|
}
|
|
127638
|
-
async scoreItems(assessment, responses, itemIdentifiers) {
|
|
129422
|
+
async scoreItems(assessment, responses, artifactIdentity, itemIdentifiers) {
|
|
129423
|
+
await this.artifactLoader.loadMatchedScoring(assessment, artifactIdentity);
|
|
127639
129424
|
const client2 = this.requireClient();
|
|
127640
129425
|
const scoringItems = assessmentScoringRequests(assessment, responses).filter((item) => !itemIdentifiers || itemIdentifiers.has(item.itemIdentifier));
|
|
127641
129426
|
const scoringTasks = scoringItems.flatMap((item, itemIndex) => item.requests.map((request) => ({
|
|
@@ -127665,8 +129450,8 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
127665
129450
|
};
|
|
127666
129451
|
});
|
|
127667
129452
|
}
|
|
127668
|
-
async scoreItem(assessment, responses, itemIdentifier) {
|
|
127669
|
-
const [scoring] = await this.scoreItems(assessment, responses, new Set([itemIdentifier]));
|
|
129453
|
+
async scoreItem(assessment, responses, itemIdentifier, artifactIdentity) {
|
|
129454
|
+
const [scoring] = await this.scoreItems(assessment, responses, artifactIdentity, new Set([itemIdentifier]));
|
|
127670
129455
|
return scoring;
|
|
127671
129456
|
}
|
|
127672
129457
|
async prepareItemSubmission(params, input) {
|
|
@@ -127694,7 +129479,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
127694
129479
|
itemSubmissions: attempt.metadata.itemSubmissions,
|
|
127695
129480
|
assessment
|
|
127696
129481
|
}, input);
|
|
127697
|
-
const scoring = transition.action === "commit" ? await this.scoreItem(assessment, transition.responses, input.itemIdentifier) : null;
|
|
129482
|
+
const scoring = transition.action === "commit" ? await this.scoreItem(assessment, transition.responses, input.itemIdentifier, assessmentArtifactIdentityFromAttempt(attempt.metadata)) : null;
|
|
127698
129483
|
return {
|
|
127699
129484
|
responseVersion: attempt.metadata.responseVersion,
|
|
127700
129485
|
submissionId: input.submissionId,
|
|
@@ -127707,7 +129492,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
127707
129492
|
const assessment = await this.loadAttemptAssessment(attempt.metadata);
|
|
127708
129493
|
validateAssessmentResponses(assessment, attempt.metadata.responses);
|
|
127709
129494
|
const administeredItems = attempt.metadata.purpose === "review" ? administeredItemIdentifiers(attempt.metadata.itemSubmissions) : undefined;
|
|
127710
|
-
const itemResults = await this.scoreItems(assessment, attempt.metadata.responses, administeredItems);
|
|
129495
|
+
const itemResults = await this.scoreItems(assessment, attempt.metadata.responses, assessmentArtifactIdentityFromAttempt(attempt.metadata), administeredItems);
|
|
127711
129496
|
return {
|
|
127712
129497
|
responseVersion: attempt.metadata.responseVersion,
|
|
127713
129498
|
assessment,
|
|
@@ -128199,23 +129984,24 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
128199
129984
|
liveTestIntegrationIds: integrationIdsWithLiveTests
|
|
128200
129985
|
};
|
|
128201
129986
|
}
|
|
128202
|
-
async selectIntegration(candidates, studentId, input) {
|
|
129987
|
+
async selectIntegration(candidates, studentId, input, results) {
|
|
128203
129988
|
const {
|
|
128204
129989
|
rows: candidateRows,
|
|
128205
129990
|
enrollmentByCourse,
|
|
128206
129991
|
liveTestIntegrationIds: integrationIdsWithLiveTests
|
|
128207
129992
|
} = candidates;
|
|
128208
129993
|
const needsOpenAnnotation = candidateRows.length > 1 || candidateRows.some((row) => !integrationIdsWithLiveTests.has(row.id));
|
|
128209
|
-
const
|
|
129994
|
+
const openAttemptScope = {
|
|
128210
129995
|
studentId,
|
|
128211
129996
|
activityId: input.activityId,
|
|
128212
129997
|
purpose: input.purpose,
|
|
128213
|
-
enrollmentIdByCourse: new Map(candidateRows.map((row) => [
|
|
128214
|
-
row.courseId,
|
|
128215
|
-
enrollmentByCourse.get(row.courseId).id
|
|
128216
|
-
])),
|
|
129998
|
+
enrollmentIdByCourse: new Map(candidateRows.map((row) => [row.courseId, enrollmentByCourse.get(row.courseId).id])),
|
|
128217
129999
|
requestFingerprint: this.startRequestFingerprint(input)
|
|
128218
|
-
}
|
|
130000
|
+
};
|
|
130001
|
+
let openAttemptCourseIds = new Set;
|
|
130002
|
+
if (needsOpenAnnotation) {
|
|
130003
|
+
openAttemptCourseIds = results ? this.openAttemptCourseIdsFromResults(results, openAttemptScope) : await this.openAttemptCourseIds(openAttemptScope);
|
|
130004
|
+
}
|
|
128219
130005
|
const annotated = candidateRows.map((row) => ({
|
|
128220
130006
|
...row,
|
|
128221
130007
|
hasLiveTests: integrationIdsWithLiveTests.has(row.id),
|
|
@@ -128249,14 +130035,24 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
128249
130035
|
})).filter((test) => !matchesMasteryStandard || matchesMasteryStandard(test.standard));
|
|
128250
130036
|
}
|
|
128251
130037
|
async openAttemptCourseIds(scope) {
|
|
128252
|
-
|
|
130038
|
+
return this.openAttemptCourseIdsFromResults(await this.studentAssessmentResults(scope.studentId), scope);
|
|
130039
|
+
}
|
|
130040
|
+
async studentAssessmentResults(studentId) {
|
|
130041
|
+
const stream = this.requireClient().api.oneroster.assessmentResults.stream({
|
|
128253
130042
|
where: {
|
|
128254
|
-
"student.sourcedId":
|
|
130043
|
+
"student.sourcedId": studentId,
|
|
128255
130044
|
status: ONEROSTER_STATUS2.active
|
|
128256
130045
|
}
|
|
128257
130046
|
});
|
|
130047
|
+
const results = [];
|
|
130048
|
+
for await (const result of stream) {
|
|
130049
|
+
results.push(result);
|
|
130050
|
+
}
|
|
130051
|
+
return results;
|
|
130052
|
+
}
|
|
130053
|
+
openAttemptCourseIdsFromResults(results, scope) {
|
|
128258
130054
|
const courseIds = new Set;
|
|
128259
|
-
for
|
|
130055
|
+
for (const result of results) {
|
|
128260
130056
|
const metadata2 = playcademyAssessmentResultMetadata(result.metadata);
|
|
128261
130057
|
if (metadata2 && metadata2.activityId === scope.activityId && metadata2.purpose === scope.purpose && this.attemptMatchesRequestFingerprint(metadata2, scope.requestFingerprint) && result.student.sourcedId === scope.studentId && metadata2.enrollmentId === scope.enrollmentIdByCourse.get(metadata2.courseId) && (this.isOpen(result) || this.isAwaitingAward(result))) {
|
|
128262
130058
|
courseIds.add(metadata2.courseId);
|
|
@@ -128265,15 +130061,12 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
128265
130061
|
return courseIds;
|
|
128266
130062
|
}
|
|
128267
130063
|
async listAttempts(scope) {
|
|
128268
|
-
|
|
128269
|
-
|
|
128270
|
-
|
|
128271
|
-
status: ONEROSTER_STATUS2.active
|
|
128272
|
-
}
|
|
128273
|
-
});
|
|
130064
|
+
return this.attemptListing(await this.studentAssessmentResults(scope.studentId), scope);
|
|
130065
|
+
}
|
|
130066
|
+
attemptListing(results, scope) {
|
|
128274
130067
|
const attempts = new Map;
|
|
128275
130068
|
const reviewChildren = new Map;
|
|
128276
|
-
for
|
|
130069
|
+
for (const result of results) {
|
|
128277
130070
|
const metadata2 = playcademyAssessmentResultMetadata(result.metadata);
|
|
128278
130071
|
const childMetadata = scope.purpose === "review" ? playcademyAssessmentItemResultMetadata(result.metadata) : null;
|
|
128279
130072
|
if (childMetadata && childMetadata.activityId === scope.activityId && childMetadata.integrationId === scope.integrationId && childMetadata.enrollmentId === scope.enrollmentId && result.student.sourcedId === scope.studentId) {
|
|
@@ -128485,6 +130278,57 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
128485
130278
|
...counts.selectedCount === undefined ? {} : { "app.assessment.selected_count": counts.selectedCount }
|
|
128486
130279
|
});
|
|
128487
130280
|
}
|
|
130281
|
+
async loadOrCompileManagedArtifact(identifier, diagnosticManifest) {
|
|
130282
|
+
const playable = await this.artifactLoader.loadManagedPlayable(identifier);
|
|
130283
|
+
if (playable) {
|
|
130284
|
+
const identity = {
|
|
130285
|
+
kind: "assessment",
|
|
130286
|
+
qtiTestIdentifier: identifier,
|
|
130287
|
+
contentRevision: playable.contentRevision
|
|
130288
|
+
};
|
|
130289
|
+
const scoring = await this.artifactLoader.loadMatchedScoring(playable, identity);
|
|
130290
|
+
const expectedRoutingRevision = diagnosticManifest ? await diagnosticRoutingRevision(diagnosticManifest) : undefined;
|
|
130291
|
+
if (scoring && (expectedRoutingRevision === undefined || scoring.diagnostic?.routingRevision === expectedRoutingRevision)) {
|
|
130292
|
+
return { assessment: playable, source: "existing" };
|
|
130293
|
+
}
|
|
130294
|
+
}
|
|
130295
|
+
const source = await loadHydratedQtiTest(this.requireClient(), identifier);
|
|
130296
|
+
const compiled = await compileManagedAssessmentArtifacts({
|
|
130297
|
+
source,
|
|
130298
|
+
diagnosticRoutingManifest: diagnosticManifest
|
|
130299
|
+
});
|
|
130300
|
+
return this.storeCompiledArtifact(compiled, `assessment artifacts for ${identifier}`);
|
|
130301
|
+
}
|
|
130302
|
+
async loadOrCompileReviewBankArtifact(catalog) {
|
|
130303
|
+
const identity = {
|
|
130304
|
+
kind: "review-bank",
|
|
130305
|
+
qtiTestIdentifier: catalog.bank.bankIdentifier,
|
|
130306
|
+
sourceContentRevision: catalog.bank.sourceContentRevision,
|
|
130307
|
+
bankRevision: catalog.bank.bankRevision
|
|
130308
|
+
};
|
|
130309
|
+
const playable = await this.artifactLoader.loadPlayable(identity);
|
|
130310
|
+
const scoring = playable ? await this.artifactLoader.loadMatchedScoring(playable, identity) : null;
|
|
130311
|
+
if (playable && scoring) {
|
|
130312
|
+
return { assessment: playable, source: "existing" };
|
|
130313
|
+
}
|
|
130314
|
+
const itemIdentifiers = catalog.bank.items.map((item) => item.itemIdentifier);
|
|
130315
|
+
const questions = await hydrateQtiTestQuestionSelection(this.requireClient(), catalog.references, itemIdentifiers);
|
|
130316
|
+
const compiled = await compileReviewBankArtifacts({
|
|
130317
|
+
source: { test: catalog.test, questions },
|
|
130318
|
+
bank: catalog.bank
|
|
130319
|
+
});
|
|
130320
|
+
return this.storeCompiledArtifact(compiled, `review-bank artifacts for ${catalog.bank.bankIdentifier}`);
|
|
130321
|
+
}
|
|
130322
|
+
async storeCompiledArtifact(compiled, label) {
|
|
130323
|
+
const assessment = await storeAndVerifyCompiledAssessmentArtifacts({
|
|
130324
|
+
compiled,
|
|
130325
|
+
label,
|
|
130326
|
+
store: (pair) => this.deps.assessmentArtifacts.storePair(pair),
|
|
130327
|
+
loadPlayable: (identity) => this.artifactLoader.loadPlayable(identity),
|
|
130328
|
+
loadMatchedScoring: (playable, identity) => this.artifactLoader.loadMatchedScoring(playable, identity)
|
|
130329
|
+
});
|
|
130330
|
+
return { assessment, source: "compiled" };
|
|
130331
|
+
}
|
|
128488
130332
|
async loadReviewBankCatalog(identifier, expectedSourceRevision) {
|
|
128489
130333
|
if (expectedSourceRevision) {
|
|
128490
130334
|
const cached3 = this.reviewBankCache.get(`${identifier}\x00${expectedSourceRevision}`);
|
|
@@ -128537,6 +130381,16 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
128537
130381
|
return assessment;
|
|
128538
130382
|
}
|
|
128539
130383
|
async hydrateReviewBankSelection(catalog, itemIdentifiers) {
|
|
130384
|
+
const identity = {
|
|
130385
|
+
kind: "review-bank",
|
|
130386
|
+
qtiTestIdentifier: catalog.bank.bankIdentifier,
|
|
130387
|
+
sourceContentRevision: catalog.bank.sourceContentRevision,
|
|
130388
|
+
bankRevision: catalog.bank.bankRevision
|
|
130389
|
+
};
|
|
130390
|
+
const artifact = await this.artifactLoader.loadPlayableContainingItems(identity, itemIdentifiers);
|
|
130391
|
+
if (artifact) {
|
|
130392
|
+
return { assessment: artifact, bank: catalog.bank };
|
|
130393
|
+
}
|
|
128540
130394
|
const assessment = await this.cachedReviewSelection({
|
|
128541
130395
|
identifier: catalog.test.identifier,
|
|
128542
130396
|
contentRevision: catalog.bank.sourceContentRevision,
|
|
@@ -128550,6 +130404,14 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
128550
130404
|
}
|
|
128551
130405
|
async loadPinnedReviewBankSource(metadata2) {
|
|
128552
130406
|
const itemIdentifiers = metadata2.review.selections.map((selection) => selection.itemIdentifier);
|
|
130407
|
+
const identity = assessmentArtifactIdentityFromAttempt(metadata2);
|
|
130408
|
+
const artifact = await this.artifactLoader.loadPlayableContainingItems(identity, itemIdentifiers);
|
|
130409
|
+
if (artifact) {
|
|
130410
|
+
return {
|
|
130411
|
+
assessment: artifact,
|
|
130412
|
+
bank: pinnedReviewBankFromAttempt(metadata2, artifact.identifier)
|
|
130413
|
+
};
|
|
130414
|
+
}
|
|
128553
130415
|
let assessment;
|
|
128554
130416
|
try {
|
|
128555
130417
|
assessment = await this.cachedReviewSelection({
|
|
@@ -128575,18 +130437,19 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
128575
130437
|
}
|
|
128576
130438
|
return {
|
|
128577
130439
|
assessment,
|
|
128578
|
-
bank:
|
|
128579
|
-
bankIdentifier: assessment.identifier,
|
|
128580
|
-
sourceContentRevision: metadata2.selectedTest.contentRevision,
|
|
128581
|
-
bankRevision: metadata2.review.bankRevision,
|
|
128582
|
-
items: metadata2.review.selections.map((selection) => ({
|
|
128583
|
-
itemIdentifier: selection.itemIdentifier,
|
|
128584
|
-
standards: [selection.standard]
|
|
128585
|
-
}))
|
|
128586
|
-
}
|
|
130440
|
+
bank: pinnedReviewBankFromAttempt(metadata2, assessment.identifier)
|
|
128587
130441
|
};
|
|
128588
130442
|
}
|
|
128589
130443
|
async loadAssessment(identifier, expectedRevision) {
|
|
130444
|
+
const artifact = expectedRevision ? await this.artifactLoader.loadPlayable({
|
|
130445
|
+
kind: "assessment",
|
|
130446
|
+
qtiTestIdentifier: identifier,
|
|
130447
|
+
contentRevision: expectedRevision
|
|
130448
|
+
}) : await this.artifactLoader.loadManagedPlayable(identifier);
|
|
130449
|
+
if (artifact) {
|
|
130450
|
+
return artifact;
|
|
130451
|
+
}
|
|
130452
|
+
this.artifactLoader.recordFallback("playable", "assessment");
|
|
128590
130453
|
if (expectedRevision) {
|
|
128591
130454
|
const cached3 = this.assessmentCache.get(`${identifier}\x00${expectedRevision}`);
|
|
128592
130455
|
if (cached3) {
|
|
@@ -129354,6 +131217,72 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
129354
131217
|
scoreFromResult(result, possible, metadata2) {
|
|
129355
131218
|
return metadata2.score ?? assessmentScoreFromPercentage(result.score, possible);
|
|
129356
131219
|
}
|
|
131220
|
+
recordPreparation(outcome, purpose, startedAt, details = {}) {
|
|
131221
|
+
addEvent("assessment.preparation", {
|
|
131222
|
+
"app.assessment.purpose": purpose,
|
|
131223
|
+
"app.assessment.preparation_outcome": outcome,
|
|
131224
|
+
"app.assessment.duration_ms": Date.now() - startedAt,
|
|
131225
|
+
...details.reason ? { "app.assessment.reason": details.reason } : {},
|
|
131226
|
+
...details.artifactSource ? { "app.assessment.artifact_source": details.artifactSource } : {},
|
|
131227
|
+
...details.error ? {
|
|
131228
|
+
"exception.type": errorType(details.error),
|
|
131229
|
+
"app.error.message": errorMessage2(details.error)
|
|
131230
|
+
} : {}
|
|
131231
|
+
});
|
|
131232
|
+
}
|
|
131233
|
+
recordPreparedStart(outcome, purpose, startedAt, details = {}) {
|
|
131234
|
+
addEvent("assessment.prepared_start", {
|
|
131235
|
+
"app.assessment.purpose": purpose,
|
|
131236
|
+
"app.assessment.prepared_start_outcome": outcome,
|
|
131237
|
+
"app.assessment.duration_ms": Date.now() - startedAt,
|
|
131238
|
+
...details.reason ? { "app.assessment.reason": details.reason } : {},
|
|
131239
|
+
...details.plan ? {
|
|
131240
|
+
"app.assessment.preparation_age_ms": Math.max(0, Date.now() - details.plan.preparedAt)
|
|
131241
|
+
} : {},
|
|
131242
|
+
...details.error ? {
|
|
131243
|
+
"exception.type": errorType(details.error),
|
|
131244
|
+
"app.error.message": errorMessage2(details.error)
|
|
131245
|
+
} : {}
|
|
131246
|
+
});
|
|
131247
|
+
}
|
|
131248
|
+
async preparedPlanTestIsLive(plan, db2) {
|
|
131249
|
+
const tests = await this.liveTests(plan.integrationId, plan.purpose, db2, plan.purpose === "mastery" ? plan.mastery.standard : undefined, plan.purpose === "diagnostic" ? plan.diagnostic.diagnosticKey : undefined);
|
|
131250
|
+
return preparedAssessmentTestIsLive(plan, tests);
|
|
131251
|
+
}
|
|
131252
|
+
async loadPreparedPlanAssessment(plan) {
|
|
131253
|
+
const identity = preparedAssessmentArtifactIdentity(plan);
|
|
131254
|
+
const playable = plan.purpose === "review" ? await this.artifactLoader.loadPlayableContainingItems(identity, plan.review.selections.map((selection) => selection.itemIdentifier)) : await this.artifactLoader.loadPlayable(identity);
|
|
131255
|
+
if (!playable) {
|
|
131256
|
+
return null;
|
|
131257
|
+
}
|
|
131258
|
+
const scoring = await this.artifactLoader.loadMatchedScoring(playable, identity);
|
|
131259
|
+
if (!preparedAssessmentScoringIsCompatible(plan, scoring)) {
|
|
131260
|
+
return null;
|
|
131261
|
+
}
|
|
131262
|
+
if (plan.purpose !== "review") {
|
|
131263
|
+
return playable;
|
|
131264
|
+
}
|
|
131265
|
+
return projectReviewAssessment(playable, preparedReviewBankIndex(plan), plan.review.selections);
|
|
131266
|
+
}
|
|
131267
|
+
async createPreparedAttempt(plan, assessment, integration) {
|
|
131268
|
+
const timestamp6 = new Date().toISOString();
|
|
131269
|
+
const metadata2 = buildPreparedAssessmentMetadata(plan, timestamp6, assessment.title);
|
|
131270
|
+
const result = await this.requireClient().api.oneroster.assessmentResults.upsert(plan.attemptId, {
|
|
131271
|
+
status: ONEROSTER_STATUS2.active,
|
|
131272
|
+
assessmentLineItem: { sourcedId: plan.parentLineItemId },
|
|
131273
|
+
student: { sourcedId: plan.studentId },
|
|
131274
|
+
score: 0,
|
|
131275
|
+
scoreDate: timestamp6,
|
|
131276
|
+
scoreStatus: ASSESSMENT_ATTEMPT_OPEN.scoreStatus,
|
|
131277
|
+
inProgress: ASSESSMENT_ATTEMPT_OPEN.inProgress,
|
|
131278
|
+
metadata: buildAssessmentResultMetadata({
|
|
131279
|
+
metadata: metadata2,
|
|
131280
|
+
testName: assessment.title,
|
|
131281
|
+
grade: integration.grade
|
|
131282
|
+
})
|
|
131283
|
+
});
|
|
131284
|
+
return this.isPlatformRoutedDiagnosticMetadata(metadata2) ? this.diagnosticSnapshot(result, metadata2, assessment, integration, metadata2.diagnostic.state) : this.snapshot(result, metadata2, assessment, integration);
|
|
131285
|
+
}
|
|
129357
131286
|
async claimAttemptId(scope, from) {
|
|
129358
131287
|
for (let attemptNumber = from;attemptNumber < from + 20; attemptNumber += 1) {
|
|
129359
131288
|
const attemptId = await assessmentAttemptId({ ...scope, attemptNumber });
|
|
@@ -129508,6 +131437,35 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
129508
131437
|
}
|
|
129509
131438
|
};
|
|
129510
131439
|
});
|
|
131440
|
+
function assessmentArtifactFailureFallback(error88) {
|
|
131441
|
+
if (error88 instanceof ValidationError || error88 instanceof ConflictError || error88 instanceof AssessmentRuntimeError) {
|
|
131442
|
+
throw error88;
|
|
131443
|
+
}
|
|
131444
|
+
return false;
|
|
131445
|
+
}
|
|
131446
|
+
function assessmentArtifactPublicationWarning(artifactsAvailable) {
|
|
131447
|
+
return artifactsAvailable ? undefined : ASSESSMENT_ARTIFACTS_UNAVAILABLE_WARNING;
|
|
131448
|
+
}
|
|
131449
|
+
function assessmentArtifactImportMessage(message, artifactsAvailable) {
|
|
131450
|
+
return artifactsAvailable ? message : `${message} Optimized ${ARTIFACTS_UNAVAILABLE_DETAIL}`;
|
|
131451
|
+
}
|
|
131452
|
+
function assessmentArtifactReviewBankWarnings(warnings, artifactsAvailable) {
|
|
131453
|
+
return artifactsAvailable ? warnings : [
|
|
131454
|
+
...warnings,
|
|
131455
|
+
{
|
|
131456
|
+
code: "ASSESSMENT_ARTIFACTS_UNAVAILABLE",
|
|
131457
|
+
message: REVIEW_BANK_ARTIFACTS_UNAVAILABLE_WARNING
|
|
131458
|
+
}
|
|
131459
|
+
];
|
|
131460
|
+
}
|
|
131461
|
+
var ARTIFACTS_UNAVAILABLE_DETAIL = "assessment artifacts are unavailable. Students will use the slower QTI fallback until compilation is repaired.";
|
|
131462
|
+
var ASSESSMENT_ARTIFACTS_UNAVAILABLE_WARNING;
|
|
131463
|
+
var REVIEW_BANK_ARTIFACTS_UNAVAILABLE_WARNING;
|
|
131464
|
+
var init_assessment_artifact_fallback_util = __esm(() => {
|
|
131465
|
+
init_errors2();
|
|
131466
|
+
ASSESSMENT_ARTIFACTS_UNAVAILABLE_WARNING = `Published successfully, but optimized ${ARTIFACTS_UNAVAILABLE_DETAIL}`;
|
|
131467
|
+
REVIEW_BANK_ARTIFACTS_UNAVAILABLE_WARNING = `Review bank synchronized successfully, but optimized ${ARTIFACTS_UNAVAILABLE_DETAIL}`;
|
|
131468
|
+
});
|
|
129511
131469
|
function diagnosticRoutingCapabilities(assessment, scoring) {
|
|
129512
131470
|
return assessment.items.map((item) => ({
|
|
129513
131471
|
itemIdentifier: item.identifier,
|
|
@@ -129842,7 +131800,7 @@ class TimebackAssessmentsService {
|
|
|
129842
131800
|
assertManagedAssessmentIdentity(loaded.test, entry2.assessmentKey);
|
|
129843
131801
|
assertAssessmentHasQuestions(loaded.questions.questions);
|
|
129844
131802
|
assertPlayableAssessmentImportQuestions(targetStatus, loaded.questions.questions.map(({ question }) => question));
|
|
129845
|
-
return { entry: entry2,
|
|
131803
|
+
return { entry: entry2, source: loaded };
|
|
129846
131804
|
} catch (error88) {
|
|
129847
131805
|
return { entry: entry2, error: error88 };
|
|
129848
131806
|
}
|
|
@@ -129862,7 +131820,7 @@ class TimebackAssessmentsService {
|
|
|
129862
131820
|
const pendingInserts = [];
|
|
129863
131821
|
for (const validation of input.validated) {
|
|
129864
131822
|
const { entry: entry2 } = validation;
|
|
129865
|
-
const base = this.associationImportResultBase(entry2, "
|
|
131823
|
+
const base = this.associationImportResultBase(entry2, "source" in validation ? validation.source.test : undefined, input.gameSlug);
|
|
129866
131824
|
if ("error" in validation) {
|
|
129867
131825
|
input.results[entry2.index] = {
|
|
129868
131826
|
...base,
|
|
@@ -129873,7 +131831,7 @@ class TimebackAssessmentsService {
|
|
|
129873
131831
|
const existingForKey = input.existingByKey.get(entry2.assessmentKey);
|
|
129874
131832
|
const decision = planAssessmentAssociationImport(entry2, existingForKey, input.existingByIdentifier.get(entry2.qtiTestIdentifier));
|
|
129875
131833
|
if (decision.kind === "insert") {
|
|
129876
|
-
pendingInserts.push({ entry: entry2,
|
|
131834
|
+
pendingInserts.push({ entry: entry2, source: validation.source });
|
|
129877
131835
|
} else {
|
|
129878
131836
|
input.results[entry2.index] = {
|
|
129879
131837
|
...base,
|
|
@@ -129905,9 +131863,13 @@ class TimebackAssessmentsService {
|
|
|
129905
131863
|
}
|
|
129906
131864
|
}
|
|
129907
131865
|
async insertImportedAssessments(input) {
|
|
129908
|
-
for (const { entry: entry2,
|
|
129909
|
-
const base = this.associationImportResultBase(entry2, test, input.gameSlug);
|
|
131866
|
+
for (const { entry: entry2, source } of input.pending) {
|
|
131867
|
+
const base = this.associationImportResultBase(entry2, source.test, input.gameSlug);
|
|
129910
131868
|
try {
|
|
131869
|
+
let artifactsAvailable = true;
|
|
131870
|
+
if (input.targetStatus === "live") {
|
|
131871
|
+
artifactsAvailable = await this.storeManagedArtifacts(source, managedAssessmentDiagnosticRoutingManifest(source.test, entry2.assessmentKey));
|
|
131872
|
+
}
|
|
129911
131873
|
const [row] = await input.db.insert(gameTimebackAssessmentTests).values({
|
|
129912
131874
|
integrationId: input.integrationId,
|
|
129913
131875
|
assessmentKey: entry2.assessmentKey,
|
|
@@ -129930,11 +131892,12 @@ class TimebackAssessmentsService {
|
|
|
129930
131892
|
throw new Error("Assessment association insert conflicted");
|
|
129931
131893
|
}
|
|
129932
131894
|
} else {
|
|
131895
|
+
const message = attachedAssessmentImportMessage(input.targetStatus, base.editable ?? false);
|
|
129933
131896
|
input.results[entry2.index] = {
|
|
129934
131897
|
...base,
|
|
129935
131898
|
status: "created",
|
|
129936
131899
|
association: this.associationImportSummary(row),
|
|
129937
|
-
message:
|
|
131900
|
+
message: assessmentArtifactImportMessage(message, artifactsAvailable)
|
|
129938
131901
|
};
|
|
129939
131902
|
}
|
|
129940
131903
|
} catch (error88) {
|
|
@@ -130000,6 +131963,7 @@ class TimebackAssessmentsService {
|
|
|
130000
131963
|
assertQtiTestOwnedByGame(test, ownership.gameSlug);
|
|
130001
131964
|
await client2.qtiApi.assessmentTests.update(qtiTestIdentifier, buildQtiTestUpdateInput(test, input.title));
|
|
130002
131965
|
}
|
|
131966
|
+
let publicationWarning;
|
|
130003
131967
|
if (publishing) {
|
|
130004
131968
|
if (nextPurpose === "diagnostic" && !nextDiagnostic) {
|
|
130005
131969
|
throw new ValidationError("Platform-routed diagnostics require a diagnostic key and routing manifest before publication.");
|
|
@@ -130015,6 +131979,7 @@ class TimebackAssessmentsService {
|
|
|
130015
131979
|
if (publishing) {
|
|
130016
131980
|
const publication = await this.publishManagedAssessment(row, nextPurpose, nextDiagnostic?.routingManifest ?? null, loaded);
|
|
130017
131981
|
updates.qtiTestIdentifier = publication.qtiTestIdentifier;
|
|
131982
|
+
publicationWarning = publication.warning;
|
|
130018
131983
|
if (nextPurpose === "diagnostic") {
|
|
130019
131984
|
updates.diagnosticRoutingManifest = publication.diagnosticRoutingManifest;
|
|
130020
131985
|
}
|
|
@@ -130026,7 +131991,7 @@ class TimebackAssessmentsService {
|
|
|
130026
131991
|
updated = updatedRow ?? row;
|
|
130027
131992
|
}
|
|
130028
131993
|
setAttribute("app.assessment.operation", input.title !== undefined ? "update_test" : "update_association");
|
|
130029
|
-
return this.associationSummary(updated);
|
|
131994
|
+
return this.associationSummary(updated, publicationWarning);
|
|
130030
131995
|
});
|
|
130031
131996
|
} catch (error88) {
|
|
130032
131997
|
if (isUniqueViolation(error88)) {
|
|
@@ -130109,7 +132074,7 @@ class TimebackAssessmentsService {
|
|
|
130109
132074
|
assertReviewBankSinkOwnership(existingSink, integrationId);
|
|
130110
132075
|
}
|
|
130111
132076
|
let unchangedResult = null;
|
|
130112
|
-
if (existingSink) {
|
|
132077
|
+
if (existingSink?.title === PLAYCADEMY_REVIEW_BANK_TITLE) {
|
|
130113
132078
|
try {
|
|
130114
132079
|
unchangedResult = await unchangedReviewBankSynchronizationResult({
|
|
130115
132080
|
sink: existingSink,
|
|
@@ -130143,8 +132108,17 @@ class TimebackAssessmentsService {
|
|
|
130143
132108
|
const diff = reviewBankSynchronizationDiff({ existingSink, plan });
|
|
130144
132109
|
addedItemCount = diff.addedItemCount;
|
|
130145
132110
|
removedItemCount = diff.removedItemCount;
|
|
130146
|
-
catalogUnchanged =
|
|
132111
|
+
catalogUnchanged = false;
|
|
130147
132112
|
}
|
|
132113
|
+
const artifactTest = catalogUnchanged ? existingSink : buildReviewBankSinkInput({
|
|
132114
|
+
identifier: plan.bankIdentifier,
|
|
132115
|
+
title: PLAYCADEMY_REVIEW_BANK_TITLE,
|
|
132116
|
+
metadata: reviewBankSinkMetadata(integrationId, plan),
|
|
132117
|
+
itemIdentifiers: plan.itemIdentifiers,
|
|
132118
|
+
itemHref: (identifier) => qtiItemHref(client2, identifier),
|
|
132119
|
+
includeIdentifier: true
|
|
132120
|
+
});
|
|
132121
|
+
const artifactsAvailable = await this.storeReviewBankGeneration(client2, artifactTest, catalogUnchanged ? reviewBankTestItemIdentifiers(existingSink) : plan.itemIdentifiers);
|
|
130148
132122
|
const committed = await this.withLockedIntegrationAssessmentRows(integrationId, async (tx, lockedAssociations) => {
|
|
130149
132123
|
const lockedSources = this.reviewBankMasterySources(lockedAssociations);
|
|
130150
132124
|
const lockedFingerprint = await reviewBankContributorFingerprint(lockedSources);
|
|
@@ -130177,7 +132151,10 @@ class TimebackAssessmentsService {
|
|
|
130177
132151
|
unchanged,
|
|
130178
132152
|
warnings: plan.warnings
|
|
130179
132153
|
};
|
|
130180
|
-
return this.reviewMappingResult(
|
|
132154
|
+
return this.reviewMappingResult({
|
|
132155
|
+
...result,
|
|
132156
|
+
warnings: assessmentArtifactReviewBankWarnings(result.warnings, artifactsAvailable)
|
|
132157
|
+
});
|
|
130181
132158
|
});
|
|
130182
132159
|
if (committed) {
|
|
130183
132160
|
return committed;
|
|
@@ -130479,6 +132456,36 @@ class TimebackAssessmentsService {
|
|
|
130479
132456
|
};
|
|
130480
132457
|
});
|
|
130481
132458
|
}
|
|
132459
|
+
async storeReviewBankGeneration(client2, test, itemIdentifiers) {
|
|
132460
|
+
const startedAt = Date.now();
|
|
132461
|
+
const bank = await reviewBankIndexFromManifest(test.metadata, {
|
|
132462
|
+
bankIdentifier: test.identifier,
|
|
132463
|
+
membershipItemIdentifiers: itemIdentifiers
|
|
132464
|
+
});
|
|
132465
|
+
if (!bank) {
|
|
132466
|
+
throw new ValidationError(`Review bank ${test.identifier} has no revisioned mapping to compile`);
|
|
132467
|
+
}
|
|
132468
|
+
const identity = {
|
|
132469
|
+
kind: "review-bank",
|
|
132470
|
+
qtiTestIdentifier: bank.bankIdentifier,
|
|
132471
|
+
sourceContentRevision: bank.sourceContentRevision,
|
|
132472
|
+
bankRevision: bank.bankRevision
|
|
132473
|
+
};
|
|
132474
|
+
try {
|
|
132475
|
+
if (await this.deps.assessmentArtifacts.loadPair(identity)) {
|
|
132476
|
+
return true;
|
|
132477
|
+
}
|
|
132478
|
+
} catch (error88) {
|
|
132479
|
+
return this.handleArtifactFailure(test.identifier, "review-bank", startedAt, error88);
|
|
132480
|
+
}
|
|
132481
|
+
return this.compileAndStoreArtifacts(test.identifier, "review-bank", async () => compileReviewBankArtifacts({
|
|
132482
|
+
source: {
|
|
132483
|
+
test,
|
|
132484
|
+
questions: await hydrateQtiItemSelection(client2, test, itemIdentifiers)
|
|
132485
|
+
},
|
|
132486
|
+
bank
|
|
132487
|
+
}));
|
|
132488
|
+
}
|
|
130482
132489
|
async writeReviewBankSink(input) {
|
|
130483
132490
|
function sinkInput(title, includeIdentifier = false) {
|
|
130484
132491
|
return buildReviewBankSinkInput({
|
|
@@ -130491,11 +132498,11 @@ class TimebackAssessmentsService {
|
|
|
130491
132498
|
});
|
|
130492
132499
|
}
|
|
130493
132500
|
if (input.existingSink) {
|
|
130494
|
-
await input.client.qtiApi.assessmentTests.update(input.plan.bankIdentifier, sinkInput(
|
|
132501
|
+
await input.client.qtiApi.assessmentTests.update(input.plan.bankIdentifier, sinkInput(PLAYCADEMY_REVIEW_BANK_TITLE));
|
|
130495
132502
|
return;
|
|
130496
132503
|
}
|
|
130497
132504
|
try {
|
|
130498
|
-
const createInput = sinkInput(
|
|
132505
|
+
const createInput = sinkInput(PLAYCADEMY_REVIEW_BANK_TITLE, true);
|
|
130499
132506
|
await input.client.qtiApi.assessmentTests.create(createInput);
|
|
130500
132507
|
} catch (error88) {
|
|
130501
132508
|
if (!isApiError(error88) || error88.statusCode !== 409) {
|
|
@@ -130503,7 +132510,7 @@ class TimebackAssessmentsService {
|
|
|
130503
132510
|
}
|
|
130504
132511
|
const concurrentlyCreated = await input.client.qtiApi.assessmentTests.get(input.plan.bankIdentifier);
|
|
130505
132512
|
assertReviewBankSinkOwnership(concurrentlyCreated, input.integrationId);
|
|
130506
|
-
await input.client.qtiApi.assessmentTests.update(input.plan.bankIdentifier, sinkInput(
|
|
132513
|
+
await input.client.qtiApi.assessmentTests.update(input.plan.bankIdentifier, sinkInput(PLAYCADEMY_REVIEW_BANK_TITLE));
|
|
130507
132514
|
}
|
|
130508
132515
|
}
|
|
130509
132516
|
async installReviewBankAssociation(input) {
|
|
@@ -130688,15 +132695,64 @@ class TimebackAssessmentsService {
|
|
|
130688
132695
|
return get();
|
|
130689
132696
|
}
|
|
130690
132697
|
async ensureManagedQtiPublication(client2, plan) {
|
|
130691
|
-
await runWithConcurrency(plan.items, QTI_HYDRATION_CONCURRENCY, async (plannedItem) => {
|
|
132698
|
+
const existingItems = await runWithConcurrency(plan.items, QTI_HYDRATION_CONCURRENCY, async (plannedItem) => {
|
|
130692
132699
|
const existing = await this.ensureImmutableQtiResource(async () => await client2.qtiApi.assessmentItems.get(plannedItem.qtiItemIdentifier), () => client2.course.createAssessmentItemXml({
|
|
130693
132700
|
xml: plannedItem.xml,
|
|
130694
132701
|
metadata: plannedItem.metadata
|
|
130695
132702
|
}));
|
|
130696
132703
|
await assertManagedAssessmentItem(existing, plannedItem);
|
|
132704
|
+
return existing;
|
|
130697
132705
|
});
|
|
130698
132706
|
const existingTest = await this.ensureImmutableQtiResource(async () => await client2.qtiApi.assessmentTests.get(plan.qtiTestIdentifier), () => client2.qtiApi.assessmentTests.create(plan.testInput));
|
|
130699
132707
|
assertManagedAssessmentPublication(existingTest, plan);
|
|
132708
|
+
return {
|
|
132709
|
+
test: existingTest,
|
|
132710
|
+
questions: {
|
|
132711
|
+
...plan.artifactSource.questions,
|
|
132712
|
+
assessmentTest: existingTest.identifier,
|
|
132713
|
+
title: existingTest.title,
|
|
132714
|
+
totalQuestions: existingItems.length,
|
|
132715
|
+
questions: plan.artifactSource.questions.questions.map((entry2, index2) => ({
|
|
132716
|
+
...entry2,
|
|
132717
|
+
reference: {
|
|
132718
|
+
...entry2.reference,
|
|
132719
|
+
identifier: existingItems[index2].identifier
|
|
132720
|
+
},
|
|
132721
|
+
question: existingItems[index2]
|
|
132722
|
+
}))
|
|
132723
|
+
}
|
|
132724
|
+
};
|
|
132725
|
+
}
|
|
132726
|
+
async compileAndStoreArtifacts(qtiTestIdentifier, identityKind, compile) {
|
|
132727
|
+
const startedAt = Date.now();
|
|
132728
|
+
try {
|
|
132729
|
+
const compiled = await compile();
|
|
132730
|
+
const stored = await this.deps.assessmentArtifacts.storePair(compiled);
|
|
132731
|
+
addEvent("assessment.artifacts_compiled", {
|
|
132732
|
+
"app.assessment.qti_test_identifier": compiled.identity.qtiTestIdentifier,
|
|
132733
|
+
"app.assessment.artifact_identity_kind": compiled.identity.kind,
|
|
132734
|
+
"app.assessment.artifact_duration_ms": Date.now() - startedAt,
|
|
132735
|
+
"app.assessment.artifact_fallback_item_count": qtiFallbackItemCount(compiled),
|
|
132736
|
+
"app.assessment.playable_artifact_status": stored.playable.status,
|
|
132737
|
+
"app.assessment.scoring_artifact_status": stored.scoring.status
|
|
132738
|
+
});
|
|
132739
|
+
} catch (error88) {
|
|
132740
|
+
return this.handleArtifactFailure(qtiTestIdentifier, identityKind, startedAt, error88);
|
|
132741
|
+
}
|
|
132742
|
+
return true;
|
|
132743
|
+
}
|
|
132744
|
+
handleArtifactFailure(qtiTestIdentifier, identityKind, startedAt, error88) {
|
|
132745
|
+
addEvent("assessment.artifact_compilation_failed", {
|
|
132746
|
+
"app.assessment.qti_test_identifier": qtiTestIdentifier,
|
|
132747
|
+
"app.assessment.artifact_identity_kind": identityKind,
|
|
132748
|
+
"app.assessment.artifact_duration_ms": Date.now() - startedAt,
|
|
132749
|
+
"exception.type": errorType(error88),
|
|
132750
|
+
"app.error.message": errorMessage2(error88)
|
|
132751
|
+
});
|
|
132752
|
+
return assessmentArtifactFailureFallback(error88);
|
|
132753
|
+
}
|
|
132754
|
+
async storeManagedArtifacts(source, diagnosticRoutingManifest) {
|
|
132755
|
+
return this.compileAndStoreArtifacts(source.test.identifier, "assessment", () => compileManagedAssessmentArtifacts({ source, diagnosticRoutingManifest }));
|
|
130700
132756
|
}
|
|
130701
132757
|
async publishManagedAssessment(row, purpose, diagnosticRoutingManifest, loaded) {
|
|
130702
132758
|
if (!row.assessmentKey) {
|
|
@@ -130711,8 +132767,10 @@ class TimebackAssessmentsService {
|
|
|
130711
132767
|
diagnosticRoutingManifest,
|
|
130712
132768
|
itemHref: (identifier) => qtiItemHref(client2, identifier)
|
|
130713
132769
|
});
|
|
130714
|
-
await this.ensureManagedQtiPublication(client2, plan);
|
|
130715
|
-
|
|
132770
|
+
const artifactSource = await this.ensureManagedQtiPublication(client2, plan);
|
|
132771
|
+
const artifactsAvailable = await this.storeManagedArtifacts(artifactSource, managedAssessmentDiagnosticRoutingManifest(artifactSource.test, plan.assessmentKey));
|
|
132772
|
+
const warning = assessmentArtifactPublicationWarning(artifactsAvailable);
|
|
132773
|
+
return { ...plan, ...warning ? { warning } : {} };
|
|
130716
132774
|
}
|
|
130717
132775
|
async cleanupQtiAssessmentCopy(client2, testIdentifier, itemIdentifiers) {
|
|
130718
132776
|
let testDeleted = !testIdentifier;
|
|
@@ -130745,7 +132803,7 @@ class TimebackAssessmentsService {
|
|
|
130745
132803
|
}
|
|
130746
132804
|
}
|
|
130747
132805
|
}
|
|
130748
|
-
associationSummary(row) {
|
|
132806
|
+
associationSummary(row, warning) {
|
|
130749
132807
|
return {
|
|
130750
132808
|
id: row.id,
|
|
130751
132809
|
integrationId: row.integrationId,
|
|
@@ -130756,7 +132814,8 @@ class TimebackAssessmentsService {
|
|
|
130756
132814
|
standard: assessmentStandardForRow(row),
|
|
130757
132815
|
diagnostic: diagnosticDefinitionForRow(row),
|
|
130758
132816
|
createdAt: row.createdAt,
|
|
130759
|
-
updatedAt: row.updatedAt
|
|
132817
|
+
updatedAt: row.updatedAt,
|
|
132818
|
+
...warning ? { warning } : {}
|
|
130760
132819
|
};
|
|
130761
132820
|
}
|
|
130762
132821
|
associationImportSummary(row) {
|
|
@@ -130824,7 +132883,6 @@ class TimebackAssessmentsService {
|
|
|
130824
132883
|
};
|
|
130825
132884
|
}
|
|
130826
132885
|
}
|
|
130827
|
-
var REVIEW_BANK_TITLE = "Review Bank";
|
|
130828
132886
|
var REVIEW_BANK_SYNC_ATTEMPTS = 2;
|
|
130829
132887
|
var init_timeback_assessments_service = __esm(async () => {
|
|
130830
132888
|
init_drizzle_orm();
|
|
@@ -130834,6 +132892,8 @@ var init_timeback_assessments_service = __esm(async () => {
|
|
|
130834
132892
|
init_assessment_runtime2();
|
|
130835
132893
|
init_timeback3();
|
|
130836
132894
|
init_errors2();
|
|
132895
|
+
init_assessment_artifact_fallback_util();
|
|
132896
|
+
init_timeback_assessment_artifacts_util();
|
|
130837
132897
|
init_timeback_assessment_diagnostic_util();
|
|
130838
132898
|
init_timeback_assessment_import_util();
|
|
130839
132899
|
init_timeback_assessment_publication_util();
|
|
@@ -132836,6 +134896,8 @@ function createPlatformServices(deps) {
|
|
|
132836
134896
|
timebackClient,
|
|
132837
134897
|
alerts,
|
|
132838
134898
|
mintPlatformServiceToken,
|
|
134899
|
+
mintAssessmentPreparationReceipt,
|
|
134900
|
+
verifyAssessmentPreparationReceipt,
|
|
132839
134901
|
validateDeveloperAccessBySlug,
|
|
132840
134902
|
validateDeveloperAccess,
|
|
132841
134903
|
validateGameManagementAccess
|
|
@@ -132851,6 +134913,10 @@ function createPlatformServices(deps) {
|
|
|
132851
134913
|
environment: getPlatformEnvironment(config5),
|
|
132852
134914
|
storage
|
|
132853
134915
|
});
|
|
134916
|
+
const assessmentArtifacts = new AssessmentArtifactsService({
|
|
134917
|
+
bucketName: config5.assessmentArtifactBucket,
|
|
134918
|
+
storage
|
|
134919
|
+
});
|
|
132854
134920
|
const upload = new UploadService({
|
|
132855
134921
|
uploadBucket: config5.uploadBucket,
|
|
132856
134922
|
generatePresignedPutUrl: storage.generatePresignedPutUrl.bind(storage),
|
|
@@ -132896,16 +134962,21 @@ function createPlatformServices(deps) {
|
|
|
132896
134962
|
const timebackAssessments = new TimebackAssessmentsService({
|
|
132897
134963
|
db: db2,
|
|
132898
134964
|
timeback: timebackClient,
|
|
134965
|
+
assessmentArtifacts,
|
|
132899
134966
|
validateGameManagementAccess
|
|
132900
134967
|
});
|
|
132901
134968
|
const timebackAssessmentRuntime = new TimebackAssessmentRuntimeService({
|
|
132902
134969
|
db: db2,
|
|
132903
134970
|
timeback: timebackClient,
|
|
134971
|
+
assessmentArtifacts,
|
|
132904
134972
|
validateDeveloperAccess,
|
|
132905
|
-
assessmentRuntimeLock
|
|
134973
|
+
assessmentRuntimeLock,
|
|
134974
|
+
mintAssessmentPreparationReceipt,
|
|
134975
|
+
verifyAssessmentPreparationReceipt
|
|
132906
134976
|
});
|
|
132907
134977
|
return {
|
|
132908
134978
|
assessmentAssets,
|
|
134979
|
+
assessmentArtifacts,
|
|
132909
134980
|
bucket,
|
|
132910
134981
|
upload,
|
|
132911
134982
|
kv,
|
|
@@ -132922,6 +134993,7 @@ function createPlatformServices(deps) {
|
|
|
132922
134993
|
}
|
|
132923
134994
|
var init_platform2 = __esm(async () => {
|
|
132924
134995
|
init_config2();
|
|
134996
|
+
init_assessment_artifacts_service();
|
|
132925
134997
|
init_assessment_assets_service();
|
|
132926
134998
|
init_bucket_service();
|
|
132927
134999
|
init_database_service();
|
|
@@ -133633,6 +135705,8 @@ function createServices(ctx) {
|
|
|
133633
135705
|
timebackClient: timeback2,
|
|
133634
135706
|
alerts: infra2.alerts,
|
|
133635
135707
|
mintPlatformServiceToken: auth2.mintPlatformServiceToken.bind(auth2),
|
|
135708
|
+
mintAssessmentPreparationReceipt: auth2.mintAssessmentPreparationReceipt.bind(auth2),
|
|
135709
|
+
verifyAssessmentPreparationReceipt: auth2.verifyAssessmentPreparationReceipt.bind(auth2),
|
|
133636
135710
|
...gaming.validators
|
|
133637
135711
|
});
|
|
133638
135712
|
const standalone = createStandaloneServices({ db: db2, auth: auth2, timeback: timeback2 });
|
|
@@ -133763,6 +135837,13 @@ function defineControllerNames(namespace, handlers) {
|
|
|
133763
135837
|
}
|
|
133764
135838
|
return handlers;
|
|
133765
135839
|
}
|
|
135840
|
+
var init_assessment_artifact_backfill_util = __esm(() => {
|
|
135841
|
+
init_tables_index();
|
|
135842
|
+
init_timeback_assessment_artifacts_util();
|
|
135843
|
+
init_timeback_assessment_publication_util();
|
|
135844
|
+
init_timeback_qti_hydration_util();
|
|
135845
|
+
init_timeback_review_mapping_util();
|
|
135846
|
+
});
|
|
133766
135847
|
function generateUsername(email5) {
|
|
133767
135848
|
const baseUsername = (email5.split("@")[0] || "user").toLowerCase();
|
|
133768
135849
|
const cleanUsername = baseUsername.replace(/[^a-z0-9]/g, "");
|
|
@@ -133959,6 +136040,9 @@ var init_validation_util = __esm(() => {
|
|
|
133959
136040
|
var init_utils7 = __esm(() => {
|
|
133960
136041
|
init_auth_util();
|
|
133961
136042
|
init_assessment_runtime_lock_util();
|
|
136043
|
+
init_assessment_artifact_envelope_util();
|
|
136044
|
+
init_assessment_artifact_backfill_util();
|
|
136045
|
+
init_timeback_assessment_artifacts_util();
|
|
133962
136046
|
init_dashboard_util();
|
|
133963
136047
|
init_deployment_util();
|
|
133964
136048
|
init_leaderboard_util();
|
|
@@ -134110,6 +136194,32 @@ function createSandboxAuthProvider() {
|
|
|
134110
136194
|
}));
|
|
134111
136195
|
return `${header}.${payload}.sandbox`;
|
|
134112
136196
|
},
|
|
136197
|
+
async mintAssessmentPreparationReceipt(plan) {
|
|
136198
|
+
const now2 = Date.now();
|
|
136199
|
+
for (const [receipt2, prepared] of sandboxAssessmentPreparationReceipts) {
|
|
136200
|
+
if (prepared.expiresAt <= now2) {
|
|
136201
|
+
sandboxAssessmentPreparationReceipts.delete(receipt2);
|
|
136202
|
+
}
|
|
136203
|
+
}
|
|
136204
|
+
const receipt = `sandbox_assessment_preparation_${crypto.randomUUID()}`;
|
|
136205
|
+
const expiresAt = now2 + ASSESSMENT_PREPARATION_RECEIPT_LIFETIME_MS;
|
|
136206
|
+
sandboxAssessmentPreparationReceipts.set(receipt, {
|
|
136207
|
+
expiresAt,
|
|
136208
|
+
plan: structuredClone(plan)
|
|
136209
|
+
});
|
|
136210
|
+
return { receipt, expiresAt: new Date(expiresAt).toISOString() };
|
|
136211
|
+
},
|
|
136212
|
+
async verifyAssessmentPreparationReceipt(receipt) {
|
|
136213
|
+
const prepared = sandboxAssessmentPreparationReceipts.get(receipt);
|
|
136214
|
+
if (!prepared) {
|
|
136215
|
+
return null;
|
|
136216
|
+
}
|
|
136217
|
+
if (prepared.expiresAt <= Date.now()) {
|
|
136218
|
+
sandboxAssessmentPreparationReceipts.delete(receipt);
|
|
136219
|
+
return null;
|
|
136220
|
+
}
|
|
136221
|
+
return structuredClone(prepared.plan);
|
|
136222
|
+
},
|
|
134113
136223
|
async mintLogStreamToken(userId, workerId) {
|
|
134114
136224
|
const jti = crypto.randomUUID();
|
|
134115
136225
|
const payload = {
|
|
@@ -134152,10 +136262,14 @@ function createSandboxAuthProvider() {
|
|
|
134152
136262
|
};
|
|
134153
136263
|
}
|
|
134154
136264
|
var sandboxApiKeys;
|
|
136265
|
+
var sandboxAssessmentPreparationReceipts;
|
|
136266
|
+
var ASSESSMENT_PREPARATION_RECEIPT_LIFETIME_MS;
|
|
134155
136267
|
var init_auth_provider = __esm(() => {
|
|
134156
136268
|
init_src6();
|
|
134157
136269
|
init_constants();
|
|
134158
136270
|
sandboxApiKeys = new Map;
|
|
136271
|
+
sandboxAssessmentPreparationReceipts = new Map;
|
|
136272
|
+
ASSESSMENT_PREPARATION_RECEIPT_LIFETIME_MS = 300000;
|
|
134159
136273
|
});
|
|
134160
136274
|
function createSandboxCacheProvider() {
|
|
134161
136275
|
return {
|
|
@@ -134320,6 +136434,7 @@ function buildConfig(options) {
|
|
|
134320
136434
|
baseUrl,
|
|
134321
136435
|
gameDomain: "localhost",
|
|
134322
136436
|
uploadBucket: "sandbox-uploads",
|
|
136437
|
+
assessmentArtifactBucket: "sandbox-assessment-artifacts",
|
|
134323
136438
|
ltiTestMode: true,
|
|
134324
136439
|
secretsManifestPepper: "sandbox-secrets-manifest-pepper",
|
|
134325
136440
|
...options.config
|
|
@@ -193453,6 +195568,7 @@ var endActivity;
|
|
|
193453
195568
|
var heartbeat;
|
|
193454
195569
|
var advanceCourse;
|
|
193455
195570
|
var unenrollCourse;
|
|
195571
|
+
var prepareRuntimeAssessment;
|
|
193456
195572
|
var startRuntimeAssessment;
|
|
193457
195573
|
var getRuntimeAssessment;
|
|
193458
195574
|
var getLatestRuntimeAssessment;
|
|
@@ -193739,13 +195855,24 @@ var init_timeback_controller = __esm(() => {
|
|
|
193739
195855
|
user: ctx.user
|
|
193740
195856
|
});
|
|
193741
195857
|
});
|
|
195858
|
+
prepareRuntimeAssessment = requireDeveloper(async (ctx) => {
|
|
195859
|
+
const body2 = await parseRequestBody(ctx.request, PrepareRuntimeAssessmentRequestSchema);
|
|
195860
|
+
const { gameId, studentId, ...input } = body2;
|
|
195861
|
+
return ctx.services.timebackAssessmentRuntime.prepare({
|
|
195862
|
+
gameId,
|
|
195863
|
+
studentId,
|
|
195864
|
+
input,
|
|
195865
|
+
user: ctx.user
|
|
195866
|
+
});
|
|
195867
|
+
});
|
|
193742
195868
|
startRuntimeAssessment = requireDeveloper(async (ctx) => {
|
|
193743
195869
|
const body2 = await parseRequestBody(ctx.request, StartRuntimeAssessmentRequestSchema);
|
|
193744
|
-
const { gameId, studentId, ...input } = body2;
|
|
195870
|
+
const { gameId, studentId, preparationReceipt, ...input } = body2;
|
|
193745
195871
|
return ctx.services.timebackAssessmentRuntime.start({
|
|
193746
195872
|
gameId,
|
|
193747
195873
|
studentId,
|
|
193748
195874
|
input,
|
|
195875
|
+
preparationReceipt,
|
|
193749
195876
|
user: ctx.user
|
|
193750
195877
|
});
|
|
193751
195878
|
});
|
|
@@ -194308,6 +196435,7 @@ var init_timeback_controller = __esm(() => {
|
|
|
194308
196435
|
heartbeat,
|
|
194309
196436
|
advanceCourse,
|
|
194310
196437
|
unenrollCourse,
|
|
196438
|
+
prepareRuntimeAssessment,
|
|
194311
196439
|
startRuntimeAssessment,
|
|
194312
196440
|
getLatestRuntimeAssessment,
|
|
194313
196441
|
getRuntimeAssessment,
|
|
@@ -195261,6 +197389,7 @@ var init_timeback8 = __esm(async () => {
|
|
|
195261
197389
|
timebackRouter.post("/heartbeat", handle2(timeback2.heartbeat));
|
|
195262
197390
|
timebackRouter.post("/advance-course", handle2(timeback2.advanceCourse));
|
|
195263
197391
|
timebackRouter.post("/unenroll-course", handle2(timeback2.unenrollCourse));
|
|
197392
|
+
timebackRouter.post("/assessments/prepare", handle2(timeback2.prepareRuntimeAssessment));
|
|
195264
197393
|
timebackRouter.post("/assessments/start", handle2(timeback2.startRuntimeAssessment));
|
|
195265
197394
|
timebackRouter.get("/assessments/latest", handle2(timeback2.getLatestRuntimeAssessment));
|
|
195266
197395
|
timebackRouter.get("/assessments/:attemptId", handle2(timeback2.getRuntimeAssessment));
|