@playcademy/vite-plugin 1.3.1-beta.3 → 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.
Files changed (2) hide show
  1. package/dist/index.js +3002 -873
  2. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -24317,7 +24317,7 @@ import path2 from "node:path";
24317
24317
  // package.json
24318
24318
  var package_default = {
24319
24319
  name: "@playcademy/vite-plugin",
24320
- version: "1.3.1-beta.3",
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.1-beta.3",
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
- StartRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.and(StartAssessmentBodySchema);
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 isRecord6(value) {
122643
+ function isRecord7(value) {
121917
122644
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
121918
122645
  }
121919
122646
  function metadataOf(record3) {
121920
- return isRecord6(record3) && isRecord6(record3.metadata) ? record3.metadata : {};
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 isRecord6(value) ? stringField3(value.sourcedId) : "";
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 (!isRecord6(record3)) {
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 (isRecord6(set3)) {
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 (isRecord6(set3)) {
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 (isRecord6(item)) {
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 = isRecord6(questionRef) && isRecord6(questionRef.question) ? questionRef.question : null;
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 = isRecord6(resultMetadata.attemptedQuestionData) ? resultMetadata.attemptedQuestionData : null;
122208
- const attemptedResult = isRecord6(attemptedQuestionData?.result) ? attemptedQuestionData.result : null;
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 openAttemptCourseIds = needsOpenAnnotation ? await this.openAttemptCourseIds({
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
- }) : new Set;
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
- const results = this.requireClient().api.oneroster.assessmentResults.stream({
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": scope.studentId,
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 await (const result of results) {
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
- const results = this.requireClient().api.oneroster.assessmentResults.stream({
128269
- where: {
128270
- "student.sourcedId": scope.studentId,
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 await (const result of results) {
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, test: loaded.test };
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, "test" in validation ? validation.test : undefined, input.gameSlug);
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, test: validation.test });
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, test } of input.pending) {
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: attachedAssessmentImportMessage(input.targetStatus, base.editable ?? false)
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 = diff.unchanged;
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(result);
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(input.existingSink.title));
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(REVIEW_BANK_TITLE, true);
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(concurrentlyCreated.title));
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
- return plan;
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));