@playcademy/sandbox 0.8.1-beta.2 → 0.8.1-beta.4

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 (3) hide show
  1. package/dist/cli.js +3001 -980
  2. package/dist/server.js +3001 -980
  3. package/package.json +1 -1
package/dist/server.js CHANGED
@@ -1127,7 +1127,7 @@ var package_default;
1127
1127
  var init_package = __esm(() => {
1128
1128
  package_default = {
1129
1129
  name: "@playcademy/sandbox",
1130
- version: "0.8.1-beta.2",
1130
+ version: "0.8.1-beta.4",
1131
1131
  description: "Local development server for Playcademy game development",
1132
1132
  type: "module",
1133
1133
  exports: {
@@ -10766,39 +10766,21 @@ function compareNewestCompletedAssessmentAttempt(left, right) {
10766
10766
  return newestAttempt(left, right, "scoreDate");
10767
10767
  }
10768
10768
  function compareAssessmentCatalogOrder(left, right) {
10769
- if (left.sortOrder !== null || right.sortOrder !== null) {
10770
- if (left.sortOrder === null) {
10771
- return 1;
10772
- }
10773
- if (right.sortOrder === null) {
10774
- return -1;
10775
- }
10776
- const orderDifference = left.sortOrder - right.sortOrder;
10777
- if (orderDifference) {
10778
- return orderDifference;
10779
- }
10780
- }
10781
- const leftUpdatedAt = Date.parse(left.updatedAt);
10782
- const rightUpdatedAt = Date.parse(right.updatedAt);
10783
- const leftHasValidDate = Number.isFinite(leftUpdatedAt);
10784
- const rightHasValidDate = Number.isFinite(rightUpdatedAt);
10785
- if (leftHasValidDate && rightHasValidDate) {
10786
- const updatedDifference = leftUpdatedAt - rightUpdatedAt;
10787
- if (updatedDifference) {
10788
- return updatedDifference;
10789
- }
10790
- } else if (leftHasValidDate !== rightHasValidDate) {
10791
- return leftHasValidDate ? -1 : 1;
10792
- }
10793
10769
  if (left.assessmentKey !== null && right.assessmentKey !== null) {
10794
- const keyDifference = left.assessmentKey.localeCompare(right.assessmentKey);
10770
+ const keyDifference = compareCodePoints(left.assessmentKey, right.assessmentKey);
10795
10771
  if (keyDifference) {
10796
10772
  return keyDifference;
10797
10773
  }
10798
10774
  } else if (left.assessmentKey !== null || right.assessmentKey !== null) {
10799
10775
  return left.assessmentKey === null ? 1 : -1;
10800
10776
  }
10801
- return left.qtiTestIdentifier.localeCompare(right.qtiTestIdentifier);
10777
+ return compareCodePoints(left.qtiTestIdentifier, right.qtiTestIdentifier);
10778
+ }
10779
+ function compareCodePoints(left, right) {
10780
+ if (left === right) {
10781
+ return 0;
10782
+ }
10783
+ return left < right ? -1 : 1;
10802
10784
  }
10803
10785
  function orderRuntimeAssessmentTests(tests) {
10804
10786
  return [...tests].toSorted(compareAssessmentCatalogOrder);
@@ -11582,7 +11564,7 @@ function playcademyDiagnosticAssessmentItemResultMetadata(value) {
11582
11564
  const normalized = metadata2.responses === undefined ? { ...metadata2, responses: {} } : metadata2;
11583
11565
  return isPlaycademyDiagnosticAssessmentItemResultMetadataV1(normalized) ? normalized : null;
11584
11566
  }
11585
- var SCRIPT_SCHEMES, DATA_RASTER_IMAGE_PATTERN, QTI_MATHML_ALLOWED_TAGS, metadataSymbol, parser, SUPPORTED_QTI_INTERACTION_TYPES, URL_ATTRIBUTES, IMAGE_URL_ATTRIBUTES, INTERACTION_TYPES, EMPHASIS_TAGS, STRONG_TAGS, CONTENT_SKIPPED_TAGS, BLOCK_CONTENT_KINDS, GAP_MATCH_TOKEN_TAGS, STAGE_GRAPHIC_EXCLUDED_CONTAINERS, BLANK_SENTINEL = "￿", MARKED_BLANK_PATTERN, MARKED_BLANK_GLOBAL_PATTERN, MATCH_CORRECT_TEMPLATE = "http://www.imsglobal.org/question/qti_v3p0/rptemplates/match_correct", MAP_RESPONSE_TEMPLATE = "https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/map_response", MAP_RESPONSE_TEMPLATES, RESPONSE_PROCESSING_TEMPLATES, VOID_BODY_ELEMENTS, URL_ATTRIBUTES2, IMAGE_URL_ATTRIBUTES2, VOID_CHOICE_ELEMENTS, NUMERIC_COMPARISON_ATTRIBUTES, RESPONSE_CARDINALITIES, RESPONSE_BASE_TYPES, POINT_INTERACTION_TYPES, INDEPENDENT_PROCESSING_TAGS, UUID_PATTERN, COMMON_CORE_MATH_FRAMEWORK_ALIASES, COMMON_CORE_ELA_FRAMEWORK_ALIASES, COMMON_CORE_FRAMEWORK_ALIASES, ASSESSMENT_ATTEMPT_OPEN, ASSESSMENT_ATTEMPT_COMPLETED, ASSESSMENT_ATTEMPT_AWAITING_AWARD, ASSESSMENT_ATTEMPT_SUPERSEDED, ASSESSMENT_RUNTIME_ERROR_STATUS, ROUTING_KEY_MAX_LENGTH = 128, ROUTING_RESULT_KEY_MAX_LENGTH = 256, ROUTING_STAGE_LIMIT = 32, ROUTING_TRACK_LIMIT = 128, ROUTING_NODE_LIMIT = 1000, ROUTING_TERMINAL_LIMIT = 1000, ROUTING_GROUP_LIMIT = 128, ROUTING_PREDICATE_DEPTH_LIMIT = 8, ROUTING_EXPLORATION_STATE_LIMIT = 200000, RoutingKeySchema, RoutingResultKeySchema, DiagnosticTrackOutcomeSchema, DiagnosticActivationPredicateSchema, DiagnosticRoutingTransitionSchema, DiagnosticRoutingManifestV1Schema, GRADE_VALUES, POINT_RESPONSE_PATTERN, REVIEW_SELECTION_POLICY_VERSION = "unseen-first-least-recently-delivered-v1", PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY = "playcademyReviewBank", PLAYCADEMY_REVIEW_BANK_MANIFEST_VERSION = 1, DEFAULT_REVIEW_SELECTION_POLICY, ASSESSMENT_RUNTIME_FIXTURE_BUNDLE_VERSION = 7, RuntimeSubjectSchema, RuntimeGradeSchema, OptionalQueryGradeSchema, AssessmentResponseValueSchema, AssessmentRuntimeIdentitySchema, ResponseKeySchema, StartAssessmentBaseSchema, AssessmentStandardRefSchema, LatestAssessmentFilterBaseSchema, LatestAssessmentFiltersSchema, LatestRuntimeAssessmentQuerySchema, StartAssessmentBodySchema, SaveAssessmentBodySchema, SubmitAssessmentItemBodySchema, SubmitAssessmentBodySchema, FinalizeAssessmentBodySchema, StartRuntimeAssessmentRequestSchema, SaveRuntimeAssessmentRequestSchema, SubmitAssessmentItemRuntimeRequestSchema, SubmitRuntimeAssessmentRequestSchema, FinalizeRuntimeAssessmentRequestSchema;
11567
+ var SCRIPT_SCHEMES, DATA_RASTER_IMAGE_PATTERN, QTI_MATHML_ALLOWED_TAGS, metadataSymbol, parser, SUPPORTED_QTI_INTERACTION_TYPES, URL_ATTRIBUTES, IMAGE_URL_ATTRIBUTES, INTERACTION_TYPES, EMPHASIS_TAGS, STRONG_TAGS, CONTENT_SKIPPED_TAGS, BLOCK_CONTENT_KINDS, GAP_MATCH_TOKEN_TAGS, STAGE_GRAPHIC_EXCLUDED_CONTAINERS, BLANK_SENTINEL = "￿", MARKED_BLANK_PATTERN, MARKED_BLANK_GLOBAL_PATTERN, MATCH_CORRECT_TEMPLATE = "http://www.imsglobal.org/question/qti_v3p0/rptemplates/match_correct", MAP_RESPONSE_TEMPLATE = "https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/map_response", MAP_RESPONSE_TEMPLATES, RESPONSE_PROCESSING_TEMPLATES, VOID_BODY_ELEMENTS, URL_ATTRIBUTES2, IMAGE_URL_ATTRIBUTES2, VOID_CHOICE_ELEMENTS, NUMERIC_COMPARISON_ATTRIBUTES, RESPONSE_CARDINALITIES, RESPONSE_BASE_TYPES, POINT_INTERACTION_TYPES, INDEPENDENT_PROCESSING_TAGS, SUPPORTED_BASE_TYPES, SUPPORTED_CARDINALITIES, MATCH_CORRECT_TEMPLATES, UUID_PATTERN, COMMON_CORE_MATH_FRAMEWORK_ALIASES, COMMON_CORE_ELA_FRAMEWORK_ALIASES, COMMON_CORE_FRAMEWORK_ALIASES, ASSESSMENT_ATTEMPT_OPEN, ASSESSMENT_ATTEMPT_COMPLETED, ASSESSMENT_ATTEMPT_AWAITING_AWARD, ASSESSMENT_ATTEMPT_SUPERSEDED, ASSESSMENT_RUNTIME_ERROR_STATUS, ROUTING_KEY_MAX_LENGTH = 128, ROUTING_RESULT_KEY_MAX_LENGTH = 256, ROUTING_STAGE_LIMIT = 32, ROUTING_TRACK_LIMIT = 128, ROUTING_NODE_LIMIT = 1000, ROUTING_TERMINAL_LIMIT = 1000, ROUTING_GROUP_LIMIT = 128, ROUTING_PREDICATE_DEPTH_LIMIT = 8, ROUTING_EXPLORATION_STATE_LIMIT = 200000, RoutingKeySchema, RoutingResultKeySchema, DiagnosticTrackOutcomeSchema, DiagnosticActivationPredicateSchema, DiagnosticRoutingTransitionSchema, DiagnosticRoutingManifestV1Schema, GRADE_VALUES, POINT_RESPONSE_PATTERN, REVIEW_SELECTION_POLICY_VERSION = "unseen-first-least-recently-delivered-v1", PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY = "playcademyReviewBank", PLAYCADEMY_REVIEW_BANK_MANIFEST_VERSION = 1, DEFAULT_REVIEW_SELECTION_POLICY, ASSESSMENT_RUNTIME_FIXTURE_BUNDLE_VERSION = 8, RuntimeSubjectSchema, RuntimeGradeSchema, OptionalQueryGradeSchema, AssessmentResponseValueSchema, AssessmentRuntimeIdentitySchema, ResponseKeySchema, StartAssessmentBaseSchema, AssessmentStandardRefSchema, LatestAssessmentFilterBaseSchema, LatestAssessmentFiltersSchema, LatestRuntimeAssessmentQuerySchema, StartAssessmentBodySchema, AssessmentPreparationReceiptSchema, StartAssessmentRequestBodySchema, SaveAssessmentBodySchema, SubmitAssessmentItemBodySchema, SubmitAssessmentBodySchema, FinalizeAssessmentBodySchema, PrepareRuntimeAssessmentRequestSchema, StartRuntimeAssessmentRequestSchema, SaveRuntimeAssessmentRequestSchema, SubmitAssessmentItemRuntimeRequestSchema, SubmitRuntimeAssessmentRequestSchema, FinalizeRuntimeAssessmentRequestSchema;
11586
11568
  var init_assessment_runtime2 = __esm(() => {
11587
11569
  init_timeback3();
11588
11570
  init_timeback3();
@@ -11736,6 +11718,23 @@ var init_assessment_runtime2 = __esm(() => {
11736
11718
  "qti-sum",
11737
11719
  "qti-base-value"
11738
11720
  ]);
11721
+ SUPPORTED_BASE_TYPES = new Set([
11722
+ "string",
11723
+ "identifier",
11724
+ "integer",
11725
+ "float",
11726
+ "directedPair"
11727
+ ]);
11728
+ SUPPORTED_CARDINALITIES = new Set([
11729
+ "single",
11730
+ "multiple",
11731
+ "ordered"
11732
+ ]);
11733
+ MATCH_CORRECT_TEMPLATES = new Set([
11734
+ "http://www.imsglobal.org/question/qti_v3p0/rptemplates/match_correct",
11735
+ "https://www.imsglobal.org/question/qti_v3p0/rptemplates/match_correct",
11736
+ "https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/match_correct"
11737
+ ].flatMap((template) => [template, `${template}.xml`]));
11739
11738
  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;
11740
11739
  COMMON_CORE_MATH_FRAMEWORK_ALIASES = new Set([
11741
11740
  "ccssm",
@@ -11912,6 +11911,8 @@ var init_assessment_runtime2 = __esm(() => {
11912
11911
  standard: AssessmentStandardRefSchema
11913
11912
  })
11914
11913
  ]);
11914
+ AssessmentPreparationReceiptSchema = exports_external.string().max(32000);
11915
+ StartAssessmentRequestBodySchema = exports_external.intersection(StartAssessmentBodySchema, exports_external.object({ preparationReceipt: AssessmentPreparationReceiptSchema.optional() }));
11915
11916
  SaveAssessmentBodySchema = exports_external.object({
11916
11917
  expectedResponseVersion: exports_external.number().int().nonnegative(),
11917
11918
  responses: exports_external.record(ResponseKeySchema, exports_external.record(ResponseKeySchema, AssessmentResponseValueSchema.nullable()))
@@ -11939,7 +11940,8 @@ var init_assessment_runtime2 = __esm(() => {
11939
11940
  message: "Provide either masteredUnits or masteredUnitsAbsolute, not both",
11940
11941
  path: ["masteredUnits"]
11941
11942
  });
11942
- StartRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.and(StartAssessmentBodySchema);
11943
+ PrepareRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.and(StartAssessmentBodySchema);
11944
+ StartRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.and(StartAssessmentRequestBodySchema);
11943
11945
  SaveRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.extend(SaveAssessmentBodySchema.shape);
11944
11946
  SubmitAssessmentItemRuntimeRequestSchema = AssessmentRuntimeIdentitySchema.extend(SubmitAssessmentItemBodySchema.shape);
11945
11947
  SubmitRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.extend(SubmitAssessmentBodySchema.shape).extend({
@@ -12467,6 +12469,7 @@ var init_schema = __esm(() => {
12467
12469
  ltiTestMode: exports_external.boolean().default(false),
12468
12470
  platformServiceJwt: platformServiceJwtConfigSchema.optional(),
12469
12471
  uploadBucket: exports_external.string().optional(),
12472
+ assessmentArtifactBucket: exports_external.string().optional(),
12470
12473
  cdnOrigin: exports_external.string().url().startsWith("https://").optional(),
12471
12474
  queueIngressSecret: exports_external.string().optional(),
12472
12475
  secretsManifestPepper: exports_external.string().optional()
@@ -38546,6 +38549,319 @@ var init_infra2 = __esm(() => {
38546
38549
  init_kv_backup_service();
38547
38550
  });
38548
38551
 
38552
+ // ../api-core/src/utils/assessment-artifact-envelope.util.ts
38553
+ function nonEmptyIdentityValue(value) {
38554
+ return typeof value === "string" && value.length > 0 && value === value.trim();
38555
+ }
38556
+ function assessmentArtifactPairKeys(identity) {
38557
+ assertAssessmentArtifactIdentity(identity);
38558
+ const identitySegments = identity.kind === "assessment" ? ["qti", identity.qtiTestIdentifier] : [
38559
+ "review",
38560
+ identity.qtiTestIdentifier,
38561
+ identity.sourceContentRevision,
38562
+ identity.bankRevision
38563
+ ];
38564
+ const identityPath = identitySegments.map(encodeURIComponent).join("/");
38565
+ const versionPath = `artifact-v${ASSESSMENT_ARTIFACT_SCHEMA_VERSION}/grader-v${ASSESSMENT_GRADER_PROTOCOL_VERSION}`;
38566
+ const prefix = `${ARTIFACT_KEY_PREFIX}/${identityPath}/${versionPath}`;
38567
+ return {
38568
+ playable: `${prefix}/playable.json`,
38569
+ scoring: `${prefix}/scoring.json`
38570
+ };
38571
+ }
38572
+ function jsonValidationMessage(value, path, ancestors) {
38573
+ if (value === null || typeof value === "string" || typeof value === "boolean") {
38574
+ return null;
38575
+ }
38576
+ if (typeof value === "number") {
38577
+ return Number.isFinite(value) ? null : `${path} must contain a finite number`;
38578
+ }
38579
+ if (typeof value !== "object") {
38580
+ return `${path} is not JSON-serializable`;
38581
+ }
38582
+ if (ancestors.has(value)) {
38583
+ return `${path} contains a circular reference`;
38584
+ }
38585
+ const prototype = Object.getPrototypeOf(value);
38586
+ if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) {
38587
+ return `${path} must contain only plain JSON objects`;
38588
+ }
38589
+ 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)) {
38590
+ return `${path} must contain a dense JSON array`;
38591
+ }
38592
+ if (!Array.isArray(value) && Reflect.ownKeys(value).length !== Object.keys(value).length) {
38593
+ return `${path} contains non-JSON object properties`;
38594
+ }
38595
+ ancestors.add(value);
38596
+ const entries = Array.isArray(value) ? value.map((entry, index2) => [String(index2), entry]) : Object.entries(value);
38597
+ for (const [key, entry] of entries) {
38598
+ const message = jsonValidationMessage(entry, `${path}.${key}`, ancestors);
38599
+ if (message) {
38600
+ ancestors.delete(value);
38601
+ return message;
38602
+ }
38603
+ }
38604
+ ancestors.delete(value);
38605
+ return null;
38606
+ }
38607
+ function assertJsonValue(value, path) {
38608
+ const message = jsonValidationMessage(value, path, new Set);
38609
+ if (message) {
38610
+ throw new ValidationError(message);
38611
+ }
38612
+ }
38613
+ function isRecord6(value) {
38614
+ return typeof value === "object" && value !== null && !Array.isArray(value);
38615
+ }
38616
+ function hasExactlyKeys(value, keys) {
38617
+ const actual = Object.keys(value).toSorted();
38618
+ const expected = [...keys].toSorted();
38619
+ return actual.length === expected.length && actual.every((key, index2) => key === expected[index2]);
38620
+ }
38621
+ function assertAssessmentArtifactIdentity(identity) {
38622
+ if (!isRecord6(identity) || !nonEmptyIdentityValue(identity.qtiTestIdentifier)) {
38623
+ throw new ValidationError("Assessment artifact identity is invalid");
38624
+ }
38625
+ if (identity.kind === "assessment" && hasExactlyKeys(identity, ["kind", "qtiTestIdentifier", "contentRevision"]) && nonEmptyIdentityValue(identity.contentRevision)) {
38626
+ return;
38627
+ }
38628
+ if (identity.kind === "review-bank" && hasExactlyKeys(identity, [
38629
+ "kind",
38630
+ "qtiTestIdentifier",
38631
+ "sourceContentRevision",
38632
+ "bankRevision"
38633
+ ]) && nonEmptyIdentityValue(identity.sourceContentRevision) && nonEmptyIdentityValue(identity.bankRevision)) {
38634
+ return;
38635
+ }
38636
+ throw new ValidationError("Assessment artifact identity is invalid");
38637
+ }
38638
+ function envelopeWithoutChecksum({
38639
+ checksum: _checksum,
38640
+ ...content
38641
+ }) {
38642
+ return content;
38643
+ }
38644
+ async function prepareAssessmentArtifact(input) {
38645
+ assertAssessmentArtifactIdentity(input.identity);
38646
+ assertJsonValue(input.payload, "Assessment artifact payload");
38647
+ const content = {
38648
+ artifactKind: input.artifactKind,
38649
+ artifactSchemaVersion: ASSESSMENT_ARTIFACT_SCHEMA_VERSION,
38650
+ graderProtocolVersion: ASSESSMENT_GRADER_PROTOCOL_VERSION,
38651
+ identity: input.identity,
38652
+ payload: input.payload
38653
+ };
38654
+ const checksum = await sha256Hex(canonicalJson2(content));
38655
+ const envelope = {
38656
+ ...content,
38657
+ checksum: { algorithm: "sha256", value: checksum }
38658
+ };
38659
+ return {
38660
+ envelope,
38661
+ bytes: textEncoder.encode(canonicalJson2(envelope)),
38662
+ key: assessmentArtifactPairKeys(input.identity)[input.artifactKind]
38663
+ };
38664
+ }
38665
+ function parseEnvelope(value) {
38666
+ if (!isRecord6(value) || !hasExactlyKeys(value, [
38667
+ "artifactKind",
38668
+ "artifactSchemaVersion",
38669
+ "graderProtocolVersion",
38670
+ "identity",
38671
+ "checksum",
38672
+ "payload"
38673
+ ]) || value.artifactKind !== "playable" && value.artifactKind !== "scoring" || value.artifactSchemaVersion !== ASSESSMENT_ARTIFACT_SCHEMA_VERSION || value.graderProtocolVersion !== ASSESSMENT_GRADER_PROTOCOL_VERSION) {
38674
+ throw new ValidationError("Assessment artifact envelope is invalid or incompatible");
38675
+ }
38676
+ assertAssessmentArtifactIdentity(value.identity);
38677
+ if (!isRecord6(value.checksum) || !hasExactlyKeys(value.checksum, ["algorithm", "value"]) || value.checksum.algorithm !== "sha256" || typeof value.checksum.value !== "string" || !SHA256_HEX.test(value.checksum.value)) {
38678
+ throw new ValidationError("Assessment artifact checksum is invalid");
38679
+ }
38680
+ assertJsonValue(value.payload, "Assessment artifact payload");
38681
+ return value;
38682
+ }
38683
+ async function verifyAssessmentArtifactBytes(bytes) {
38684
+ let source;
38685
+ let parsed;
38686
+ try {
38687
+ source = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
38688
+ parsed = JSON.parse(source);
38689
+ } catch {
38690
+ throw new ValidationError("Assessment artifact is not valid UTF-8 JSON");
38691
+ }
38692
+ const envelope = parseEnvelope(parsed);
38693
+ const canonicalEnvelope = canonicalJson2(envelope);
38694
+ if (source !== canonicalEnvelope) {
38695
+ throw new ValidationError("Assessment artifact bytes are not canonically encoded");
38696
+ }
38697
+ const expectedChecksum = await sha256Hex(canonicalJson2(envelopeWithoutChecksum(envelope)));
38698
+ if (envelope.checksum.value !== expectedChecksum) {
38699
+ throw new ValidationError("Assessment artifact checksum does not match its contents");
38700
+ }
38701
+ return envelope;
38702
+ }
38703
+ function assessmentArtifactIdentitiesMatch(left, right) {
38704
+ return canonicalJson2(left) === canonicalJson2(right);
38705
+ }
38706
+ function assertMatchingAssessmentArtifactPair(playable, scoring, expectedIdentity) {
38707
+ 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)) {
38708
+ throw new ValidationError("Assessment artifact pair does not describe one revision");
38709
+ }
38710
+ }
38711
+ var ASSESSMENT_ARTIFACT_SCHEMA_VERSION = 1, ASSESSMENT_GRADER_PROTOCOL_VERSION = 1, ARTIFACT_KEY_PREFIX = "assessment-artifacts", SHA256_HEX, textEncoder;
38712
+ var init_assessment_artifact_envelope_util = __esm(() => {
38713
+ init_errors2();
38714
+ SHA256_HEX = /^[a-f0-9]{64}$/;
38715
+ textEncoder = new TextEncoder;
38716
+ });
38717
+
38718
+ // ../api-core/src/utils/assessment-artifact-storage.util.ts
38719
+ function requireBucketName(deps) {
38720
+ const bucketName = deps.bucketName?.trim();
38721
+ if (!bucketName) {
38722
+ throw new ServiceUnavailableError("Assessment artifact storage is not configured");
38723
+ }
38724
+ return bucketName;
38725
+ }
38726
+ function bytesEqual(left, right) {
38727
+ return left.length === right.length && left.every((byte, index2) => byte === right[index2]);
38728
+ }
38729
+ async function holdsExpectedBytes(deps, bucketName, artifact) {
38730
+ const stored = await deps.storage.getObject(bucketName, artifact.key);
38731
+ if (!stored) {
38732
+ return false;
38733
+ }
38734
+ if (!bytesEqual(stored.body, artifact.bytes)) {
38735
+ throw new ConflictError("Immutable assessment artifact key contains different bytes", {
38736
+ key: artifact.key
38737
+ });
38738
+ }
38739
+ return true;
38740
+ }
38741
+ async function ensureStored(deps, bucketName, artifact, alreadyStored) {
38742
+ const created = !alreadyStored && await deps.storage.putObjectIfAbsent(bucketName, artifact.key, artifact.bytes, {
38743
+ contentType: "application/json"
38744
+ });
38745
+ if (!await holdsExpectedBytes(deps, bucketName, artifact)) {
38746
+ throw new ServiceUnavailableError("Assessment artifact disappeared during storage", {
38747
+ key: artifact.key
38748
+ });
38749
+ }
38750
+ return created ? "created" : "existing";
38751
+ }
38752
+ async function storeAssessmentArtifactPair(deps, input) {
38753
+ const bucketName = requireBucketName(deps);
38754
+ const [playable, scoring] = await Promise.all([
38755
+ prepareAssessmentArtifact({
38756
+ artifactKind: "playable",
38757
+ identity: input.identity,
38758
+ payload: input.playablePayload
38759
+ }),
38760
+ prepareAssessmentArtifact({
38761
+ artifactKind: "scoring",
38762
+ identity: input.identity,
38763
+ payload: input.scoringPayload
38764
+ })
38765
+ ]);
38766
+ const [playableStored, scoringStored] = await Promise.all([
38767
+ holdsExpectedBytes(deps, bucketName, playable),
38768
+ holdsExpectedBytes(deps, bucketName, scoring)
38769
+ ]);
38770
+ const playableStatus = await ensureStored(deps, bucketName, playable, playableStored);
38771
+ const scoringStatus = await ensureStored(deps, bucketName, scoring, scoringStored);
38772
+ const [storedPlayable, storedScoring] = await Promise.all([
38773
+ verifyAssessmentArtifactBytes(playable.bytes),
38774
+ verifyAssessmentArtifactBytes(scoring.bytes)
38775
+ ]);
38776
+ assertMatchingAssessmentArtifactPair(storedPlayable, storedScoring, input.identity);
38777
+ return {
38778
+ playable: {
38779
+ key: playable.key,
38780
+ checksum: playable.envelope.checksum.value,
38781
+ status: playableStatus
38782
+ },
38783
+ scoring: {
38784
+ key: scoring.key,
38785
+ checksum: scoring.envelope.checksum.value,
38786
+ status: scoringStatus
38787
+ }
38788
+ };
38789
+ }
38790
+ async function loadAssessmentArtifactPair(deps, identity) {
38791
+ const bucketName = requireBucketName(deps);
38792
+ const keys = assessmentArtifactPairKeys(identity);
38793
+ const [playableObject, scoringObject] = await Promise.all([
38794
+ deps.storage.getObject(bucketName, keys.playable),
38795
+ deps.storage.getObject(bucketName, keys.scoring)
38796
+ ]);
38797
+ if (!playableObject || !scoringObject) {
38798
+ return null;
38799
+ }
38800
+ const [playable, scoring] = await Promise.all([
38801
+ verifyAssessmentArtifactBytes(playableObject.body),
38802
+ verifyAssessmentArtifactBytes(scoringObject.body)
38803
+ ]);
38804
+ assertMatchingAssessmentArtifactPair(playable, scoring, identity);
38805
+ return {
38806
+ playable,
38807
+ scoring
38808
+ };
38809
+ }
38810
+ async function loadVerifiedArtifact(deps, key) {
38811
+ const bucketName = requireBucketName(deps);
38812
+ const object = await deps.storage.getObject(bucketName, key);
38813
+ return object ? verifyAssessmentArtifactBytes(object.body) : null;
38814
+ }
38815
+ async function loadAssessmentArtifact(deps, artifactKind, identity) {
38816
+ const artifact = await loadVerifiedArtifact(deps, assessmentArtifactPairKeys(identity)[artifactKind]);
38817
+ if (artifact && (artifact.artifactKind !== artifactKind || !assessmentArtifactIdentitiesMatch(artifact.identity, identity))) {
38818
+ throw new ValidationError("Assessment artifact does not describe the requested revision");
38819
+ }
38820
+ return artifact;
38821
+ }
38822
+ async function loadManagedPlayableArtifact(deps, qtiTestIdentifier) {
38823
+ const key = assessmentArtifactPairKeys({
38824
+ kind: "assessment",
38825
+ qtiTestIdentifier,
38826
+ contentRevision: "unbound-managed-lookup"
38827
+ }).playable;
38828
+ const artifact = await loadVerifiedArtifact(deps, key);
38829
+ if (artifact && (artifact.artifactKind !== "playable" || artifact.identity.kind !== "assessment" || artifact.identity.qtiTestIdentifier !== qtiTestIdentifier)) {
38830
+ throw new ValidationError("Assessment artifact does not describe the requested assessment");
38831
+ }
38832
+ return artifact;
38833
+ }
38834
+ var init_assessment_artifact_storage_util = __esm(() => {
38835
+ init_errors2();
38836
+ init_assessment_artifact_envelope_util();
38837
+ });
38838
+
38839
+ // ../api-core/src/services/assessment-artifacts.service.ts
38840
+ class AssessmentArtifactsService {
38841
+ deps;
38842
+ constructor(deps) {
38843
+ this.deps = deps;
38844
+ }
38845
+ storePair(input) {
38846
+ return storeAssessmentArtifactPair(this.deps, input);
38847
+ }
38848
+ loadPair(identity) {
38849
+ return loadAssessmentArtifactPair(this.deps, identity);
38850
+ }
38851
+ loadPlayable(identity) {
38852
+ return loadAssessmentArtifact(this.deps, "playable", identity);
38853
+ }
38854
+ loadScoring(identity) {
38855
+ return loadAssessmentArtifact(this.deps, "scoring", identity);
38856
+ }
38857
+ loadManagedPlayable(qtiTestIdentifier) {
38858
+ return loadManagedPlayableArtifact(this.deps, qtiTestIdentifier);
38859
+ }
38860
+ }
38861
+ var init_assessment_artifacts_service = __esm(() => {
38862
+ init_assessment_artifact_storage_util();
38863
+ });
38864
+
38549
38865
  // ../../node_modules/.bun/sharp@0.34.2/node_modules/sharp/lib/is.js
38550
38866
  var require_is = __commonJS((exports, module2) => {
38551
38867
  var defined = function(val) {
@@ -47141,7 +47457,7 @@ function rejectSystemManagedReview(input, context2) {
47141
47457
  });
47142
47458
  }
47143
47459
  }
47144
- var TimebackGradeSchema, TimebackSubjectSchema, CourseGoalsSchema, UpdateGameTimebackIntegrationRequestSchema, CreateGameTimebackIntegrationRequestSchema, TimebackActivityDataSchema, EndActivityRequestSchema, GameRunMetricsSchema, GameCourseMetricsSchema, GameMetricsResponseSchema, AdvanceCourseRequestSchema, UnenrollCourseRequestSchema, HeartbeatRequestSchema, PopulateStudentRequestSchema, DerivedPlatformCourseConfigSchema, TimebackBaseConfigSchema, PlatformTimebackSetupRequestSchema, AdminTimebackMutationBaseSchema, AdminAttributionDateSchema, ADMIN_GRANT_XP_MIN = -1e5, ADMIN_GRANT_XP_MAX = 1e5, ADMIN_GRANT_XP_AMOUNT_RANGE_MESSAGE, GrantTimebackXpRequestSchema, AdjustTimebackTimeRequestSchema, AdjustTimebackMasteryRequestSchema, ReconcileMasteryForConfigChangeSchema, EnrollStudentRequestSchema, UnenrollStudentRequestSchema, ReactivateEnrollmentRequestSchema, VerifyTimebackMetricDiscrepancyRequestSchema, AssessmentPurposeSchema, AssessmentStatusSchema, AssessmentKeySchema, DiagnosticKeySchema, DiagnosticRoutingManifestSchema, DiagnosticAssessmentDefinitionInputSchema, AssessmentStandardRefSchema2, CreateAssessmentRequestSchema, UpdateAssessmentRequestSchema, CopyAssessmentRequestSchema, AssessmentAssociationImportEntrySchema, AttachExistingAssessmentsRequestSchema, ReorderAssessmentsRequestSchema, ReorderQuestionsRequestSchema;
47460
+ var TimebackGradeSchema, TimebackSubjectSchema, CourseGoalsSchema, UpdateGameTimebackIntegrationRequestSchema, CreateGameTimebackIntegrationRequestSchema, TimebackActivityDataSchema, EndActivityRequestSchema, GameRunMetricsSchema, GameCourseMetricsSchema, GameMetricsResponseSchema, AdvanceCourseRequestSchema, UnenrollCourseRequestSchema, HeartbeatRequestSchema, PopulateStudentRequestSchema, DerivedPlatformCourseConfigSchema, TimebackBaseConfigSchema, PlatformTimebackSetupRequestSchema, AdminTimebackMutationBaseSchema, AdminAttributionDateSchema, ADMIN_GRANT_XP_MIN = -1e5, ADMIN_GRANT_XP_MAX = 1e5, ADMIN_GRANT_XP_AMOUNT_RANGE_MESSAGE, GrantTimebackXpRequestSchema, AdjustTimebackTimeRequestSchema, AdjustTimebackMasteryRequestSchema, ReconcileMasteryForConfigChangeSchema, EnrollStudentRequestSchema, UnenrollStudentRequestSchema, ReactivateEnrollmentRequestSchema, VerifyTimebackMetricDiscrepancyRequestSchema, AssessmentPurposeSchema, AssessmentStatusSchema, AssessmentKeySchema, DiagnosticKeySchema, DiagnosticRoutingManifestSchema, DiagnosticAssessmentDefinitionInputSchema, AssessmentStandardRefSchema2, CreateAssessmentRequestSchema, UpdateAssessmentRequestSchema, CopyAssessmentRequestSchema, AssessmentAssociationImportEntrySchema, AttachExistingAssessmentsRequestSchema, ReorderQuestionsRequestSchema;
47145
47461
  var init_schemas4 = __esm(() => {
47146
47462
  init_esm();
47147
47463
  init_src();
@@ -47428,8 +47744,7 @@ var init_schemas4 = __esm(() => {
47428
47744
  assessmentKey: AssessmentKeySchema,
47429
47745
  qtiTestIdentifier: exports_external.string().trim().min(1, "Assessment identifier is required"),
47430
47746
  purpose: AssessmentPurposeSchema,
47431
- standard: AssessmentStandardRefSchema2.optional(),
47432
- sortOrder: exports_external.number().int().nonnegative().nullable()
47747
+ standard: AssessmentStandardRefSchema2.optional()
47433
47748
  }).superRefine((input, context2) => {
47434
47749
  requireMasteryStandard(input, context2);
47435
47750
  rejectSystemManagedReview(input, context2);
@@ -47479,10 +47794,6 @@ var init_schemas4 = __esm(() => {
47479
47794
  seenKeys.add(assessment.assessmentKey);
47480
47795
  });
47481
47796
  });
47482
- ReorderAssessmentsRequestSchema = exports_external.object({
47483
- purpose: AssessmentPurposeSchema,
47484
- testIdentifiers: exports_external.array(exports_external.string().trim().min(1, "Assessment identifier is required")).min(1, "At least one assessment is required")
47485
- }).superRefine(rejectSystemManagedReview);
47486
47797
  ReorderQuestionsRequestSchema = exports_external.object({
47487
47798
  items: exports_external.array(exports_external.object({
47488
47799
  identifier: exports_external.string().min(1),
@@ -95679,6 +95990,365 @@ function supportedQtiQuestionInteractionType(question) {
95679
95990
  return null;
95680
95991
  }
95681
95992
  }
95993
+ function childElements(node, localName2) {
95994
+ return node.children.filter((child) => child.kind === "element" && (!localName2 || child.localName === localName2));
95995
+ }
95996
+ function onlyWhitespaceText(nodes) {
95997
+ return nodes.every((node) => node.kind === "element" || node.value.trim() === "");
95998
+ }
95999
+ function onlyAttributes(node, allowed) {
96000
+ const names = Object.keys(node.attributes);
96001
+ return names.length === allowed.length && names.every((name3) => allowed.includes(name3));
96002
+ }
96003
+ function directText(node) {
96004
+ if (childElements(node).length > 0) {
96005
+ return null;
96006
+ }
96007
+ return node.children.map((child) => child.kind === "text" ? child.value : "").join("").trim();
96008
+ }
96009
+ function itemElement(root) {
96010
+ return root.localName === "qti-assessment-item" ? root : xmlDescendants(root, (candidate) => candidate.localName === "qti-assessment-item")[0];
96011
+ }
96012
+ function fallback(itemIdentifier, maxScore, reason) {
96013
+ return {
96014
+ strategy: "qti",
96015
+ itemIdentifier,
96016
+ maxScore,
96017
+ reason
96018
+ };
96019
+ }
96020
+ function parsedNumericValue(value, baseType) {
96021
+ const syntax = baseType === "integer" ? /^[+-]?\d+$/ : /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i;
96022
+ if (!syntax.test(value.trim())) {
96023
+ return null;
96024
+ }
96025
+ const numeric3 = Number(value);
96026
+ return Number.isFinite(numeric3) && (baseType !== "integer" || Number.isSafeInteger(numeric3)) ? numeric3 : null;
96027
+ }
96028
+ function numericCorrectValue(declaration) {
96029
+ if (declaration.cardinality !== "single" || declaration.correctValues.length !== 1 || declaration.baseType !== "integer" && declaration.baseType !== "float") {
96030
+ return null;
96031
+ }
96032
+ return parsedNumericValue(declaration.correctValues[0], declaration.baseType);
96033
+ }
96034
+ function declarationResult(raw, normalized) {
96035
+ const identifier = raw.attributes.identifier?.trim();
96036
+ if (!normalized || !identifier || normalized.identifier !== identifier) {
96037
+ return { reason: "invalid-correct-response" };
96038
+ }
96039
+ const cardinality = (raw.attributes.cardinality || "single").trim();
96040
+ if (!SUPPORTED_CARDINALITIES2.has(cardinality)) {
96041
+ return { reason: "unsupported-cardinality" };
96042
+ }
96043
+ const baseType = (raw.attributes["base-type"] || "string").trim();
96044
+ if (!SUPPORTED_BASE_TYPES2.has(baseType)) {
96045
+ return { reason: "unsupported-base-type" };
96046
+ }
96047
+ if (baseType === "directedPair" && cardinality !== "multiple") {
96048
+ return { reason: "unsupported-cardinality" };
96049
+ }
96050
+ const declaration = {
96051
+ identifier,
96052
+ cardinality,
96053
+ baseType,
96054
+ correctValues: [...normalized.correctValues]
96055
+ };
96056
+ if (declaration.correctValues.length === 0 || declaration.correctValues.some((value) => value.trim() === "") || declaration.cardinality === "single" && declaration.correctValues.length !== 1 && declaration.baseType !== "string") {
96057
+ return { reason: "invalid-correct-response" };
96058
+ }
96059
+ if (declaration.baseType === "directedPair" && (declaration.correctValues.some((value) => !DIRECTED_PAIR_VALUE.test(value)) || new Set(declaration.correctValues).size !== declaration.correctValues.length)) {
96060
+ return { reason: "invalid-correct-response" };
96061
+ }
96062
+ if ((declaration.baseType === "integer" || declaration.baseType === "float") && declaration.correctValues.some((value) => parsedNumericValue(value, declaration.baseType) === null)) {
96063
+ return { reason: "invalid-correct-response" };
96064
+ }
96065
+ return { declaration };
96066
+ }
96067
+ function scoringDeclarations(rawDeclarations, normalizedDeclarations) {
96068
+ const rawIdentifiers = rawDeclarations.map((declaration) => declaration.attributes.identifier?.trim());
96069
+ if (rawDeclarations.length !== normalizedDeclarations.length || rawIdentifiers.some((identifier) => !identifier) || new Set(rawIdentifiers).size !== rawIdentifiers.length) {
96070
+ return { reason: "invalid-correct-response" };
96071
+ }
96072
+ const declarations = new Map;
96073
+ for (const [index2, rawDeclaration] of rawDeclarations.entries()) {
96074
+ const normalized = normalizedDeclarations[index2];
96075
+ if (normalized?.correctValues.length) {
96076
+ const result = declarationResult(rawDeclaration, normalized);
96077
+ if ("reason" in result) {
96078
+ return result;
96079
+ }
96080
+ if (declarations.has(result.declaration.identifier)) {
96081
+ return { reason: "invalid-correct-response" };
96082
+ }
96083
+ declarations.set(result.declaration.identifier, result.declaration);
96084
+ }
96085
+ }
96086
+ return declarations.size > 0 ? { declarations } : { reason: "no-scorable-response" };
96087
+ }
96088
+ function declarationConstructReason(declarations) {
96089
+ if (declarations.some((declaration) => childElements(declaration, "qti-mapping").length > 0)) {
96090
+ return "response-mapping";
96091
+ }
96092
+ return declarations.some((declaration) => childElements(declaration, "qti-area-mapping").length > 0) ? "area-mapping" : null;
96093
+ }
96094
+ function sameScore(left, right) {
96095
+ return Math.abs(left - right) <= Number.EPSILON * Math.max(1, Math.abs(left), Math.abs(right));
96096
+ }
96097
+ function platformResult(itemIdentifier, parsedMaxScore, rules) {
96098
+ const ruleMaximum = rules.reduce((sum, rule) => sum + rule.points, 0);
96099
+ if (!Number.isFinite(ruleMaximum) || ruleMaximum <= 0) {
96100
+ return fallback(itemIdentifier, parsedMaxScore, "no-scorable-response");
96101
+ }
96102
+ if (!sameScore(parsedMaxScore, ruleMaximum)) {
96103
+ return fallback(itemIdentifier, parsedMaxScore, "inconsistent-max-score");
96104
+ }
96105
+ return {
96106
+ strategy: "platform",
96107
+ itemIdentifier,
96108
+ maxScore: ruleMaximum,
96109
+ rules
96110
+ };
96111
+ }
96112
+ function matchRule(declaration, points) {
96113
+ return {
96114
+ responseIdentifier: declaration.identifier,
96115
+ cardinality: declaration.cardinality,
96116
+ baseType: declaration.baseType,
96117
+ points,
96118
+ comparison: {
96119
+ kind: "match",
96120
+ correctValues: [...declaration.correctValues]
96121
+ }
96122
+ };
96123
+ }
96124
+ function operandIdentifiers(comparison) {
96125
+ const operands = childElements(comparison);
96126
+ if (operands.length !== 2 || !onlyWhitespaceText(comparison.children) || operands.some((operand) => !onlyAttributes(operand, ["identifier"]))) {
96127
+ return null;
96128
+ }
96129
+ const variable = operands.find((operand) => operand.localName === "qti-variable");
96130
+ const correct = operands.find((operand) => operand.localName === "qti-correct");
96131
+ if (!variable || !correct || childElements(variable).length > 0 || childElements(correct).length > 0) {
96132
+ return null;
96133
+ }
96134
+ if (!onlyWhitespaceText(variable.children) || !onlyWhitespaceText(correct.children)) {
96135
+ return null;
96136
+ }
96137
+ const variableIdentifier = variable.attributes.identifier?.trim();
96138
+ const correctIdentifier = correct.attributes.identifier?.trim();
96139
+ return variableIdentifier && correctIdentifier ? { variable: variableIdentifier, correct: correctIdentifier } : null;
96140
+ }
96141
+ function comparisonResult(comparison, declaration) {
96142
+ const identifiers = operandIdentifiers(comparison);
96143
+ if (!identifiers || identifiers.variable !== declaration.identifier || identifiers.correct !== declaration.identifier) {
96144
+ return null;
96145
+ }
96146
+ if (comparison.localName === "qti-match" && onlyAttributes(comparison, [])) {
96147
+ return {
96148
+ kind: "match",
96149
+ correctValues: [...declaration.correctValues]
96150
+ };
96151
+ }
96152
+ const correctValue = numericCorrectValue(declaration);
96153
+ if (correctValue === null) {
96154
+ return null;
96155
+ }
96156
+ if (comparison.localName === "qti-equal" && onlyAttributes(comparison, [])) {
96157
+ return { kind: "numeric-equal", correctValue };
96158
+ }
96159
+ if (comparison.localName !== "qti-equal-rounded" || !onlyAttributes(comparison, ["rounding-mode", "figures"])) {
96160
+ return null;
96161
+ }
96162
+ const roundingMode = comparison.attributes["rounding-mode"];
96163
+ const figures = Number(comparison.attributes.figures);
96164
+ const validFigures = Number.isInteger(figures) && (roundingMode === "decimalPlaces" ? figures >= 0 : figures > 0);
96165
+ if (roundingMode !== "decimalPlaces" && roundingMode !== "significantFigures" || !validFigures) {
96166
+ return null;
96167
+ }
96168
+ return {
96169
+ kind: "numeric-equal-rounded",
96170
+ correctValue,
96171
+ roundingMode,
96172
+ figures
96173
+ };
96174
+ }
96175
+ function responseReferences(node, responseIdentifiers) {
96176
+ 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)));
96177
+ }
96178
+ function additivePoints(setOutcome) {
96179
+ if (!onlyAttributes(setOutcome, ["identifier"]) || setOutcome.attributes.identifier !== "SCORE" || !onlyWhitespaceText(setOutcome.children)) {
96180
+ return null;
96181
+ }
96182
+ const outcomeChildren = childElements(setOutcome);
96183
+ if (outcomeChildren.length !== 1 || outcomeChildren[0]?.localName !== "qti-sum") {
96184
+ return null;
96185
+ }
96186
+ const sum = outcomeChildren[0];
96187
+ if (!onlyAttributes(sum, []) || !onlyWhitespaceText(sum.children)) {
96188
+ return null;
96189
+ }
96190
+ const operands = childElements(sum);
96191
+ const scoreVariable = operands.find((operand) => operand.localName === "qti-variable" && operand.attributes.identifier === "SCORE");
96192
+ const pointValue = operands.find((operand) => operand.localName === "qti-base-value");
96193
+ 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") {
96194
+ return null;
96195
+ }
96196
+ const rawPoints = directText(pointValue);
96197
+ const points = rawPoints === null ? Number.NaN : Number(rawPoints);
96198
+ return Number.isFinite(points) && points > 0 ? points : null;
96199
+ }
96200
+ function additiveRule(condition, declarations) {
96201
+ const responseIdentifiers = new Set(declarations.keys());
96202
+ const references = responseReferences(condition, responseIdentifiers);
96203
+ if (references.size > 1) {
96204
+ return { reason: "cross-response-processing" };
96205
+ }
96206
+ if (references.size !== 1 || !onlyAttributes(condition, []) || !onlyWhitespaceText(condition.children)) {
96207
+ return { reason: "unsupported-response-processing" };
96208
+ }
96209
+ const conditionChildren = childElements(condition);
96210
+ if (conditionChildren.length !== 1 || conditionChildren[0]?.localName !== "qti-response-if") {
96211
+ return { reason: "unsupported-response-processing" };
96212
+ }
96213
+ const responseIf = conditionChildren[0];
96214
+ if (!onlyAttributes(responseIf, []) || !onlyWhitespaceText(responseIf.children)) {
96215
+ return { reason: "unsupported-response-processing" };
96216
+ }
96217
+ const responseIfChildren = childElements(responseIf);
96218
+ if (responseIfChildren.length !== 2) {
96219
+ return { reason: "unsupported-response-processing" };
96220
+ }
96221
+ const [comparison, setOutcome] = responseIfChildren;
96222
+ if (!comparison || !setOutcome || !["qti-match", "qti-equal", "qti-equal-rounded"].includes(comparison.localName) || setOutcome.localName !== "qti-set-outcome-value") {
96223
+ return { reason: "unsupported-response-processing" };
96224
+ }
96225
+ const responseIdentifier = [...references][0];
96226
+ const declaration = declarations.get(responseIdentifier);
96227
+ const points = additivePoints(setOutcome);
96228
+ const comparisonPayload = declaration ? comparisonResult(comparison, declaration) : null;
96229
+ if (!declaration || points === null || !comparisonPayload) {
96230
+ return { reason: "unsupported-response-processing" };
96231
+ }
96232
+ return {
96233
+ rule: {
96234
+ responseIdentifier,
96235
+ cardinality: declaration.cardinality,
96236
+ baseType: declaration.baseType,
96237
+ points,
96238
+ comparison: comparisonPayload
96239
+ }
96240
+ };
96241
+ }
96242
+ function additiveRules(processingRules, declarations) {
96243
+ const rules = [];
96244
+ const scored = new Set;
96245
+ for (const condition of processingRules) {
96246
+ const result = additiveRule(condition, declarations);
96247
+ if ("reason" in result) {
96248
+ return result;
96249
+ }
96250
+ if (scored.has(result.rule.responseIdentifier)) {
96251
+ return { reason: "unsupported-response-processing" };
96252
+ }
96253
+ scored.add(result.rule.responseIdentifier);
96254
+ rules.push(result.rule);
96255
+ }
96256
+ if (scored.size !== declarations.size || [...declarations.keys()].some((identifier) => !scored.has(identifier))) {
96257
+ return { reason: "unsupported-response-processing" };
96258
+ }
96259
+ return { rules };
96260
+ }
96261
+ function scoreDefaultIsZero(item) {
96262
+ const scoreOutcomes = childElements(item, "qti-outcome-declaration").filter((outcome) => outcome.attributes.identifier === "SCORE");
96263
+ if (scoreOutcomes.length !== 1) {
96264
+ return false;
96265
+ }
96266
+ const scoreOutcome = scoreOutcomes[0];
96267
+ if (!onlyAttributes(scoreOutcome, ["identifier", "cardinality", "base-type"]) || scoreOutcome.attributes.cardinality !== "single" || scoreOutcome.attributes["base-type"] !== "float") {
96268
+ return false;
96269
+ }
96270
+ const defaults = childElements(scoreOutcome, "qti-default-value");
96271
+ if (defaults.length !== 1) {
96272
+ return false;
96273
+ }
96274
+ const values = childElements(defaults[0], "qti-value");
96275
+ const defaultText = values.length === 1 ? directText(values[0]) : null;
96276
+ return defaultText !== null && defaultText !== "" && Number(defaultText) === 0;
96277
+ }
96278
+ function compileResponseProcessing(item, itemIdentifier, maxScore, declarations) {
96279
+ const processingElements = childElements(item, "qti-response-processing");
96280
+ if (processingElements.length === 0) {
96281
+ return platformResult(itemIdentifier, maxScore, [...declarations.values()].map((declaration) => matchRule(declaration, 1)));
96282
+ }
96283
+ if (processingElements.length !== 1) {
96284
+ return fallback(itemIdentifier, maxScore, "unsupported-response-processing");
96285
+ }
96286
+ const processing = processingElements[0];
96287
+ const template = processing.attributes.template;
96288
+ const processingRules = childElements(processing);
96289
+ if (template !== undefined) {
96290
+ if (!MATCH_CORRECT_TEMPLATES2.has(template.trim())) {
96291
+ return fallback(itemIdentifier, maxScore, "unsupported-processing-template");
96292
+ }
96293
+ if (processingRules.length > 0 || !onlyWhitespaceText(processing.children) || !onlyAttributes(processing, ["template"])) {
96294
+ return fallback(itemIdentifier, maxScore, "unsupported-response-processing");
96295
+ }
96296
+ const response = declarations.get("RESPONSE");
96297
+ return response && declarations.size === 1 ? platformResult(itemIdentifier, maxScore, [matchRule(response, 1)]) : fallback(itemIdentifier, maxScore, "unsupported-response-processing");
96298
+ }
96299
+ if (!onlyAttributes(processing, []) || !onlyWhitespaceText(processing.children) || processingRules.length === 0 || processingRules.some((rule) => rule.localName !== "qti-response-condition") || !scoreDefaultIsZero(item)) {
96300
+ return fallback(itemIdentifier, maxScore, "unsupported-response-processing");
96301
+ }
96302
+ const compiled = additiveRules(processingRules, declarations);
96303
+ return "reason" in compiled ? fallback(itemIdentifier, maxScore, compiled.reason) : platformResult(itemIdentifier, maxScore, compiled.rules);
96304
+ }
96305
+ function compileQtiScoringArtifactItem(rawXml) {
96306
+ let root;
96307
+ let parsed;
96308
+ try {
96309
+ root = parseXmlDocument(rawXml);
96310
+ parsed = parseQtiItemXml(rawXml);
96311
+ } catch {
96312
+ throw new QtiScoringArtifactValidationError("QTI assessment item contains invalid XML");
96313
+ }
96314
+ const item = itemElement(root);
96315
+ if (!item) {
96316
+ throw new QtiScoringArtifactValidationError("QTI XML is missing a qti-assessment-item document element");
96317
+ }
96318
+ const itemIdentifier = parsed.identifier.trim();
96319
+ if (!itemIdentifier) {
96320
+ throw new QtiScoringArtifactValidationError("QTI assessment item is missing an identifier");
96321
+ }
96322
+ const maxScore = qtiItemMaxScore(parsed);
96323
+ const rawDeclarations = childElements(item, "qti-response-declaration");
96324
+ const constructReason = declarationConstructReason(rawDeclarations);
96325
+ if (constructReason) {
96326
+ return fallback(itemIdentifier, maxScore, constructReason);
96327
+ }
96328
+ if (childElements(item, "qti-template-processing").length > 0 || childElements(item, "qti-template-declaration").length > 0) {
96329
+ return fallback(itemIdentifier, maxScore, "unsupported-response-processing");
96330
+ }
96331
+ const compiledDeclarations = scoringDeclarations(rawDeclarations, parsed.responseDeclarations);
96332
+ return "reason" in compiledDeclarations ? fallback(itemIdentifier, maxScore, compiledDeclarations.reason) : compileResponseProcessing(item, itemIdentifier, maxScore, compiledDeclarations.declarations);
96333
+ }
96334
+ function compileQtiScoringArtifact(questions) {
96335
+ const identifiers = new Set;
96336
+ const items = questions.questions.map(({ question, reference }) => {
96337
+ if (!question.rawXml) {
96338
+ throw new QtiScoringArtifactValidationError(`QTI question ${reference.identifier} is missing raw XML`);
96339
+ }
96340
+ const item = compileQtiScoringArtifactItem(question.rawXml);
96341
+ if (item.itemIdentifier !== question.identifier) {
96342
+ throw new QtiScoringArtifactValidationError(`Hydrated QTI question ${question.identifier} contains item ${item.itemIdentifier}`);
96343
+ }
96344
+ if (identifiers.has(item.itemIdentifier)) {
96345
+ throw new QtiScoringArtifactValidationError(`QTI questions contain duplicate item ${item.itemIdentifier}`);
96346
+ }
96347
+ identifiers.add(item.itemIdentifier);
96348
+ return item;
96349
+ });
96350
+ return { items };
96351
+ }
95682
96352
  function isRecord22(value) {
95683
96353
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
95684
96354
  }
@@ -95834,7 +96504,7 @@ function qtiStandardsFromMetadata(metadata2) {
95834
96504
  standards.push(...qtiAlignmentStandardsFromMetadata2(metadata2));
95835
96505
  return dedupeStandards2(standards);
95836
96506
  }
95837
- var EVENT_HANDLER_ATTRIBUTE, SCRIPT_SCHEMES2, DATA_RASTER_IMAGE_PATTERN2, QTI_MATHML_ALLOWED_TAGS2, metadataSymbol2, parser2, SUPPORTED_QTI_INTERACTION_TYPES2, URL_ATTRIBUTES3, IMAGE_URL_ATTRIBUTES3, INTERACTION_TYPES2, XML_NAMED_ENTITIES, EMPHASIS_TAGS2, STRONG_TAGS2, CONTENT_SKIPPED_TAGS2, BLOCK_CONTENT_KINDS2, GAP_MATCH_TOKEN_TAGS2, STAGE_GRAPHIC_EXCLUDED_CONTAINERS2, BLANK_SENTINEL2 = "￿", MARKED_BLANK_PATTERN2, MARKED_BLANK_GLOBAL_PATTERN2, REGION_SHAPE_COORDS, QTI_NAMESPACE = "http://www.imsglobal.org/xsd/imsqtiasi_v3p0", QTI_AUTHORING_BLANK_MARKER = "___", MATCH_CORRECT_TEMPLATE2 = "http://www.imsglobal.org/question/qti_v3p0/rptemplates/match_correct", MAP_RESPONSE_TEMPLATE2 = "https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/map_response", MAP_RESPONSE_TEMPLATES2, RESPONSE_PROCESSING_TEMPLATES2, GLOBAL_BODY_ATTRIBUTES, BODY_ELEMENT_ATTRIBUTES, MATHML_ATTRIBUTES, VOID_BODY_ELEMENTS2, URL_ATTRIBUTES22, IMAGE_URL_ATTRIBUTES22, INTERACTION_ATTRIBUTES, CHOICE_ATTRIBUTES, CHOICE_ELEMENT_ATTRIBUTES, VOID_CHOICE_ELEMENTS2, NUMERIC_COMPARISON_ATTRIBUTES2, RESPONSE_CARDINALITIES2, RESPONSE_BASE_TYPES2, BLANK_SENTINEL22 = "￿", PLAYABLE_POINT_VALUE, POINT_INTERACTION_TYPES2, INDEPENDENT_PROCESSING_TAGS2, COMMON_CORE_MATH_FRAMEWORK_ALIASES2, COMMON_CORE_ELA_FRAMEWORK_ALIASES2, COMMON_CORE_FRAMEWORK_ALIASES2;
96507
+ var EVENT_HANDLER_ATTRIBUTE, SCRIPT_SCHEMES2, DATA_RASTER_IMAGE_PATTERN2, QTI_MATHML_ALLOWED_TAGS2, metadataSymbol2, parser2, SUPPORTED_QTI_INTERACTION_TYPES2, URL_ATTRIBUTES3, IMAGE_URL_ATTRIBUTES3, INTERACTION_TYPES2, XML_NAMED_ENTITIES, EMPHASIS_TAGS2, STRONG_TAGS2, CONTENT_SKIPPED_TAGS2, BLOCK_CONTENT_KINDS2, GAP_MATCH_TOKEN_TAGS2, STAGE_GRAPHIC_EXCLUDED_CONTAINERS2, BLANK_SENTINEL2 = "￿", MARKED_BLANK_PATTERN2, MARKED_BLANK_GLOBAL_PATTERN2, REGION_SHAPE_COORDS, QTI_NAMESPACE = "http://www.imsglobal.org/xsd/imsqtiasi_v3p0", QTI_AUTHORING_BLANK_MARKER = "___", MATCH_CORRECT_TEMPLATE2 = "http://www.imsglobal.org/question/qti_v3p0/rptemplates/match_correct", MAP_RESPONSE_TEMPLATE2 = "https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/map_response", MAP_RESPONSE_TEMPLATES2, RESPONSE_PROCESSING_TEMPLATES2, GLOBAL_BODY_ATTRIBUTES, BODY_ELEMENT_ATTRIBUTES, MATHML_ATTRIBUTES, VOID_BODY_ELEMENTS2, URL_ATTRIBUTES22, IMAGE_URL_ATTRIBUTES22, INTERACTION_ATTRIBUTES, CHOICE_ATTRIBUTES, CHOICE_ELEMENT_ATTRIBUTES, VOID_CHOICE_ELEMENTS2, NUMERIC_COMPARISON_ATTRIBUTES2, RESPONSE_CARDINALITIES2, RESPONSE_BASE_TYPES2, BLANK_SENTINEL22 = "￿", PLAYABLE_POINT_VALUE, POINT_INTERACTION_TYPES2, INDEPENDENT_PROCESSING_TAGS2, SUPPORTED_BASE_TYPES2, SUPPORTED_CARDINALITIES2, MATCH_CORRECT_TEMPLATES2, DIRECTED_PAIR_VALUE, QtiScoringArtifactValidationError, COMMON_CORE_MATH_FRAMEWORK_ALIASES2, COMMON_CORE_ELA_FRAMEWORK_ALIASES2, COMMON_CORE_FRAMEWORK_ALIASES2;
95838
96508
  var init_qti = __esm(() => {
95839
96509
  init_timeback3();
95840
96510
  init_timeback3();
@@ -96084,6 +96754,30 @@ var init_qti = __esm(() => {
96084
96754
  "qti-sum",
96085
96755
  "qti-base-value"
96086
96756
  ]);
96757
+ SUPPORTED_BASE_TYPES2 = new Set([
96758
+ "string",
96759
+ "identifier",
96760
+ "integer",
96761
+ "float",
96762
+ "directedPair"
96763
+ ]);
96764
+ SUPPORTED_CARDINALITIES2 = new Set([
96765
+ "single",
96766
+ "multiple",
96767
+ "ordered"
96768
+ ]);
96769
+ MATCH_CORRECT_TEMPLATES2 = new Set([
96770
+ "http://www.imsglobal.org/question/qti_v3p0/rptemplates/match_correct",
96771
+ "https://www.imsglobal.org/question/qti_v3p0/rptemplates/match_correct",
96772
+ "https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/match_correct"
96773
+ ].flatMap((template) => [template, `${template}.xml`]));
96774
+ DIRECTED_PAIR_VALUE = /^[A-Za-z_][\w.-]* [A-Za-z_][\w.-]*$/;
96775
+ QtiScoringArtifactValidationError = class QtiScoringArtifactValidationError extends Error {
96776
+ constructor(message) {
96777
+ super(message);
96778
+ this.name = "QtiScoringArtifactValidationError";
96779
+ }
96780
+ };
96087
96781
  COMMON_CORE_MATH_FRAMEWORK_ALIASES2 = new Set([
96088
96782
  "ccssm",
96089
96783
  "ccssmath",
@@ -96109,11 +96803,11 @@ var init_qti = __esm(() => {
96109
96803
  });
96110
96804
 
96111
96805
  // ../api-core/src/utils/timeback-grade-level-results.util.ts
96112
- function isRecord6(value) {
96806
+ function isRecord7(value) {
96113
96807
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
96114
96808
  }
96115
96809
  function metadataOf(record3) {
96116
- return isRecord6(record3) && isRecord6(record3.metadata) ? record3.metadata : {};
96810
+ return isRecord7(record3) && isRecord7(record3.metadata) ? record3.metadata : {};
96117
96811
  }
96118
96812
  function stringField3(value) {
96119
96813
  return typeof value === "string" ? value : "";
@@ -96125,7 +96819,7 @@ function numberField(value) {
96125
96819
  return typeof value === "number" && Number.isFinite(value) ? value : null;
96126
96820
  }
96127
96821
  function sourceIdFromRef(value) {
96128
- return isRecord6(value) ? stringField3(value.sourcedId) : "";
96822
+ return isRecord7(value) ? stringField3(value.sourcedId) : "";
96129
96823
  }
96130
96824
  function buildGradeLevelTestResultWhere(options) {
96131
96825
  return {
@@ -96201,7 +96895,7 @@ function mapGradeLevelTestSummary(result, options) {
96201
96895
  };
96202
96896
  }
96203
96897
  function readLearningObjectiveSets(record3) {
96204
- if (!isRecord6(record3)) {
96898
+ if (!isRecord7(record3)) {
96205
96899
  return [];
96206
96900
  }
96207
96901
  const metadata2 = metadataOf(record3);
@@ -96231,7 +96925,7 @@ function dedupeStandards3(standards) {
96231
96925
  function standardsFromLineItem(lineItem) {
96232
96926
  const standards = [];
96233
96927
  for (const set3 of readLearningObjectiveSets(lineItem)) {
96234
- if (isRecord6(set3)) {
96928
+ if (isRecord7(set3)) {
96235
96929
  const source = stringField3(set3.source) || "OneRoster";
96236
96930
  const ids = Array.isArray(set3.learningObjectiveIds) ? set3.learningObjectiveIds : [];
96237
96931
  for (const id of ids) {
@@ -96247,11 +96941,11 @@ function standardsFromLineItem(lineItem) {
96247
96941
  function standardsFromResult(result) {
96248
96942
  const standards = [];
96249
96943
  for (const set3 of readLearningObjectiveSets(result)) {
96250
- if (isRecord6(set3)) {
96944
+ if (isRecord7(set3)) {
96251
96945
  const source = stringField3(set3.source) || "OneRoster";
96252
96946
  const values = Array.isArray(set3.learningObjectiveResults) ? set3.learningObjectiveResults : [];
96253
96947
  for (const item of values) {
96254
- if (isRecord6(item)) {
96948
+ if (isRecord7(item)) {
96255
96949
  const id = stringField3(item.learningObjectiveId).trim();
96256
96950
  if (id) {
96257
96951
  standards.push({ source, id });
@@ -96263,7 +96957,7 @@ function standardsFromResult(result) {
96263
96957
  return dedupeStandards3(standards);
96264
96958
  }
96265
96959
  function standardsFromQtiQuestion(questionRef) {
96266
- const question = isRecord6(questionRef) && isRecord6(questionRef.question) ? questionRef.question : null;
96960
+ const question = isRecord7(questionRef) && isRecord7(questionRef.question) ? questionRef.question : null;
96267
96961
  return qtiStandardsFromMetadata(metadataOf(question));
96268
96962
  }
96269
96963
  function enrichStandardsFromQti(standards, qtiStandards) {
@@ -96400,8 +97094,8 @@ function mapGradeLevelReviewQuestions(options) {
96400
97094
  return options.questionLineItems.toSorted((a, b) => naturalTitleSort.compare(a.title, b.title)).map((lineItem) => {
96401
97095
  const result = options.latestResultsByLineItem.get(lineItem.sourcedId ?? "");
96402
97096
  const resultMetadata = metadataOf(result);
96403
- const attemptedQuestionData = isRecord6(resultMetadata.attemptedQuestionData) ? resultMetadata.attemptedQuestionData : null;
96404
- const attemptedResult = isRecord6(attemptedQuestionData?.result) ? attemptedQuestionData.result : null;
97097
+ const attemptedQuestionData = isRecord7(resultMetadata.attemptedQuestionData) ? resultMetadata.attemptedQuestionData : null;
97098
+ const attemptedResult = isRecord7(attemptedQuestionData?.result) ? attemptedQuestionData.result : null;
96405
97099
  const index2 = parseQuestionIndex(lineItem.title);
96406
97100
  const qtiQuestion = index2 ? qtiQuestionByIndex.get(index2) ?? options.qtiQuestions[index2 - 1] : undefined;
96407
97101
  const qtiStandards = standardsFromQtiQuestion(qtiQuestion);
@@ -98444,6 +99138,452 @@ var init_timeback_admin_service = __esm(async () => {
98444
99138
  ]);
98445
99139
  });
98446
99140
 
99141
+ // ../api-core/src/utils/assessment-artifact-runtime.util.ts
99142
+ function normalizedKey(key) {
99143
+ return key.replaceAll(/[^a-z0-9]/gi, "").toLowerCase();
99144
+ }
99145
+ function containsPrivatePlayableKey(value) {
99146
+ if (Array.isArray(value)) {
99147
+ return value.some(containsPrivatePlayableKey);
99148
+ }
99149
+ if (!isRecord(value)) {
99150
+ return false;
99151
+ }
99152
+ return Object.entries(value).some(([key, entry]) => PRIVATE_PLAYABLE_KEYS.has(normalizedKey(key)) || containsPrivatePlayableKey(entry));
99153
+ }
99154
+ function playableSchema(identity) {
99155
+ return exports_external.object({
99156
+ contractVersion: exports_external.literal(PLAYCADEMY_ASSESSMENT_CONTRACT_VERSION),
99157
+ identifier: exports_external.literal(identity.qtiTestIdentifier),
99158
+ contentRevision: exports_external.literal(identity.kind === "assessment" ? identity.contentRevision : identity.sourceContentRevision),
99159
+ title: trimmedNonEmptyString,
99160
+ items: exports_external.array(playableItemSchema)
99161
+ }).strict();
99162
+ }
99163
+ function scoringSchema(identity) {
99164
+ if (identity.kind === "assessment") {
99165
+ return exports_external.object({
99166
+ items: uniqueScoringItemsSchema,
99167
+ reviewBank: exports_external.undefined(),
99168
+ diagnostic: diagnosticContextSchema.optional()
99169
+ });
99170
+ }
99171
+ return exports_external.object({
99172
+ items: uniqueScoringItemsSchema,
99173
+ reviewBank: exports_external.object({
99174
+ sourceContentRevision: exports_external.literal(identity.sourceContentRevision),
99175
+ bankRevision: exports_external.literal(identity.bankRevision),
99176
+ items: exports_external.array(exports_external.object({
99177
+ itemIdentifier: trimmedNonEmptyString,
99178
+ standards: exports_external.array(exports_external.unknown())
99179
+ }))
99180
+ }),
99181
+ diagnostic: exports_external.undefined()
99182
+ }).refine((payload) => payload.reviewBank.items.length === payload.items.length && payload.reviewBank.items.every((item, index2) => item.itemIdentifier === payload.items[index2]?.itemIdentifier));
99183
+ }
99184
+ function assertPlayableAssessmentArtifact(payload, identity) {
99185
+ if (!playableSchema(identity).safeParse(payload).success || containsPrivatePlayableKey(payload)) {
99186
+ throw new ValidationError("Playable assessment artifact payload is invalid or private");
99187
+ }
99188
+ }
99189
+ function assertScoringAssessmentArtifact(payload, identity) {
99190
+ if (!scoringSchema(identity).safeParse(payload).success) {
99191
+ throw new ValidationError("Scoring assessment artifact payload is invalid or incompatible");
99192
+ }
99193
+ }
99194
+ function assertScoringArtifactMatchesPlayable(assessment, scoring, identity) {
99195
+ const scoringByIdentifier = new Map(scoring.items.map((item) => [item.itemIdentifier, item.maxScore]));
99196
+ const playableMatches = assessment.items.every((item) => scoringByIdentifier.get(item.identifier) === item.maxScore);
99197
+ const hasExactManagedMembership = identity.kind === "review-bank" || scoring.items.length === assessment.items.length;
99198
+ if (!playableMatches || !hasExactManagedMembership) {
99199
+ throw new ValidationError("Scoring artifact does not match the playable assessment");
99200
+ }
99201
+ }
99202
+ function assertPlayableArtifactContainsItems(assessment, itemIdentifiers) {
99203
+ const available = new Set(assessment.items.map((item) => item.identifier));
99204
+ const missing = [...new Set(itemIdentifiers)].filter((identifier) => !available.has(identifier));
99205
+ if (missing.length > 0) {
99206
+ throw new ValidationError("Playable assessment artifact is missing required items", {
99207
+ missingItemIdentifiers: missing
99208
+ });
99209
+ }
99210
+ }
99211
+ function assessmentArtifactIdentityFromAttempt(metadata2) {
99212
+ return metadata2.purpose === "review" ? {
99213
+ kind: "review-bank",
99214
+ qtiTestIdentifier: metadata2.selectedTest.identifier,
99215
+ sourceContentRevision: metadata2.selectedTest.contentRevision,
99216
+ bankRevision: metadata2.review.bankRevision
99217
+ } : {
99218
+ kind: "assessment",
99219
+ qtiTestIdentifier: metadata2.selectedTest.identifier,
99220
+ contentRevision: metadata2.selectedTest.contentRevision
99221
+ };
99222
+ }
99223
+ function pinnedReviewBankFromAttempt(metadata2, bankIdentifier) {
99224
+ return {
99225
+ bankIdentifier,
99226
+ sourceContentRevision: metadata2.selectedTest.contentRevision,
99227
+ bankRevision: metadata2.review.bankRevision,
99228
+ items: metadata2.review.selections.map((selection) => ({
99229
+ itemIdentifier: selection.itemIdentifier,
99230
+ standards: [selection.standard]
99231
+ }))
99232
+ };
99233
+ }
99234
+ var PRIVATE_PLAYABLE_KEYS, trimmedNonEmptyString, finiteNumber2, nonnegativeInteger, playableContentNodesSchema, playableChoiceSchema, playableMatchChoiceSchema, playableGraphicSchema, playableHotspotSchema, playableGapTextSchema, playableGapImageSchema, playableInteractionBase, playableInteractionSchema, playableItemSchema, scoringRuleSchema, scoringItemSchema, uniqueScoringItemsSchema, diagnosticContextSchema;
99235
+ var init_assessment_artifact_runtime_util = __esm(() => {
99236
+ init_esm();
99237
+ init_src();
99238
+ init_errors2();
99239
+ PRIVATE_PLAYABLE_KEYS = new Set([
99240
+ "answer",
99241
+ "answerkey",
99242
+ "correctanswer",
99243
+ "correctresponse",
99244
+ "correctresponses",
99245
+ "correct",
99246
+ "correctvalue",
99247
+ "correctvalues",
99248
+ "iscorrect",
99249
+ "responseprocessing",
99250
+ "rule",
99251
+ "rules",
99252
+ "scoring",
99253
+ "scoringrules",
99254
+ "solution",
99255
+ "expected",
99256
+ "expectedanswer",
99257
+ "expectedvalue",
99258
+ "expectedvalues"
99259
+ ]);
99260
+ trimmedNonEmptyString = exports_external.string().min(1).refine((value) => value === value.trim());
99261
+ finiteNumber2 = exports_external.number().finite();
99262
+ nonnegativeInteger = exports_external.number().int().nonnegative();
99263
+ playableContentNodesSchema = exports_external.array(exports_external.unknown());
99264
+ playableChoiceSchema = exports_external.object({
99265
+ identifier: trimmedNonEmptyString,
99266
+ content: exports_external.string(),
99267
+ contentNodes: playableContentNodesSchema.optional()
99268
+ }).strict();
99269
+ playableMatchChoiceSchema = exports_external.object({
99270
+ identifier: trimmedNonEmptyString,
99271
+ content: exports_external.string(),
99272
+ contentNodes: playableContentNodesSchema.optional(),
99273
+ matchMax: nonnegativeInteger
99274
+ }).strict();
99275
+ playableGraphicSchema = exports_external.object({
99276
+ src: trimmedNonEmptyString,
99277
+ width: finiteNumber2.optional(),
99278
+ height: finiteNumber2.optional(),
99279
+ description: exports_external.string().optional()
99280
+ }).strict();
99281
+ playableHotspotSchema = exports_external.object({
99282
+ identifier: trimmedNonEmptyString,
99283
+ shape: exports_external.enum(["circle", "ellipse", "rect", "poly", "default"]),
99284
+ coords: exports_external.array(finiteNumber2),
99285
+ matchMax: nonnegativeInteger.optional(),
99286
+ description: exports_external.string().optional()
99287
+ }).strict();
99288
+ playableGapTextSchema = exports_external.object({
99289
+ identifier: trimmedNonEmptyString,
99290
+ content: exports_external.string(),
99291
+ matchMax: nonnegativeInteger
99292
+ }).strict();
99293
+ playableGapImageSchema = exports_external.object({
99294
+ identifier: trimmedNonEmptyString,
99295
+ image: playableGraphicSchema.optional(),
99296
+ matchMax: nonnegativeInteger,
99297
+ description: exports_external.string().optional()
99298
+ }).strict();
99299
+ playableInteractionBase = {
99300
+ responseIdentifier: trimmedNonEmptyString,
99301
+ prompt: exports_external.string().optional()
99302
+ };
99303
+ playableInteractionSchema = exports_external.discriminatedUnion("type", [
99304
+ exports_external.object({
99305
+ ...playableInteractionBase,
99306
+ type: exports_external.literal("choice"),
99307
+ choices: exports_external.array(playableChoiceSchema),
99308
+ minChoices: nonnegativeInteger,
99309
+ maxChoices: nonnegativeInteger
99310
+ }).strict(),
99311
+ exports_external.object({
99312
+ ...playableInteractionBase,
99313
+ type: exports_external.literal("inline-choice"),
99314
+ choices: exports_external.array(playableChoiceSchema)
99315
+ }).strict(),
99316
+ exports_external.object({
99317
+ ...playableInteractionBase,
99318
+ type: exports_external.literal("text-entry"),
99319
+ expectedLength: nonnegativeInteger.optional(),
99320
+ placeholder: exports_external.string().optional()
99321
+ }).strict(),
99322
+ exports_external.object({
99323
+ ...playableInteractionBase,
99324
+ type: exports_external.literal("order"),
99325
+ choices: exports_external.array(playableChoiceSchema),
99326
+ shuffle: exports_external.boolean()
99327
+ }).strict(),
99328
+ exports_external.object({
99329
+ ...playableInteractionBase,
99330
+ type: exports_external.literal("match"),
99331
+ sourceChoices: exports_external.array(playableMatchChoiceSchema),
99332
+ targetChoices: exports_external.array(playableMatchChoiceSchema),
99333
+ maxAssociations: nonnegativeInteger,
99334
+ shuffle: exports_external.boolean()
99335
+ }).strict(),
99336
+ exports_external.object({
99337
+ ...playableInteractionBase,
99338
+ type: exports_external.literal("hottext"),
99339
+ segments: exports_external.array(exports_external.object({
99340
+ identifier: trimmedNonEmptyString.optional(),
99341
+ content: exports_external.string(),
99342
+ selectable: exports_external.boolean()
99343
+ }).strict()),
99344
+ maxChoices: nonnegativeInteger
99345
+ }).strict(),
99346
+ exports_external.object({
99347
+ ...playableInteractionBase,
99348
+ type: exports_external.literal("hotspot"),
99349
+ image: playableGraphicSchema,
99350
+ hotspots: exports_external.array(playableHotspotSchema),
99351
+ minChoices: nonnegativeInteger,
99352
+ maxChoices: nonnegativeInteger
99353
+ }).strict(),
99354
+ exports_external.object({
99355
+ ...playableInteractionBase,
99356
+ type: exports_external.literal("gap-match"),
99357
+ content: exports_external.string(),
99358
+ contentNodes: playableContentNodesSchema,
99359
+ gapTexts: exports_external.array(playableGapTextSchema),
99360
+ gaps: exports_external.array(trimmedNonEmptyString),
99361
+ maxAssociations: nonnegativeInteger
99362
+ }).strict(),
99363
+ exports_external.object({
99364
+ ...playableInteractionBase,
99365
+ type: exports_external.literal("graphic-gap-match"),
99366
+ image: playableGraphicSchema,
99367
+ gapImages: exports_external.array(playableGapImageSchema),
99368
+ hotspots: exports_external.array(playableHotspotSchema),
99369
+ maxAssociations: nonnegativeInteger
99370
+ }).strict(),
99371
+ exports_external.object({
99372
+ ...playableInteractionBase,
99373
+ type: exports_external.literal("select-point"),
99374
+ image: playableGraphicSchema,
99375
+ minChoices: nonnegativeInteger,
99376
+ maxChoices: nonnegativeInteger
99377
+ }).strict(),
99378
+ exports_external.object({
99379
+ ...playableInteractionBase,
99380
+ type: exports_external.literal("graphic-associate"),
99381
+ image: playableGraphicSchema,
99382
+ hotspots: exports_external.array(playableHotspotSchema),
99383
+ maxAssociations: nonnegativeInteger
99384
+ }).strict(),
99385
+ exports_external.object({
99386
+ ...playableInteractionBase,
99387
+ type: exports_external.literal("position-object"),
99388
+ stage: playableGraphicSchema,
99389
+ object: playableGraphicSchema,
99390
+ maxPlacements: nonnegativeInteger
99391
+ }).strict()
99392
+ ]);
99393
+ playableItemSchema = exports_external.object({
99394
+ identifier: trimmedNonEmptyString,
99395
+ title: exports_external.string(),
99396
+ prompt: exports_external.string(),
99397
+ promptContent: playableContentNodesSchema.optional(),
99398
+ maxScore: finiteNumber2,
99399
+ interactions: exports_external.array(playableInteractionSchema)
99400
+ }).strict();
99401
+ scoringRuleSchema = exports_external.object({
99402
+ responseIdentifier: trimmedNonEmptyString,
99403
+ cardinality: exports_external.enum(["single", "multiple", "ordered"]),
99404
+ baseType: exports_external.enum(["string", "identifier", "integer", "float", "directedPair"]),
99405
+ points: finiteNumber2,
99406
+ comparison: exports_external.discriminatedUnion("kind", [
99407
+ exports_external.object({ kind: exports_external.literal("match"), correctValues: exports_external.array(exports_external.string()) }),
99408
+ exports_external.object({ kind: exports_external.literal("numeric-equal"), correctValue: finiteNumber2 }),
99409
+ exports_external.object({
99410
+ kind: exports_external.literal("numeric-equal-rounded"),
99411
+ correctValue: finiteNumber2,
99412
+ roundingMode: exports_external.enum(["decimalPlaces", "significantFigures"]),
99413
+ figures: exports_external.number().int().nonnegative()
99414
+ })
99415
+ ])
99416
+ });
99417
+ scoringItemSchema = exports_external.discriminatedUnion("strategy", [
99418
+ exports_external.object({
99419
+ strategy: exports_external.literal("qti"),
99420
+ itemIdentifier: trimmedNonEmptyString,
99421
+ maxScore: finiteNumber2,
99422
+ reason: exports_external.enum([
99423
+ "response-mapping",
99424
+ "area-mapping",
99425
+ "unsupported-base-type",
99426
+ "unsupported-cardinality",
99427
+ "invalid-correct-response",
99428
+ "no-scorable-response",
99429
+ "unsupported-processing-template",
99430
+ "unsupported-response-processing",
99431
+ "cross-response-processing",
99432
+ "inconsistent-max-score"
99433
+ ])
99434
+ }),
99435
+ exports_external.object({
99436
+ strategy: exports_external.literal("platform"),
99437
+ itemIdentifier: trimmedNonEmptyString,
99438
+ maxScore: finiteNumber2,
99439
+ rules: exports_external.array(scoringRuleSchema)
99440
+ })
99441
+ ]);
99442
+ uniqueScoringItemsSchema = exports_external.array(scoringItemSchema).refine((items) => new Set(items.map((item) => item.itemIdentifier)).size === items.length);
99443
+ diagnosticContextSchema = exports_external.object({
99444
+ routingRevision: trimmedNonEmptyString,
99445
+ routingManifest: exports_external.record(exports_external.unknown())
99446
+ });
99447
+ });
99448
+
99449
+ // ../api-core/src/utils/assessment-artifact-loader.util.ts
99450
+ class AssessmentArtifactRuntimeLoader {
99451
+ artifacts;
99452
+ playableCache;
99453
+ scoringCache;
99454
+ playableLoads = new Map;
99455
+ scoringLoads = new Map;
99456
+ constructor(artifacts, options) {
99457
+ this.artifacts = artifacts;
99458
+ this.playableCache = this.immutableCache(options.maxSize);
99459
+ this.scoringCache = this.immutableCache(options.maxSize);
99460
+ }
99461
+ loadPlayable(identity) {
99462
+ return this.load({
99463
+ artifactKind: "playable",
99464
+ identity,
99465
+ key: this.cacheKey(identity),
99466
+ cache: this.playableCache,
99467
+ loads: this.playableLoads,
99468
+ load: () => this.artifacts.loadPlayable(identity),
99469
+ assert: assertPlayableAssessmentArtifact
99470
+ });
99471
+ }
99472
+ async loadPlayableContainingItems(identity, itemIdentifiers) {
99473
+ const startedAt = Date.now();
99474
+ const assessment = await this.loadPlayable(identity);
99475
+ if (assessment) {
99476
+ try {
99477
+ assertPlayableArtifactContainsItems(assessment, itemIdentifiers);
99478
+ return assessment;
99479
+ } catch (error88) {
99480
+ this.recordLoad("playable", identity.kind, "validation_failure", startedAt, error88);
99481
+ }
99482
+ }
99483
+ this.recordFallback("playable", identity.kind);
99484
+ return null;
99485
+ }
99486
+ loadManagedPlayable(qtiTestIdentifier) {
99487
+ return this.load({
99488
+ artifactKind: "playable",
99489
+ key: `managed\x00${qtiTestIdentifier}`,
99490
+ cache: this.playableCache,
99491
+ loads: this.playableLoads,
99492
+ load: () => this.artifacts.loadManagedPlayable(qtiTestIdentifier),
99493
+ assert: assertPlayableAssessmentArtifact
99494
+ });
99495
+ }
99496
+ loadScoring(identity) {
99497
+ return this.load({
99498
+ artifactKind: "scoring",
99499
+ identity,
99500
+ key: this.cacheKey(identity),
99501
+ cache: this.scoringCache,
99502
+ loads: this.scoringLoads,
99503
+ load: () => this.artifacts.loadScoring(identity),
99504
+ assert: assertScoringAssessmentArtifact
99505
+ });
99506
+ }
99507
+ async loadMatchedScoring(assessment, identity) {
99508
+ const scoring = await this.loadScoring(identity);
99509
+ if (scoring) {
99510
+ try {
99511
+ assertScoringArtifactMatchesPlayable(assessment, scoring, identity);
99512
+ return scoring;
99513
+ } catch (error88) {
99514
+ this.recordLoad("scoring", identity.kind, "validation_failure", Date.now(), error88);
99515
+ }
99516
+ }
99517
+ this.recordFallback("scoring", identity.kind);
99518
+ return null;
99519
+ }
99520
+ recordFallback(artifactKind, identityKind) {
99521
+ addEvent("assessment.artifact_fallback", {
99522
+ "app.assessment.artifact_kind": artifactKind,
99523
+ "app.assessment.artifact_identity_kind": identityKind
99524
+ });
99525
+ }
99526
+ immutableCache(maxSize) {
99527
+ return new TimebackCache({ defaultTTL: Number.POSITIVE_INFINITY, maxSize });
99528
+ }
99529
+ cacheKey(identity) {
99530
+ return canonicalJson2(identity);
99531
+ }
99532
+ recordLoad(artifactKind, identityKind, outcome, startedAt, error88) {
99533
+ addEvent("assessment.artifact_load", {
99534
+ "app.assessment.artifact_kind": artifactKind,
99535
+ "app.assessment.artifact_identity_kind": identityKind,
99536
+ "app.assessment.artifact_outcome": outcome,
99537
+ "app.assessment.artifact_duration_ms": Date.now() - startedAt,
99538
+ ...error88 ? { "exception.type": errorType(error88) } : {}
99539
+ });
99540
+ }
99541
+ async load(input) {
99542
+ const startedAt = Date.now();
99543
+ const identityKind = input.identity?.kind ?? "assessment";
99544
+ const record3 = (outcome, error88) => this.recordLoad(input.artifactKind, identityKind, outcome, startedAt, error88);
99545
+ const cached3 = input.cache.get(input.key);
99546
+ if (cached3) {
99547
+ record3("cache_hit");
99548
+ return cached3;
99549
+ }
99550
+ const inFlight = input.loads.get(input.key);
99551
+ if (inFlight) {
99552
+ record3("coalesced");
99553
+ return inFlight;
99554
+ }
99555
+ const pending = (async () => {
99556
+ try {
99557
+ const stored = await input.load();
99558
+ if (!stored) {
99559
+ record3("missing");
99560
+ return null;
99561
+ }
99562
+ input.assert(stored.payload, input.identity ?? stored.identity);
99563
+ input.cache.set(input.key, stored.payload);
99564
+ if (!input.identity) {
99565
+ input.cache.set(this.cacheKey(stored.identity), stored.payload);
99566
+ }
99567
+ record3("loaded");
99568
+ return stored.payload;
99569
+ } catch (error88) {
99570
+ record3(error88 instanceof ValidationError ? "validation_failure" : "load_failure", error88);
99571
+ return null;
99572
+ } finally {
99573
+ input.loads.delete(input.key);
99574
+ }
99575
+ })();
99576
+ input.loads.set(input.key, pending);
99577
+ return pending;
99578
+ }
99579
+ }
99580
+ var init_assessment_artifact_loader_util = __esm(async () => {
99581
+ init_spans();
99582
+ init_errors2();
99583
+ init_assessment_artifact_runtime_util();
99584
+ await init_dist5();
99585
+ });
99586
+
98447
99587
  // ../data/src/locks.ts
98448
99588
  function createInMemoryDatabaseLock() {
98449
99589
  const tails = new Map;
@@ -98564,6 +99704,810 @@ var init_assessment_runtime_lock_util = __esm(() => {
98564
99704
  };
98565
99705
  });
98566
99706
 
99707
+ // ../api-core/src/utils/timeback-assessment-runtime.util.ts
99708
+ function assessmentPreparationContentionError(expectedResponseVersion, responseVersion, kind) {
99709
+ if (expectedResponseVersion === responseVersion) {
99710
+ return new ServiceUnavailableError(`The ${kind} response state changed repeatedly while this item was being prepared. Try again shortly.`, {
99711
+ retryable: true,
99712
+ reason: "PREPARATION_CONTENTION",
99713
+ expectedResponseVersion,
99714
+ responseVersion
99715
+ });
99716
+ }
99717
+ return AssessmentRuntimeError.from(assessmentResponseVersionConflict(expectedResponseVersion, responseVersion));
99718
+ }
99719
+ function assessmentTrackableCourse(integration) {
99720
+ if (!isTimebackGrade2(integration.grade) || !isTimebackSubject(integration.subject)) {
99721
+ return null;
99722
+ }
99723
+ return { grade: integration.grade, subject: integration.subject };
99724
+ }
99725
+ function buildAssessmentActivityData(input) {
99726
+ if (!input.course) {
99727
+ return;
99728
+ }
99729
+ return {
99730
+ activityId: input.activityId,
99731
+ activityName: input.activityName,
99732
+ ...input.course,
99733
+ courseId: input.courseId
99734
+ };
99735
+ }
99736
+ function buildDiagnosticAssessmentSnapshot(input) {
99737
+ return {
99738
+ attemptId: input.attemptId,
99739
+ responseVersion: input.metadata.responseVersion,
99740
+ status: "in_progress",
99741
+ flow: "platform-routed-item-submit",
99742
+ ...input.activityData ? { activityData: input.activityData } : {},
99743
+ assessment: assessmentPresentationForAttempt(input.assessment, input.attemptId),
99744
+ selection: {
99745
+ kind: "platform-routed-diagnostic",
99746
+ purpose: "diagnostic",
99747
+ definitionId: input.metadata.diagnostic.definitionId,
99748
+ diagnosticKey: input.metadata.diagnostic.diagnosticKey,
99749
+ routingRevision: input.metadata.diagnostic.routingRevision
99750
+ },
99751
+ routing: projectDiagnosticRoutingSnapshot(input.metadata.diagnostic.routingRevision, input.state),
99752
+ completion: null
99753
+ };
99754
+ }
99755
+ function buildConventionalAssessmentSnapshot(input) {
99756
+ const metadata2 = input.metadata;
99757
+ let selection;
99758
+ if (metadata2.purpose === "review") {
99759
+ selection = {
99760
+ kind: "standards-review",
99761
+ purpose: metadata2.purpose,
99762
+ standards: [...metadata2.review.standards],
99763
+ candidateItemsPerStandard: metadata2.review.candidateItemsPerStandard,
99764
+ selections: [...metadata2.review.selections],
99765
+ fulfillment: metadata2.review.fulfillment
99766
+ };
99767
+ } else if (metadata2.purpose === "mastery") {
99768
+ selection = {
99769
+ kind: "standard-quiz",
99770
+ purpose: metadata2.purpose,
99771
+ standard: metadata2.mastery.standard
99772
+ };
99773
+ } else {
99774
+ selection = { kind: "fixed-test", purpose: metadata2.purpose };
99775
+ }
99776
+ return {
99777
+ attemptId: input.attemptId,
99778
+ responseVersion: metadata2.responseVersion,
99779
+ status: "in_progress",
99780
+ flow: assessmentFlowForPurpose(metadata2.purpose),
99781
+ ...input.activityData ? { activityData: input.activityData } : {},
99782
+ assessment: assessmentPresentationForAttempt(input.assessment, input.attemptId),
99783
+ responses: metadata2.responses,
99784
+ itemSubmissions: metadata2.itemSubmissions,
99785
+ score: null,
99786
+ selection
99787
+ };
99788
+ }
99789
+ function buildAssessmentAwardRecord(input, mastery) {
99790
+ return {
99791
+ xpAwarded: input.xpAwarded,
99792
+ ...input.masteredUnits !== undefined ? { masteredUnits: input.masteredUnits } : {},
99793
+ ...input.masteredUnitsAbsolute !== undefined ? { masteredUnitsAbsolute: input.masteredUnitsAbsolute } : {},
99794
+ masteredUnitsApplied: mastery.masteredUnitsApplied,
99795
+ ...mastery.pctCompleteApp !== undefined ? { pctCompleteApp: mastery.pctCompleteApp } : {},
99796
+ ...mastery.warnings ? { warnings: mastery.warnings } : {}
99797
+ };
99798
+ }
99799
+ function stageAssessmentAttemptSupersession(attempt) {
99800
+ Object.assign(attempt.result, ASSESSMENT_ATTEMPT_SUPERSEDED);
99801
+ Object.assign(attempt.selection, ASSESSMENT_ATTEMPT_SUPERSEDED);
99802
+ }
99803
+ function shouldConfirmAssessmentVersionUnavailable(details) {
99804
+ return typeof details === "object" && details !== null && "expectedRevision" in details && typeof details.expectedRevision === "string" && !("currentRevision" in details);
99805
+ }
99806
+ function buildAssessmentAttemptSupersessionUpdate(result) {
99807
+ if (result.status !== "active" || !isAssessmentAttemptOpen({
99808
+ inProgress: result.inProgress ?? "",
99809
+ scoreStatus: result.scoreStatus
99810
+ })) {
99811
+ return null;
99812
+ }
99813
+ return {
99814
+ status: "active",
99815
+ assessmentLineItem: result.assessmentLineItem,
99816
+ student: result.student,
99817
+ score: result.score ?? 0,
99818
+ scoreDate: result.scoreDate,
99819
+ ...ASSESSMENT_ATTEMPT_SUPERSEDED,
99820
+ metadata: result.metadata ?? {}
99821
+ };
99822
+ }
99823
+ async function resolveAvailableAssessmentAttempt(initialDecision, select3, resume) {
99824
+ let decision = initialDecision;
99825
+ while (decision?.kind === "resume") {
99826
+ const snapshot = await resume(decision);
99827
+ if (snapshot !== null) {
99828
+ return { kind: "resumed", snapshot };
99829
+ }
99830
+ decision = select3();
99831
+ }
99832
+ return { kind: "continue", decision };
99833
+ }
99834
+ function buildReviewItemResultMetadata(input) {
99835
+ const administration = itemAdministration(input.metadata.itemSubmissions, input.selection.itemIdentifier);
99836
+ if (!administration) {
99837
+ throw new Error(`Cannot create a review result for unadministered candidate ${input.selection.itemIdentifier}`);
99838
+ }
99839
+ return {
99840
+ version: PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_VERSION,
99841
+ parentAttemptId: input.parentAttemptId,
99842
+ activityId: input.metadata.activityId,
99843
+ integrationId: input.metadata.integrationId,
99844
+ enrollmentId: input.metadata.enrollmentId,
99845
+ bankRevision: input.metadata.review.bankRevision,
99846
+ itemIdentifier: input.selection.itemIdentifier,
99847
+ standard: input.selection.standard,
99848
+ deliveredAt: administration.submittedAt,
99849
+ responseVersion: input.responseVersion ?? input.metadata.responseVersion,
99850
+ responses: input.metadata.responses[input.selection.itemIdentifier] ?? {},
99851
+ ...input.submissionId ? { submissionId: input.submissionId } : {},
99852
+ ...input.isCorrect === undefined ? {} : { isCorrect: input.isCorrect }
99853
+ };
99854
+ }
99855
+ function buildOpenReviewChildResult(input) {
99856
+ return {
99857
+ status: "active",
99858
+ assessmentLineItem: { sourcedId: input.childLineItemId },
99859
+ student: input.student,
99860
+ score: 0,
99861
+ scoreDate: input.metadata.deliveredAt,
99862
+ ...ASSESSMENT_ATTEMPT_OPEN,
99863
+ metadata: {
99864
+ [PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY]: input.metadata
99865
+ }
99866
+ };
99867
+ }
99868
+ function diagnosticQuestionLineItemId(parentLineItemId, itemIdentifier) {
99869
+ return deterministicUUID(["playcademy:diagnostic-question-line-item:v1", parentLineItemId, itemIdentifier].join("\x00"));
99870
+ }
99871
+ function diagnosticQuestionResultId(parentAttemptId, childLineItemId) {
99872
+ return deterministicUUID(["playcademy:diagnostic-question-result:v1", parentAttemptId, childLineItemId].join("\x00"));
99873
+ }
99874
+ function diagnosticAdministration(input) {
99875
+ const ledgerIndex = input.metadata.diagnostic.ledger.findIndex((entry) => entry.routingNodeKey === input.ledgerEntry.routingNodeKey);
99876
+ const persistedEntry = input.metadata.diagnostic.ledger[ledgerIndex];
99877
+ const administration = input.metadata.itemSubmissions[ledgerIndex];
99878
+ 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) {
99879
+ throw new Error(`Cannot create a diagnostic result for uncommitted routing node ${input.ledgerEntry.routingNodeKey}`);
99880
+ }
99881
+ return administration;
99882
+ }
99883
+ function buildDiagnosticItemResultMetadata(input) {
99884
+ const administration = diagnosticAdministration(input);
99885
+ return {
99886
+ version: PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_VERSION,
99887
+ parentAttemptId: input.parentAttemptId,
99888
+ activityId: input.metadata.activityId,
99889
+ integrationId: input.metadata.integrationId,
99890
+ enrollmentId: input.metadata.enrollmentId,
99891
+ definitionId: input.metadata.diagnostic.definitionId,
99892
+ diagnosticKey: input.metadata.diagnostic.diagnosticKey,
99893
+ routingRevision: input.metadata.diagnostic.routingRevision,
99894
+ contentRevision: input.metadata.selectedTest.contentRevision,
99895
+ stageKey: input.ledgerEntry.stageKey,
99896
+ trackKey: input.ledgerEntry.trackKey,
99897
+ routingNodeKey: input.ledgerEntry.routingNodeKey,
99898
+ itemIdentifier: input.ledgerEntry.itemIdentifier,
99899
+ deliveredAt: administration.submittedAt,
99900
+ responseVersion: administration.responseVersion,
99901
+ responses: input.metadata.responses[input.ledgerEntry.itemIdentifier] ?? {},
99902
+ submissionId: administration.submissionId,
99903
+ isCorrect: input.ledgerEntry.isCorrect
99904
+ };
99905
+ }
99906
+ function resolveDiagnosticChildResult(input) {
99907
+ if (!input.childResult) {
99908
+ return { action: "create" };
99909
+ }
99910
+ const childMetadata = playcademyDiagnosticAssessmentItemResultMetadata(input.childResult.metadata);
99911
+ const expectedMetadata = buildDiagnosticItemResultMetadata({
99912
+ metadata: input.parentMetadata,
99913
+ ledgerEntry: input.ledgerEntry,
99914
+ parentAttemptId: input.parentResult.sourcedId
99915
+ });
99916
+ if (input.childResult.assessmentLineItem.sourcedId !== input.childLineItemId || input.childResult.student.sourcedId !== input.parentResult.student.sourcedId || !childMetadata || canonicalJson2(childMetadata) !== canonicalJson2(expectedMetadata)) {
99917
+ return { action: "restore", reason: "incompatible" };
99918
+ }
99919
+ return input.childResult.status === "active" ? { action: "reuse", metadata: expectedMetadata } : { action: "restore", reason: "inactive" };
99920
+ }
99921
+ function buildFinalizedDiagnosticChildResult(input) {
99922
+ const administration = diagnosticAdministration(input);
99923
+ const metadata2 = buildDiagnosticItemResultMetadata(input);
99924
+ return {
99925
+ resultUpdate: {
99926
+ status: "active",
99927
+ assessmentLineItem: { sourcedId: input.childLineItemId },
99928
+ student: input.student,
99929
+ score: administration.score.earned,
99930
+ scoreDate: administration.submittedAt,
99931
+ ...ASSESSMENT_ATTEMPT_COMPLETED,
99932
+ metadata: {
99933
+ [PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY]: metadata2
99934
+ }
99935
+ }
99936
+ };
99937
+ }
99938
+ function buildFinalizedReviewChildResult(input) {
99939
+ const metadata2 = buildReviewItemResultMetadata({
99940
+ metadata: input.metadata,
99941
+ selection: input.selection,
99942
+ parentAttemptId: input.parentAttemptId,
99943
+ submissionId: input.submissionId,
99944
+ isCorrect: input.itemResult.isCorrect
99945
+ });
99946
+ return {
99947
+ resultUpdate: {
99948
+ status: "active",
99949
+ assessmentLineItem: { sourcedId: input.childLineItemId },
99950
+ student: input.student,
99951
+ score: input.itemResult.score.earned,
99952
+ scoreDate: input.timestamp,
99953
+ ...ASSESSMENT_ATTEMPT_COMPLETED,
99954
+ metadata: {
99955
+ [PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY]: metadata2
99956
+ }
99957
+ },
99958
+ outcome: {
99959
+ itemIdentifier: input.selection.itemIdentifier,
99960
+ standard: input.selection.standard,
99961
+ answered: Object.keys(input.metadata.responses[input.selection.itemIdentifier] ?? {}).length > 0,
99962
+ score: input.itemResult.score,
99963
+ isCorrect: input.itemResult.isCorrect
99964
+ }
99965
+ };
99966
+ }
99967
+ function resolveReviewChildResult(input) {
99968
+ if (!input.childResult) {
99969
+ return { action: "create" };
99970
+ }
99971
+ const childMetadata = playcademyAssessmentItemResultMetadata(input.childResult.metadata);
99972
+ const expectedResponses = input.parentMetadata.responses[input.selection.itemIdentifier] ?? {};
99973
+ const administration = itemAdministration(input.parentMetadata.itemSubmissions, input.selection.itemIdentifier);
99974
+ 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)) {
99975
+ return { action: "restore", reason: "incompatible" };
99976
+ }
99977
+ return input.childResult.status === "active" ? { action: "reuse", metadata: childMetadata } : { action: "restore", reason: "inactive" };
99978
+ }
99979
+ function buildAssessmentSubmitResult(input) {
99980
+ if (!input.metadata.score || !input.metadata.submissionId) {
99981
+ return null;
99982
+ }
99983
+ if (input.metadata.purpose === "diagnostic" && input.metadata.diagnostic) {
99984
+ const tracks = input.metadata.completion?.diagnosticTracks;
99985
+ return tracks ? {
99986
+ purpose: input.metadata.purpose,
99987
+ attemptId: input.attemptId,
99988
+ submissionId: input.metadata.submissionId,
99989
+ diagnosticKey: input.metadata.diagnostic.diagnosticKey,
99990
+ responseVersion: input.metadata.responseVersion,
99991
+ status: "awaiting_award",
99992
+ tracks
99993
+ } : null;
99994
+ }
99995
+ const base = {
99996
+ attemptId: input.attemptId,
99997
+ submissionId: input.metadata.submissionId,
99998
+ responseVersion: input.metadata.responseVersion,
99999
+ status: "awaiting_award",
100000
+ responses: input.metadata.responses,
100001
+ score: input.metadata.score
100002
+ };
100003
+ if (input.metadata.purpose === "review") {
100004
+ const itemOutcomes = input.metadata.completion?.itemOutcomes;
100005
+ return itemOutcomes ? { ...base, purpose: input.metadata.purpose, itemOutcomes } : null;
100006
+ }
100007
+ return { ...base, purpose: input.metadata.purpose };
100008
+ }
100009
+ function assessmentAwardFromResult(result, metadata2) {
100010
+ if (metadata2.award) {
100011
+ return metadata2.award;
100012
+ }
100013
+ const xp = result.metadata?.xp;
100014
+ return typeof xp === "number" && Number.isFinite(xp) && xp >= 0 ? { xpAwarded: xp, masteredUnitsApplied: 0 } : null;
100015
+ }
100016
+ function buildCompletedAssessmentSubmitResult(input) {
100017
+ const attemptId = input.result.sourcedId;
100018
+ const award = assessmentAwardFromResult(input.result, input.metadata);
100019
+ if (award === null) {
100020
+ throw new GoneError("This completed assessment is missing its recorded XP.", {
100021
+ attemptId
100022
+ });
100023
+ }
100024
+ const submitted = buildAssessmentSubmitResult({ attemptId, metadata: input.metadata });
100025
+ if (!submitted) {
100026
+ throw new GoneError("This completed assessment has incomplete persisted results.", {
100027
+ attemptId
100028
+ });
100029
+ }
100030
+ return {
100031
+ ...submitted,
100032
+ status: "completed",
100033
+ xpAwarded: award.xpAwarded,
100034
+ masteredUnitsApplied: award.masteredUnitsApplied,
100035
+ ...award.pctCompleteApp !== undefined ? { pctCompleteApp: award.pctCompleteApp } : {},
100036
+ ...award.warnings ? { warnings: award.warnings } : {}
100037
+ };
100038
+ }
100039
+ function assessmentAttemptId(input) {
100040
+ return deterministicUUID([
100041
+ "playcademy:assessment-attempt",
100042
+ input.gameId,
100043
+ input.studentId,
100044
+ input.courseId,
100045
+ input.activityId,
100046
+ input.purpose,
100047
+ String(input.attemptNumber)
100048
+ ].join("\x00"));
100049
+ }
100050
+ async function findLatestAssessmentResult(results, match) {
100051
+ let latest = null;
100052
+ let latestTimestamp = 0;
100053
+ for await (const result of results) {
100054
+ const resultTimestamp = assessmentAttemptTimestamp(result.scoreDate);
100055
+ if (latest && resultTimestamp < latestTimestamp) {
100056
+ break;
100057
+ }
100058
+ const matched = match(result);
100059
+ if (matched !== null && (!latest || compareNewestCompletedAssessmentAttempt({ attemptId: result.sourcedId, scoreDate: result.scoreDate }, {
100060
+ attemptId: latest.result.sourcedId,
100061
+ scoreDate: latest.result.scoreDate
100062
+ }) < 0)) {
100063
+ latest = { result, match: matched };
100064
+ latestTimestamp = resultTimestamp;
100065
+ }
100066
+ }
100067
+ return latest;
100068
+ }
100069
+ function playableChoice(choice) {
100070
+ return {
100071
+ identifier: choice.identifier,
100072
+ content: choice.content,
100073
+ ...choice.contentNodes ? { contentNodes: choice.contentNodes } : {}
100074
+ };
100075
+ }
100076
+ function playableAssociableChoice(choice) {
100077
+ const matchMax = qtiAttributeCapacity(choice.attributes, "match-max", 0);
100078
+ if (matchMax === null) {
100079
+ throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", "The selected assessment contains an invalid Match capacity.");
100080
+ }
100081
+ return {
100082
+ ...playableChoice(choice),
100083
+ matchMax
100084
+ };
100085
+ }
100086
+ function requireChoices2(choices) {
100087
+ if (choices.length === 0 || choices.some((candidate) => !candidate.identifier)) {
100088
+ throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", "The selected assessment contains an interaction Playcademy cannot render.");
100089
+ }
100090
+ return choices;
100091
+ }
100092
+ function isPlayableAssessmentShape(value) {
100093
+ return PLAYABLE_SHAPE_SET.has(value);
100094
+ }
100095
+ function playableGraphic(graphic) {
100096
+ return {
100097
+ src: graphic.src,
100098
+ ...graphic.width === undefined ? {} : { width: graphic.width },
100099
+ ...graphic.height === undefined ? {} : { height: graphic.height },
100100
+ ...graphic.description ? { description: graphic.description } : {}
100101
+ };
100102
+ }
100103
+ function requireGraphic(questionIdentifier, graphic) {
100104
+ if (!graphic) {
100105
+ throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${questionIdentifier} is missing a required graphic.`);
100106
+ }
100107
+ return playableGraphic(graphic);
100108
+ }
100109
+ function requireHotspots(questionIdentifier, hotspots) {
100110
+ if (hotspots.length === 0) {
100111
+ throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${questionIdentifier} declares no graphic regions.`);
100112
+ }
100113
+ return hotspots.map((hotspot) => ({
100114
+ identifier: hotspot.identifier,
100115
+ shape: isPlayableAssessmentShape(hotspot.shape) ? hotspot.shape : "default",
100116
+ coords: [...hotspot.coords],
100117
+ ...hotspot.matchMax === undefined ? {} : { matchMax: hotspot.matchMax },
100118
+ ...hotspot.description ? { description: hotspot.description } : {}
100119
+ }));
100120
+ }
100121
+ function parseInteraction2(questionIdentifier, interaction) {
100122
+ const { type } = interaction;
100123
+ const responseIdentifier = interaction.attributes["response-identifier"];
100124
+ if (!responseIdentifier) {
100125
+ throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${questionIdentifier} has no response identifier.`);
100126
+ }
100127
+ const prompt = interaction.embeddedPrompt ? { prompt: interaction.embeddedPrompt } : {};
100128
+ if (type === "choice") {
100129
+ const choices = requireChoices2(interaction.choices.map(playableChoice));
100130
+ return {
100131
+ type,
100132
+ responseIdentifier,
100133
+ ...prompt,
100134
+ choices,
100135
+ minChoices: qtiAttributeNumber(interaction.attributes, "min-choices", 1),
100136
+ maxChoices: qtiAttributeMaximum(interaction.attributes, "max-choices", 1, choices.length)
100137
+ };
100138
+ }
100139
+ if (type === "inline-choice") {
100140
+ return {
100141
+ type,
100142
+ responseIdentifier,
100143
+ ...prompt,
100144
+ choices: requireChoices2(interaction.choices.map(playableChoice))
100145
+ };
100146
+ }
100147
+ if (type === "text-entry") {
100148
+ const expectedLength = qtiAttributeNumber(interaction.attributes, "expected-length", 0);
100149
+ return {
100150
+ type,
100151
+ responseIdentifier,
100152
+ ...prompt,
100153
+ ...Number.isInteger(expectedLength) && expectedLength > 0 ? { expectedLength } : {},
100154
+ ...interaction.attributes.placeholder ? { placeholder: interaction.attributes.placeholder } : {}
100155
+ };
100156
+ }
100157
+ if (type === "order") {
100158
+ return {
100159
+ type,
100160
+ responseIdentifier,
100161
+ ...prompt,
100162
+ choices: requireChoices2(interaction.choices.map(playableChoice)),
100163
+ shuffle: qtiAttributeBoolean(interaction.attributes, "shuffle")
100164
+ };
100165
+ }
100166
+ if (type === "match") {
100167
+ const sets = interaction.choiceSets;
100168
+ if (sets.length !== 2) {
100169
+ throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", "Match interactions require exactly two choice sets.");
100170
+ }
100171
+ const sourceChoices = requireChoices2(sets[0].map(playableAssociableChoice));
100172
+ const targetChoices = requireChoices2(sets[1].map(playableAssociableChoice));
100173
+ return {
100174
+ type,
100175
+ responseIdentifier,
100176
+ ...prompt,
100177
+ sourceChoices,
100178
+ targetChoices,
100179
+ maxAssociations: qtiAttributeMaximum(interaction.attributes, "max-associations", 1, sourceChoices.length * targetChoices.length),
100180
+ shuffle: qtiAttributeBoolean(interaction.attributes, "shuffle")
100181
+ };
100182
+ }
100183
+ if (type === "hotspot") {
100184
+ const image = requireGraphic(questionIdentifier, interaction.graphic);
100185
+ const hotspots = requireHotspots(questionIdentifier, interaction.hotspots);
100186
+ return {
100187
+ type,
100188
+ responseIdentifier,
100189
+ ...prompt,
100190
+ image,
100191
+ hotspots,
100192
+ minChoices: qtiAttributeNumber(interaction.attributes, "min-choices", 1),
100193
+ maxChoices: qtiAttributeMaximum(interaction.attributes, "max-choices", 1, hotspots.length)
100194
+ };
100195
+ }
100196
+ if (type === "gap-match") {
100197
+ if (interaction.gapTexts.length === 0 || interaction.gaps.length === 0 || interaction.textWithGaps === undefined || !interaction.gapContent || !qtiGapContentMatchesGaps(interaction.gapContent, interaction.gaps)) {
100198
+ throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${questionIdentifier} has an incomplete gap-match interaction.`);
100199
+ }
100200
+ return {
100201
+ type,
100202
+ responseIdentifier,
100203
+ ...prompt,
100204
+ content: interaction.textWithGaps,
100205
+ contentNodes: interaction.gapContent,
100206
+ gapTexts: interaction.gapTexts.map((gapText) => ({
100207
+ identifier: gapText.identifier,
100208
+ content: gapText.content,
100209
+ matchMax: gapText.matchMax
100210
+ })),
100211
+ gaps: [...interaction.gaps],
100212
+ maxAssociations: qtiAttributeNumber(interaction.attributes, "max-associations", 0)
100213
+ };
100214
+ }
100215
+ if (type === "graphic-gap-match") {
100216
+ const image = requireGraphic(questionIdentifier, interaction.graphic);
100217
+ const hotspots = requireHotspots(questionIdentifier, interaction.hotspots);
100218
+ if (interaction.gapImages.length === 0) {
100219
+ throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${questionIdentifier} has no draggable gap images.`);
100220
+ }
100221
+ return {
100222
+ type,
100223
+ responseIdentifier,
100224
+ ...prompt,
100225
+ image,
100226
+ gapImages: interaction.gapImages.map((gapImage) => ({
100227
+ identifier: gapImage.identifier,
100228
+ ...gapImage.graphic ? { image: playableGraphic(gapImage.graphic) } : {},
100229
+ matchMax: gapImage.matchMax,
100230
+ ...gapImage.description ? { description: gapImage.description } : {}
100231
+ })),
100232
+ hotspots,
100233
+ maxAssociations: qtiAttributeNumber(interaction.attributes, "max-associations", 0)
100234
+ };
100235
+ }
100236
+ if (type === "select-point") {
100237
+ return {
100238
+ type,
100239
+ responseIdentifier,
100240
+ ...prompt,
100241
+ image: requireGraphic(questionIdentifier, interaction.graphic),
100242
+ minChoices: qtiAttributeNumber(interaction.attributes, "min-choices", 0),
100243
+ maxChoices: qtiAttributeNumber(interaction.attributes, "max-choices", 0)
100244
+ };
100245
+ }
100246
+ if (type === "graphic-associate") {
100247
+ return {
100248
+ type,
100249
+ responseIdentifier,
100250
+ ...prompt,
100251
+ image: requireGraphic(questionIdentifier, interaction.graphic),
100252
+ hotspots: requireHotspots(questionIdentifier, interaction.hotspots),
100253
+ maxAssociations: qtiAttributeNumber(interaction.attributes, "max-associations", 1)
100254
+ };
100255
+ }
100256
+ if (type === "position-object") {
100257
+ return {
100258
+ type,
100259
+ responseIdentifier,
100260
+ ...prompt,
100261
+ stage: requireGraphic(questionIdentifier, interaction.graphic),
100262
+ object: requireGraphic(questionIdentifier, interaction.objectGraphic),
100263
+ maxPlacements: qtiAttributeNumber(interaction.attributes, "max-choices", 1)
100264
+ };
100265
+ }
100266
+ if (type === "hottext") {
100267
+ const segments = interaction.textSegments;
100268
+ if (!segments.some((segment) => segment.selectable) || segments.some((segment) => segment.selectable && (!segment.identifier || !segment.content))) {
100269
+ throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", "Hottext interactions require identified selectable content.");
100270
+ }
100271
+ return {
100272
+ type,
100273
+ responseIdentifier,
100274
+ ...prompt,
100275
+ segments,
100276
+ maxChoices: qtiAttributeMaximum(interaction.attributes, "max-choices", 1, segments.filter((segment) => segment.selectable).length)
100277
+ };
100278
+ }
100279
+ const unsupported = type;
100280
+ throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${questionIdentifier} uses unsupported interaction ${unsupported}.`);
100281
+ }
100282
+ function normalizedQtiItem(question) {
100283
+ if (!question.rawXml) {
100284
+ throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${question.identifier} has no authoritative QTI XML.`);
100285
+ }
100286
+ try {
100287
+ return parseQtiItemXml(question.rawXml);
100288
+ } catch (error88) {
100289
+ throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${question.identifier} contains invalid QTI XML.`, { reason: errorMessage(error88) });
100290
+ }
100291
+ }
100292
+ function parsePlayableItem(question) {
100293
+ const parsed = normalizedQtiItem(question);
100294
+ const interactions = playableQtiInteractions(parsed);
100295
+ if (!interactions) {
100296
+ throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${question.identifier} must contain only supported interactions with unique response identifiers.`);
100297
+ }
100298
+ return {
100299
+ identifier: question.identifier,
100300
+ title: question.title,
100301
+ prompt: parsed.bodyTextWithBlanks,
100302
+ ...parsed.bodyContent ? { promptContent: parsed.bodyContent } : {},
100303
+ maxScore: qtiItemMaxScore(parsed),
100304
+ interactions: interactions.map((interaction) => parseInteraction2(question.identifier, interaction))
100305
+ };
100306
+ }
100307
+ function assessmentContentRevision(test, questions) {
100308
+ return sha256Hex(canonicalJson2({
100309
+ identifier: test.identifier,
100310
+ title: test.title,
100311
+ qtiVersion: test.qtiVersion,
100312
+ parts: test["qti-test-part"],
100313
+ questions: questions.questions.map(({ reference, question }) => ({
100314
+ identifier: reference.identifier,
100315
+ rawXml: question.rawXml
100316
+ }))
100317
+ }));
100318
+ }
100319
+ async function buildPlayableAssessment(test, questions) {
100320
+ return {
100321
+ identifier: test.identifier,
100322
+ contractVersion: PLAYCADEMY_ASSESSMENT_CONTRACT_VERSION,
100323
+ contentRevision: await assessmentContentRevision(test, questions),
100324
+ title: test.title,
100325
+ items: questions.questions.map(({ question }) => parsePlayableItem(question))
100326
+ };
100327
+ }
100328
+ function assessmentFixtureScoringKeys(assessment, questions) {
100329
+ const questionByIdentifier = new Map(questions.questions.map(({ question }) => [question.identifier, question]));
100330
+ const correctResponses = {};
100331
+ const responseAreas = {};
100332
+ for (const item of assessment.items) {
100333
+ const question = questionByIdentifier.get(item.identifier);
100334
+ const normalizedDeclarations = question ? normalizedQtiItem(question).responseDeclarations : [];
100335
+ const itemResponses = {};
100336
+ for (const interaction of item.interactions) {
100337
+ const responseIdentifier = interaction.responseIdentifier;
100338
+ const declaration = question?.responseDeclarations?.find((candidate) => candidate.identifier === responseIdentifier);
100339
+ const declaredValues = declaration?.correctResponse?.value ?? [];
100340
+ const normalizedDeclaration = normalizedDeclarations.find((candidate) => candidate.identifier === responseIdentifier);
100341
+ const values = declaredValues.length > 0 ? declaredValues : normalizedDeclaration?.correctValues ?? [];
100342
+ const areas = interaction.type === "select-point" || interaction.type === "position-object" ? normalizedDeclaration?.areaMapping?.entries.filter((entry) => entry.value > 0 && isPlayableAssessmentShape(entry.shape)) : undefined;
100343
+ if (areas?.length) {
100344
+ (responseAreas[item.identifier] ??= {})[responseIdentifier] = areas.map((entry) => ({
100345
+ shape: entry.shape,
100346
+ coords: [...entry.coords],
100347
+ value: entry.value
100348
+ }));
100349
+ }
100350
+ if (values.length > 0) {
100351
+ const cardinality = declaration?.cardinality ?? normalizedDeclaration?.cardinality;
100352
+ itemResponses[responseIdentifier] = cardinality === "single" && values.length === 1 ? values[0] : values;
100353
+ } else if (!areas?.length) {
100354
+ throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${item.identifier} has no fixture scoring key.`);
100355
+ }
100356
+ }
100357
+ correctResponses[item.identifier] = itemResponses;
100358
+ }
100359
+ return {
100360
+ correctResponses,
100361
+ ...Object.keys(responseAreas).length > 0 ? { responseAreas } : {}
100362
+ };
100363
+ }
100364
+ function prepareDiagnosticAssessmentResponses(assessment, current, input) {
100365
+ const update2 = { [input.itemIdentifier]: input.responses };
100366
+ validateAssessmentResponseUpdate(assessment, update2);
100367
+ const responses = applyAssessmentResponseUpdate(current, update2);
100368
+ const item = assessment.items.find((candidate) => candidate.identifier === input.itemIdentifier);
100369
+ const itemResponses = responses[input.itemIdentifier];
100370
+ const missingResponse = item?.interactions.find((interaction) => itemResponses?.[interaction.responseIdentifier] === undefined);
100371
+ if (!item || !itemResponses || missingResponse) {
100372
+ throw new AssessmentRuntimeError("INVALID_RESPONSE", `Diagnostic item ${input.itemIdentifier} requires a response for every interaction.`, {
100373
+ itemIdentifier: input.itemIdentifier,
100374
+ ...missingResponse ? { responseIdentifier: missingResponse.responseIdentifier } : {}
100375
+ });
100376
+ }
100377
+ validateAssessmentResponses(assessment, { [input.itemIdentifier]: itemResponses });
100378
+ return responses;
100379
+ }
100380
+ function validateAssessmentResponseUpdate(assessment, update2) {
100381
+ const message = assessmentResponseUpdateValidationMessage(assessment, update2);
100382
+ if (message) {
100383
+ throw new AssessmentRuntimeError("INVALID_RESPONSE", message);
100384
+ }
100385
+ }
100386
+ function validateAssessmentResponses(assessment, responses) {
100387
+ const message = assessmentResponseValidationMessage(assessment, responses);
100388
+ if (message) {
100389
+ throw new AssessmentRuntimeError("INVALID_RESPONSE", message);
100390
+ }
100391
+ }
100392
+ function validateAssessmentDraftResponses(assessment, responses) {
100393
+ const message = assessmentDraftResponseValidationMessage(assessment, responses);
100394
+ if (message) {
100395
+ throw new AssessmentRuntimeError("INVALID_RESPONSE", message);
100396
+ }
100397
+ }
100398
+ function assessmentScoringRequests(assessment, responses) {
100399
+ return assessment.items.map((item) => ({
100400
+ itemIdentifier: item.identifier,
100401
+ maxScore: item.maxScore,
100402
+ requests: item.interactions.map((interaction) => ({
100403
+ identifier: interaction.responseIdentifier,
100404
+ response: responses[item.identifier]?.[interaction.responseIdentifier] ?? ""
100405
+ }))
100406
+ }));
100407
+ }
100408
+ function buildAssessmentResultMetadata(input) {
100409
+ return {
100410
+ [PLAYCADEMY_ASSESSMENT_RESULT_METADATA_KEY]: input.metadata,
100411
+ assessmentPurpose: input.metadata.purpose,
100412
+ ...assessmentPurposeClassificationFields2(input.metadata.purpose, input)
100413
+ };
100414
+ }
100415
+ function mergeAssessmentRuntimeMetadata(resultMetadata, metadata2) {
100416
+ return {
100417
+ ...resultMetadata,
100418
+ [PLAYCADEMY_ASSESSMENT_RESULT_METADATA_KEY]: metadata2
100419
+ };
100420
+ }
100421
+ function aggregateAssessmentScore(itemScores, possible) {
100422
+ const earned = itemScores.reduce((sum, itemScore) => sum + (Number.isFinite(itemScore) ? itemScore : 0), 0);
100423
+ return {
100424
+ earned,
100425
+ possible,
100426
+ normalized: possible === 0 ? 0 : Math.min(1, Math.max(0, earned / possible))
100427
+ };
100428
+ }
100429
+ function countCorrectQuestions(verdicts) {
100430
+ return verdicts.filter((verdict) => verdict === true).length;
100431
+ }
100432
+ function assessmentScoreFromPercentage(percentage, possible) {
100433
+ const normalized = Math.min(1, Math.max(0, (percentage ?? 0) / 100));
100434
+ const earned = Math.round(normalized * possible * 1e6) / 1e6;
100435
+ return { earned, possible, normalized };
100436
+ }
100437
+ function buildAssessmentResultSubmission(input) {
100438
+ const metadata2 = {
100439
+ ...input.metadata,
100440
+ submissionId: input.submissionId,
100441
+ score: input.score,
100442
+ completion: {
100443
+ testName: input.testName,
100444
+ courseName: input.courseName,
100445
+ totalQuestions: input.totalQuestions,
100446
+ correctQuestions: input.correctQuestions,
100447
+ ...input.session ? { session: input.session } : {},
100448
+ ...input.itemOutcomes ? { itemOutcomes: input.itemOutcomes } : {},
100449
+ ...input.diagnosticTracks ? { diagnosticTracks: input.diagnosticTracks } : {}
100450
+ },
100451
+ updatedAt: input.timestamp
100452
+ };
100453
+ return {
100454
+ metadata: metadata2,
100455
+ resultUpdate: {
100456
+ status: input.result.status,
100457
+ assessmentLineItem: input.result.assessmentLineItem,
100458
+ student: input.result.student,
100459
+ score: input.score.normalized * 100,
100460
+ scoreDate: input.timestamp,
100461
+ ...ASSESSMENT_ATTEMPT_AWAITING_AWARD,
100462
+ metadata: mergeAssessmentRuntimeMetadata(input.result.metadata, metadata2)
100463
+ }
100464
+ };
100465
+ }
100466
+ function buildAssessmentResultAwardFinalization(input) {
100467
+ const completion = input.metadata.completion;
100468
+ if (!completion || completion.totalQuestions === undefined) {
100469
+ throw new Error("A scored assessment must retain its completion facts before finalization");
100470
+ }
100471
+ const metadata2 = {
100472
+ ...input.metadata,
100473
+ finalizedAt: input.timestamp,
100474
+ updatedAt: input.timestamp,
100475
+ award: input.award
100476
+ };
100477
+ const masteryRequested = input.award.masteredUnits !== undefined || input.award.masteredUnitsAbsolute !== undefined;
100478
+ return {
100479
+ metadata: metadata2,
100480
+ resultUpdate: {
100481
+ status: input.result.status,
100482
+ assessmentLineItem: input.result.assessmentLineItem,
100483
+ student: input.result.student,
100484
+ score: input.result.score ?? 0,
100485
+ scoreDate: input.result.scoreDate,
100486
+ ...ASSESSMENT_ATTEMPT_COMPLETED,
100487
+ metadata: {
100488
+ ...mergeAssessmentRuntimeMetadata(input.result.metadata, metadata2),
100489
+ xp: input.award.xpAwarded,
100490
+ ...masteryRequested ? { masteredUnits: input.award.masteredUnitsApplied } : {},
100491
+ totalQuestions: completion.totalQuestions,
100492
+ correctQuestions: completion.correctQuestions,
100493
+ appName: input.appName
100494
+ }
100495
+ }
100496
+ };
100497
+ }
100498
+ var PLAYABLE_SHAPE_SET;
100499
+ var init_timeback_assessment_runtime_util = __esm(() => {
100500
+ init_src();
100501
+ init_assessment_runtime2();
100502
+ init_qti();
100503
+ init_types2();
100504
+ init_utils6();
100505
+ init_timeback4();
100506
+ init_uuid();
100507
+ init_errors2();
100508
+ PLAYABLE_SHAPE_SET = new Set(PLAYABLE_ASSESSMENT_SHAPES);
100509
+ });
100510
+
98567
100511
  // ../api-core/src/utils/timeback-qti-authoring.util.ts
98568
100512
  function recordValue(value) {
98569
100513
  return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
@@ -99505,810 +101449,6 @@ var init_timeback_qti_authoring_util = __esm(() => {
99505
101449
  ];
99506
101450
  });
99507
101451
 
99508
- // ../api-core/src/utils/timeback-assessment-runtime.util.ts
99509
- function assessmentPreparationContentionError(expectedResponseVersion, responseVersion, kind) {
99510
- if (expectedResponseVersion === responseVersion) {
99511
- return new ServiceUnavailableError(`The ${kind} response state changed repeatedly while this item was being prepared. Try again shortly.`, {
99512
- retryable: true,
99513
- reason: "PREPARATION_CONTENTION",
99514
- expectedResponseVersion,
99515
- responseVersion
99516
- });
99517
- }
99518
- return AssessmentRuntimeError.from(assessmentResponseVersionConflict(expectedResponseVersion, responseVersion));
99519
- }
99520
- function assessmentTrackableCourse(integration) {
99521
- if (!isTimebackGrade2(integration.grade) || !isTimebackSubject(integration.subject)) {
99522
- return null;
99523
- }
99524
- return { grade: integration.grade, subject: integration.subject };
99525
- }
99526
- function buildAssessmentActivityData(input) {
99527
- if (!input.course) {
99528
- return;
99529
- }
99530
- return {
99531
- activityId: input.activityId,
99532
- activityName: input.activityName,
99533
- ...input.course,
99534
- courseId: input.courseId
99535
- };
99536
- }
99537
- function buildDiagnosticAssessmentSnapshot(input) {
99538
- return {
99539
- attemptId: input.attemptId,
99540
- responseVersion: input.metadata.responseVersion,
99541
- status: "in_progress",
99542
- flow: "platform-routed-item-submit",
99543
- ...input.activityData ? { activityData: input.activityData } : {},
99544
- assessment: assessmentPresentationForAttempt(input.assessment, input.attemptId),
99545
- selection: {
99546
- kind: "platform-routed-diagnostic",
99547
- purpose: "diagnostic",
99548
- definitionId: input.metadata.diagnostic.definitionId,
99549
- diagnosticKey: input.metadata.diagnostic.diagnosticKey,
99550
- routingRevision: input.metadata.diagnostic.routingRevision
99551
- },
99552
- routing: projectDiagnosticRoutingSnapshot(input.metadata.diagnostic.routingRevision, input.state),
99553
- completion: null
99554
- };
99555
- }
99556
- function buildConventionalAssessmentSnapshot(input) {
99557
- const metadata2 = input.metadata;
99558
- let selection;
99559
- if (metadata2.purpose === "review") {
99560
- selection = {
99561
- kind: "standards-review",
99562
- purpose: metadata2.purpose,
99563
- standards: [...metadata2.review.standards],
99564
- candidateItemsPerStandard: metadata2.review.candidateItemsPerStandard,
99565
- selections: [...metadata2.review.selections],
99566
- fulfillment: metadata2.review.fulfillment
99567
- };
99568
- } else if (metadata2.purpose === "mastery") {
99569
- selection = {
99570
- kind: "standard-quiz",
99571
- purpose: metadata2.purpose,
99572
- standard: metadata2.mastery.standard
99573
- };
99574
- } else {
99575
- selection = { kind: "fixed-test", purpose: metadata2.purpose };
99576
- }
99577
- return {
99578
- attemptId: input.attemptId,
99579
- responseVersion: metadata2.responseVersion,
99580
- status: "in_progress",
99581
- flow: assessmentFlowForPurpose(metadata2.purpose),
99582
- ...input.activityData ? { activityData: input.activityData } : {},
99583
- assessment: assessmentPresentationForAttempt(input.assessment, input.attemptId),
99584
- responses: metadata2.responses,
99585
- itemSubmissions: metadata2.itemSubmissions,
99586
- score: null,
99587
- selection
99588
- };
99589
- }
99590
- function buildAssessmentAwardRecord(input, mastery) {
99591
- return {
99592
- xpAwarded: input.xpAwarded,
99593
- ...input.masteredUnits !== undefined ? { masteredUnits: input.masteredUnits } : {},
99594
- ...input.masteredUnitsAbsolute !== undefined ? { masteredUnitsAbsolute: input.masteredUnitsAbsolute } : {},
99595
- masteredUnitsApplied: mastery.masteredUnitsApplied,
99596
- ...mastery.pctCompleteApp !== undefined ? { pctCompleteApp: mastery.pctCompleteApp } : {},
99597
- ...mastery.warnings ? { warnings: mastery.warnings } : {}
99598
- };
99599
- }
99600
- function stageAssessmentAttemptSupersession(attempt) {
99601
- Object.assign(attempt.result, ASSESSMENT_ATTEMPT_SUPERSEDED);
99602
- Object.assign(attempt.selection, ASSESSMENT_ATTEMPT_SUPERSEDED);
99603
- }
99604
- function shouldConfirmAssessmentVersionUnavailable(details) {
99605
- return typeof details === "object" && details !== null && "expectedRevision" in details && typeof details.expectedRevision === "string" && !("currentRevision" in details);
99606
- }
99607
- function buildAssessmentAttemptSupersessionUpdate(result) {
99608
- if (result.status !== "active" || !isAssessmentAttemptOpen({
99609
- inProgress: result.inProgress ?? "",
99610
- scoreStatus: result.scoreStatus
99611
- })) {
99612
- return null;
99613
- }
99614
- return {
99615
- status: "active",
99616
- assessmentLineItem: result.assessmentLineItem,
99617
- student: result.student,
99618
- score: result.score ?? 0,
99619
- scoreDate: result.scoreDate,
99620
- ...ASSESSMENT_ATTEMPT_SUPERSEDED,
99621
- metadata: result.metadata ?? {}
99622
- };
99623
- }
99624
- async function resolveAvailableAssessmentAttempt(initialDecision, select3, resume) {
99625
- let decision = initialDecision;
99626
- while (decision?.kind === "resume") {
99627
- const snapshot = await resume(decision);
99628
- if (snapshot !== null) {
99629
- return { kind: "resumed", snapshot };
99630
- }
99631
- decision = select3();
99632
- }
99633
- return { kind: "continue", decision };
99634
- }
99635
- function buildReviewItemResultMetadata(input) {
99636
- const administration = itemAdministration(input.metadata.itemSubmissions, input.selection.itemIdentifier);
99637
- if (!administration) {
99638
- throw new Error(`Cannot create a review result for unadministered candidate ${input.selection.itemIdentifier}`);
99639
- }
99640
- return {
99641
- version: PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_VERSION,
99642
- parentAttemptId: input.parentAttemptId,
99643
- activityId: input.metadata.activityId,
99644
- integrationId: input.metadata.integrationId,
99645
- enrollmentId: input.metadata.enrollmentId,
99646
- bankRevision: input.metadata.review.bankRevision,
99647
- itemIdentifier: input.selection.itemIdentifier,
99648
- standard: input.selection.standard,
99649
- deliveredAt: administration.submittedAt,
99650
- responseVersion: input.responseVersion ?? input.metadata.responseVersion,
99651
- responses: input.metadata.responses[input.selection.itemIdentifier] ?? {},
99652
- ...input.submissionId ? { submissionId: input.submissionId } : {},
99653
- ...input.isCorrect === undefined ? {} : { isCorrect: input.isCorrect }
99654
- };
99655
- }
99656
- function buildOpenReviewChildResult(input) {
99657
- return {
99658
- status: "active",
99659
- assessmentLineItem: { sourcedId: input.childLineItemId },
99660
- student: input.student,
99661
- score: 0,
99662
- scoreDate: input.metadata.deliveredAt,
99663
- ...ASSESSMENT_ATTEMPT_OPEN,
99664
- metadata: {
99665
- [PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY]: input.metadata
99666
- }
99667
- };
99668
- }
99669
- function diagnosticQuestionLineItemId(parentLineItemId, itemIdentifier) {
99670
- return deterministicUUID(["playcademy:diagnostic-question-line-item:v1", parentLineItemId, itemIdentifier].join("\x00"));
99671
- }
99672
- function diagnosticQuestionResultId(parentAttemptId, childLineItemId) {
99673
- return deterministicUUID(["playcademy:diagnostic-question-result:v1", parentAttemptId, childLineItemId].join("\x00"));
99674
- }
99675
- function diagnosticAdministration(input) {
99676
- const ledgerIndex = input.metadata.diagnostic.ledger.findIndex((entry) => entry.routingNodeKey === input.ledgerEntry.routingNodeKey);
99677
- const persistedEntry = input.metadata.diagnostic.ledger[ledgerIndex];
99678
- const administration = input.metadata.itemSubmissions[ledgerIndex];
99679
- 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) {
99680
- throw new Error(`Cannot create a diagnostic result for uncommitted routing node ${input.ledgerEntry.routingNodeKey}`);
99681
- }
99682
- return administration;
99683
- }
99684
- function buildDiagnosticItemResultMetadata(input) {
99685
- const administration = diagnosticAdministration(input);
99686
- return {
99687
- version: PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_VERSION,
99688
- parentAttemptId: input.parentAttemptId,
99689
- activityId: input.metadata.activityId,
99690
- integrationId: input.metadata.integrationId,
99691
- enrollmentId: input.metadata.enrollmentId,
99692
- definitionId: input.metadata.diagnostic.definitionId,
99693
- diagnosticKey: input.metadata.diagnostic.diagnosticKey,
99694
- routingRevision: input.metadata.diagnostic.routingRevision,
99695
- contentRevision: input.metadata.selectedTest.contentRevision,
99696
- stageKey: input.ledgerEntry.stageKey,
99697
- trackKey: input.ledgerEntry.trackKey,
99698
- routingNodeKey: input.ledgerEntry.routingNodeKey,
99699
- itemIdentifier: input.ledgerEntry.itemIdentifier,
99700
- deliveredAt: administration.submittedAt,
99701
- responseVersion: administration.responseVersion,
99702
- responses: input.metadata.responses[input.ledgerEntry.itemIdentifier] ?? {},
99703
- submissionId: administration.submissionId,
99704
- isCorrect: input.ledgerEntry.isCorrect
99705
- };
99706
- }
99707
- function resolveDiagnosticChildResult(input) {
99708
- if (!input.childResult) {
99709
- return { action: "create" };
99710
- }
99711
- const childMetadata = playcademyDiagnosticAssessmentItemResultMetadata(input.childResult.metadata);
99712
- const expectedMetadata = buildDiagnosticItemResultMetadata({
99713
- metadata: input.parentMetadata,
99714
- ledgerEntry: input.ledgerEntry,
99715
- parentAttemptId: input.parentResult.sourcedId
99716
- });
99717
- if (input.childResult.assessmentLineItem.sourcedId !== input.childLineItemId || input.childResult.student.sourcedId !== input.parentResult.student.sourcedId || !childMetadata || canonicalJson2(childMetadata) !== canonicalJson2(expectedMetadata)) {
99718
- return { action: "restore", reason: "incompatible" };
99719
- }
99720
- return input.childResult.status === "active" ? { action: "reuse", metadata: expectedMetadata } : { action: "restore", reason: "inactive" };
99721
- }
99722
- function buildFinalizedDiagnosticChildResult(input) {
99723
- const administration = diagnosticAdministration(input);
99724
- const metadata2 = buildDiagnosticItemResultMetadata(input);
99725
- return {
99726
- resultUpdate: {
99727
- status: "active",
99728
- assessmentLineItem: { sourcedId: input.childLineItemId },
99729
- student: input.student,
99730
- score: administration.score.earned,
99731
- scoreDate: administration.submittedAt,
99732
- ...ASSESSMENT_ATTEMPT_COMPLETED,
99733
- metadata: {
99734
- [PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY]: metadata2
99735
- }
99736
- }
99737
- };
99738
- }
99739
- function buildFinalizedReviewChildResult(input) {
99740
- const metadata2 = buildReviewItemResultMetadata({
99741
- metadata: input.metadata,
99742
- selection: input.selection,
99743
- parentAttemptId: input.parentAttemptId,
99744
- submissionId: input.submissionId,
99745
- isCorrect: input.itemResult.isCorrect
99746
- });
99747
- return {
99748
- resultUpdate: {
99749
- status: "active",
99750
- assessmentLineItem: { sourcedId: input.childLineItemId },
99751
- student: input.student,
99752
- score: input.itemResult.score.earned,
99753
- scoreDate: input.timestamp,
99754
- ...ASSESSMENT_ATTEMPT_COMPLETED,
99755
- metadata: {
99756
- [PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY]: metadata2
99757
- }
99758
- },
99759
- outcome: {
99760
- itemIdentifier: input.selection.itemIdentifier,
99761
- standard: input.selection.standard,
99762
- answered: Object.keys(input.metadata.responses[input.selection.itemIdentifier] ?? {}).length > 0,
99763
- score: input.itemResult.score,
99764
- isCorrect: input.itemResult.isCorrect
99765
- }
99766
- };
99767
- }
99768
- function resolveReviewChildResult(input) {
99769
- if (!input.childResult) {
99770
- return { action: "create" };
99771
- }
99772
- const childMetadata = playcademyAssessmentItemResultMetadata(input.childResult.metadata);
99773
- const expectedResponses = input.parentMetadata.responses[input.selection.itemIdentifier] ?? {};
99774
- const administration = itemAdministration(input.parentMetadata.itemSubmissions, input.selection.itemIdentifier);
99775
- 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)) {
99776
- return { action: "restore", reason: "incompatible" };
99777
- }
99778
- return input.childResult.status === "active" ? { action: "reuse", metadata: childMetadata } : { action: "restore", reason: "inactive" };
99779
- }
99780
- function buildAssessmentSubmitResult(input) {
99781
- if (!input.metadata.score || !input.metadata.submissionId) {
99782
- return null;
99783
- }
99784
- if (input.metadata.purpose === "diagnostic" && input.metadata.diagnostic) {
99785
- const tracks = input.metadata.completion?.diagnosticTracks;
99786
- return tracks ? {
99787
- purpose: input.metadata.purpose,
99788
- attemptId: input.attemptId,
99789
- submissionId: input.metadata.submissionId,
99790
- diagnosticKey: input.metadata.diagnostic.diagnosticKey,
99791
- responseVersion: input.metadata.responseVersion,
99792
- status: "awaiting_award",
99793
- tracks
99794
- } : null;
99795
- }
99796
- const base = {
99797
- attemptId: input.attemptId,
99798
- submissionId: input.metadata.submissionId,
99799
- responseVersion: input.metadata.responseVersion,
99800
- status: "awaiting_award",
99801
- responses: input.metadata.responses,
99802
- score: input.metadata.score
99803
- };
99804
- if (input.metadata.purpose === "review") {
99805
- const itemOutcomes = input.metadata.completion?.itemOutcomes;
99806
- return itemOutcomes ? { ...base, purpose: input.metadata.purpose, itemOutcomes } : null;
99807
- }
99808
- return { ...base, purpose: input.metadata.purpose };
99809
- }
99810
- function assessmentAwardFromResult(result, metadata2) {
99811
- if (metadata2.award) {
99812
- return metadata2.award;
99813
- }
99814
- const xp = result.metadata?.xp;
99815
- return typeof xp === "number" && Number.isFinite(xp) && xp >= 0 ? { xpAwarded: xp, masteredUnitsApplied: 0 } : null;
99816
- }
99817
- function buildCompletedAssessmentSubmitResult(input) {
99818
- const attemptId = input.result.sourcedId;
99819
- const award = assessmentAwardFromResult(input.result, input.metadata);
99820
- if (award === null) {
99821
- throw new GoneError("This completed assessment is missing its recorded XP.", {
99822
- attemptId
99823
- });
99824
- }
99825
- const submitted = buildAssessmentSubmitResult({ attemptId, metadata: input.metadata });
99826
- if (!submitted) {
99827
- throw new GoneError("This completed assessment has incomplete persisted results.", {
99828
- attemptId
99829
- });
99830
- }
99831
- return {
99832
- ...submitted,
99833
- status: "completed",
99834
- xpAwarded: award.xpAwarded,
99835
- masteredUnitsApplied: award.masteredUnitsApplied,
99836
- ...award.pctCompleteApp !== undefined ? { pctCompleteApp: award.pctCompleteApp } : {},
99837
- ...award.warnings ? { warnings: award.warnings } : {}
99838
- };
99839
- }
99840
- function assessmentAttemptId(input) {
99841
- return deterministicUUID([
99842
- "playcademy:assessment-attempt",
99843
- input.gameId,
99844
- input.studentId,
99845
- input.courseId,
99846
- input.activityId,
99847
- input.purpose,
99848
- String(input.attemptNumber)
99849
- ].join("\x00"));
99850
- }
99851
- async function findLatestAssessmentResult(results, match) {
99852
- let latest = null;
99853
- let latestTimestamp = 0;
99854
- for await (const result of results) {
99855
- const resultTimestamp = assessmentAttemptTimestamp(result.scoreDate);
99856
- if (latest && resultTimestamp < latestTimestamp) {
99857
- break;
99858
- }
99859
- const matched = match(result);
99860
- if (matched !== null && (!latest || compareNewestCompletedAssessmentAttempt({ attemptId: result.sourcedId, scoreDate: result.scoreDate }, {
99861
- attemptId: latest.result.sourcedId,
99862
- scoreDate: latest.result.scoreDate
99863
- }) < 0)) {
99864
- latest = { result, match: matched };
99865
- latestTimestamp = resultTimestamp;
99866
- }
99867
- }
99868
- return latest;
99869
- }
99870
- function playableChoice(choice) {
99871
- return {
99872
- identifier: choice.identifier,
99873
- content: choice.content,
99874
- ...choice.contentNodes ? { contentNodes: choice.contentNodes } : {}
99875
- };
99876
- }
99877
- function playableAssociableChoice(choice) {
99878
- const matchMax = qtiAttributeCapacity(choice.attributes, "match-max", 0);
99879
- if (matchMax === null) {
99880
- throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", "The selected assessment contains an invalid Match capacity.");
99881
- }
99882
- return {
99883
- ...playableChoice(choice),
99884
- matchMax
99885
- };
99886
- }
99887
- function requireChoices2(choices) {
99888
- if (choices.length === 0 || choices.some((candidate) => !candidate.identifier)) {
99889
- throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", "The selected assessment contains an interaction Playcademy cannot render.");
99890
- }
99891
- return choices;
99892
- }
99893
- function isPlayableAssessmentShape(value) {
99894
- return PLAYABLE_SHAPE_SET.has(value);
99895
- }
99896
- function playableGraphic(graphic) {
99897
- return {
99898
- src: graphic.src,
99899
- ...graphic.width === undefined ? {} : { width: graphic.width },
99900
- ...graphic.height === undefined ? {} : { height: graphic.height },
99901
- ...graphic.description ? { description: graphic.description } : {}
99902
- };
99903
- }
99904
- function requireGraphic(questionIdentifier, graphic) {
99905
- if (!graphic) {
99906
- throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${questionIdentifier} is missing a required graphic.`);
99907
- }
99908
- return playableGraphic(graphic);
99909
- }
99910
- function requireHotspots(questionIdentifier, hotspots) {
99911
- if (hotspots.length === 0) {
99912
- throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${questionIdentifier} declares no graphic regions.`);
99913
- }
99914
- return hotspots.map((hotspot) => ({
99915
- identifier: hotspot.identifier,
99916
- shape: isPlayableAssessmentShape(hotspot.shape) ? hotspot.shape : "default",
99917
- coords: [...hotspot.coords],
99918
- ...hotspot.matchMax === undefined ? {} : { matchMax: hotspot.matchMax },
99919
- ...hotspot.description ? { description: hotspot.description } : {}
99920
- }));
99921
- }
99922
- function parseInteraction2(questionIdentifier, interaction) {
99923
- const { type } = interaction;
99924
- const responseIdentifier = interaction.attributes["response-identifier"];
99925
- if (!responseIdentifier) {
99926
- throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${questionIdentifier} has no response identifier.`);
99927
- }
99928
- const prompt = interaction.embeddedPrompt ? { prompt: interaction.embeddedPrompt } : {};
99929
- if (type === "choice") {
99930
- const choices = requireChoices2(interaction.choices.map(playableChoice));
99931
- return {
99932
- type,
99933
- responseIdentifier,
99934
- ...prompt,
99935
- choices,
99936
- minChoices: qtiAttributeNumber(interaction.attributes, "min-choices", 1),
99937
- maxChoices: qtiAttributeMaximum(interaction.attributes, "max-choices", 1, choices.length)
99938
- };
99939
- }
99940
- if (type === "inline-choice") {
99941
- return {
99942
- type,
99943
- responseIdentifier,
99944
- ...prompt,
99945
- choices: requireChoices2(interaction.choices.map(playableChoice))
99946
- };
99947
- }
99948
- if (type === "text-entry") {
99949
- const expectedLength = qtiAttributeNumber(interaction.attributes, "expected-length", 0);
99950
- return {
99951
- type,
99952
- responseIdentifier,
99953
- ...prompt,
99954
- ...Number.isInteger(expectedLength) && expectedLength > 0 ? { expectedLength } : {},
99955
- ...interaction.attributes.placeholder ? { placeholder: interaction.attributes.placeholder } : {}
99956
- };
99957
- }
99958
- if (type === "order") {
99959
- return {
99960
- type,
99961
- responseIdentifier,
99962
- ...prompt,
99963
- choices: requireChoices2(interaction.choices.map(playableChoice)),
99964
- shuffle: qtiAttributeBoolean(interaction.attributes, "shuffle")
99965
- };
99966
- }
99967
- if (type === "match") {
99968
- const sets = interaction.choiceSets;
99969
- if (sets.length !== 2) {
99970
- throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", "Match interactions require exactly two choice sets.");
99971
- }
99972
- const sourceChoices = requireChoices2(sets[0].map(playableAssociableChoice));
99973
- const targetChoices = requireChoices2(sets[1].map(playableAssociableChoice));
99974
- return {
99975
- type,
99976
- responseIdentifier,
99977
- ...prompt,
99978
- sourceChoices,
99979
- targetChoices,
99980
- maxAssociations: qtiAttributeMaximum(interaction.attributes, "max-associations", 1, sourceChoices.length * targetChoices.length),
99981
- shuffle: qtiAttributeBoolean(interaction.attributes, "shuffle")
99982
- };
99983
- }
99984
- if (type === "hotspot") {
99985
- const image = requireGraphic(questionIdentifier, interaction.graphic);
99986
- const hotspots = requireHotspots(questionIdentifier, interaction.hotspots);
99987
- return {
99988
- type,
99989
- responseIdentifier,
99990
- ...prompt,
99991
- image,
99992
- hotspots,
99993
- minChoices: qtiAttributeNumber(interaction.attributes, "min-choices", 1),
99994
- maxChoices: qtiAttributeMaximum(interaction.attributes, "max-choices", 1, hotspots.length)
99995
- };
99996
- }
99997
- if (type === "gap-match") {
99998
- if (interaction.gapTexts.length === 0 || interaction.gaps.length === 0 || interaction.textWithGaps === undefined || !interaction.gapContent || !qtiGapContentMatchesGaps(interaction.gapContent, interaction.gaps)) {
99999
- throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${questionIdentifier} has an incomplete gap-match interaction.`);
100000
- }
100001
- return {
100002
- type,
100003
- responseIdentifier,
100004
- ...prompt,
100005
- content: interaction.textWithGaps,
100006
- contentNodes: interaction.gapContent,
100007
- gapTexts: interaction.gapTexts.map((gapText) => ({
100008
- identifier: gapText.identifier,
100009
- content: gapText.content,
100010
- matchMax: gapText.matchMax
100011
- })),
100012
- gaps: [...interaction.gaps],
100013
- maxAssociations: qtiAttributeNumber(interaction.attributes, "max-associations", 0)
100014
- };
100015
- }
100016
- if (type === "graphic-gap-match") {
100017
- const image = requireGraphic(questionIdentifier, interaction.graphic);
100018
- const hotspots = requireHotspots(questionIdentifier, interaction.hotspots);
100019
- if (interaction.gapImages.length === 0) {
100020
- throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${questionIdentifier} has no draggable gap images.`);
100021
- }
100022
- return {
100023
- type,
100024
- responseIdentifier,
100025
- ...prompt,
100026
- image,
100027
- gapImages: interaction.gapImages.map((gapImage) => ({
100028
- identifier: gapImage.identifier,
100029
- ...gapImage.graphic ? { image: playableGraphic(gapImage.graphic) } : {},
100030
- matchMax: gapImage.matchMax,
100031
- ...gapImage.description ? { description: gapImage.description } : {}
100032
- })),
100033
- hotspots,
100034
- maxAssociations: qtiAttributeNumber(interaction.attributes, "max-associations", 0)
100035
- };
100036
- }
100037
- if (type === "select-point") {
100038
- return {
100039
- type,
100040
- responseIdentifier,
100041
- ...prompt,
100042
- image: requireGraphic(questionIdentifier, interaction.graphic),
100043
- minChoices: qtiAttributeNumber(interaction.attributes, "min-choices", 0),
100044
- maxChoices: qtiAttributeNumber(interaction.attributes, "max-choices", 0)
100045
- };
100046
- }
100047
- if (type === "graphic-associate") {
100048
- return {
100049
- type,
100050
- responseIdentifier,
100051
- ...prompt,
100052
- image: requireGraphic(questionIdentifier, interaction.graphic),
100053
- hotspots: requireHotspots(questionIdentifier, interaction.hotspots),
100054
- maxAssociations: qtiAttributeNumber(interaction.attributes, "max-associations", 1)
100055
- };
100056
- }
100057
- if (type === "position-object") {
100058
- return {
100059
- type,
100060
- responseIdentifier,
100061
- ...prompt,
100062
- stage: requireGraphic(questionIdentifier, interaction.graphic),
100063
- object: requireGraphic(questionIdentifier, interaction.objectGraphic),
100064
- maxPlacements: qtiAttributeNumber(interaction.attributes, "max-choices", 1)
100065
- };
100066
- }
100067
- if (type === "hottext") {
100068
- const segments = interaction.textSegments;
100069
- if (!segments.some((segment) => segment.selectable) || segments.some((segment) => segment.selectable && (!segment.identifier || !segment.content))) {
100070
- throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", "Hottext interactions require identified selectable content.");
100071
- }
100072
- return {
100073
- type,
100074
- responseIdentifier,
100075
- ...prompt,
100076
- segments,
100077
- maxChoices: qtiAttributeMaximum(interaction.attributes, "max-choices", 1, segments.filter((segment) => segment.selectable).length)
100078
- };
100079
- }
100080
- const unsupported = type;
100081
- throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${questionIdentifier} uses unsupported interaction ${unsupported}.`);
100082
- }
100083
- function normalizedQtiItem(question) {
100084
- if (!question.rawXml) {
100085
- throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${question.identifier} has no authoritative QTI XML.`);
100086
- }
100087
- try {
100088
- return parseQtiItemXml(question.rawXml);
100089
- } catch (error88) {
100090
- throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${question.identifier} contains invalid QTI XML.`, { reason: errorMessage(error88) });
100091
- }
100092
- }
100093
- function parsePlayableItem(question) {
100094
- const parsed = normalizedQtiItem(question);
100095
- const interactions = playableQtiInteractions(parsed);
100096
- if (!interactions) {
100097
- throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${question.identifier} must contain only supported interactions with unique response identifiers.`);
100098
- }
100099
- return {
100100
- identifier: question.identifier,
100101
- title: question.title,
100102
- prompt: parsed.bodyTextWithBlanks,
100103
- ...parsed.bodyContent ? { promptContent: parsed.bodyContent } : {},
100104
- maxScore: qtiItemMaxScore(parsed),
100105
- interactions: interactions.map((interaction) => parseInteraction2(question.identifier, interaction))
100106
- };
100107
- }
100108
- function assessmentContentRevision(test, questions) {
100109
- return sha256Hex(canonicalJson2({
100110
- identifier: test.identifier,
100111
- title: test.title,
100112
- qtiVersion: test.qtiVersion,
100113
- parts: test["qti-test-part"],
100114
- questions: questions.questions.map(({ reference, question }) => ({
100115
- identifier: reference.identifier,
100116
- rawXml: question.rawXml
100117
- }))
100118
- }));
100119
- }
100120
- async function buildPlayableAssessment(test, questions) {
100121
- return {
100122
- identifier: test.identifier,
100123
- contractVersion: PLAYCADEMY_ASSESSMENT_CONTRACT_VERSION,
100124
- contentRevision: await assessmentContentRevision(test, questions),
100125
- title: test.title,
100126
- items: questions.questions.map(({ question }) => parsePlayableItem(question))
100127
- };
100128
- }
100129
- function assessmentFixtureScoringKeys(assessment, questions) {
100130
- const questionByIdentifier = new Map(questions.questions.map(({ question }) => [question.identifier, question]));
100131
- const correctResponses = {};
100132
- const responseAreas = {};
100133
- for (const item of assessment.items) {
100134
- const question = questionByIdentifier.get(item.identifier);
100135
- const normalizedDeclarations = question ? normalizedQtiItem(question).responseDeclarations : [];
100136
- const itemResponses = {};
100137
- for (const interaction of item.interactions) {
100138
- const responseIdentifier = interaction.responseIdentifier;
100139
- const declaration = question?.responseDeclarations?.find((candidate) => candidate.identifier === responseIdentifier);
100140
- const declaredValues = declaration?.correctResponse?.value ?? [];
100141
- const normalizedDeclaration = normalizedDeclarations.find((candidate) => candidate.identifier === responseIdentifier);
100142
- const values = declaredValues.length > 0 ? declaredValues : normalizedDeclaration?.correctValues ?? [];
100143
- const areas = interaction.type === "select-point" || interaction.type === "position-object" ? normalizedDeclaration?.areaMapping?.entries.filter((entry) => entry.value > 0 && isPlayableAssessmentShape(entry.shape)) : undefined;
100144
- if (areas?.length) {
100145
- (responseAreas[item.identifier] ??= {})[responseIdentifier] = areas.map((entry) => ({
100146
- shape: entry.shape,
100147
- coords: [...entry.coords],
100148
- value: entry.value
100149
- }));
100150
- }
100151
- if (values.length > 0) {
100152
- const cardinality = declaration?.cardinality ?? normalizedDeclaration?.cardinality;
100153
- itemResponses[responseIdentifier] = cardinality === "single" && values.length === 1 ? values[0] : values;
100154
- } else if (!areas?.length) {
100155
- throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${item.identifier} has no fixture scoring key.`);
100156
- }
100157
- }
100158
- correctResponses[item.identifier] = itemResponses;
100159
- }
100160
- return {
100161
- correctResponses,
100162
- ...Object.keys(responseAreas).length > 0 ? { responseAreas } : {}
100163
- };
100164
- }
100165
- function prepareDiagnosticAssessmentResponses(assessment, current, input) {
100166
- const update2 = { [input.itemIdentifier]: input.responses };
100167
- validateAssessmentResponseUpdate(assessment, update2);
100168
- const responses = applyAssessmentResponseUpdate(current, update2);
100169
- const item = assessment.items.find((candidate) => candidate.identifier === input.itemIdentifier);
100170
- const itemResponses = responses[input.itemIdentifier];
100171
- const missingResponse = item?.interactions.find((interaction) => itemResponses?.[interaction.responseIdentifier] === undefined);
100172
- if (!item || !itemResponses || missingResponse) {
100173
- throw new AssessmentRuntimeError("INVALID_RESPONSE", `Diagnostic item ${input.itemIdentifier} requires a response for every interaction.`, {
100174
- itemIdentifier: input.itemIdentifier,
100175
- ...missingResponse ? { responseIdentifier: missingResponse.responseIdentifier } : {}
100176
- });
100177
- }
100178
- validateAssessmentResponses(assessment, { [input.itemIdentifier]: itemResponses });
100179
- return responses;
100180
- }
100181
- function validateAssessmentResponseUpdate(assessment, update2) {
100182
- const message = assessmentResponseUpdateValidationMessage(assessment, update2);
100183
- if (message) {
100184
- throw new AssessmentRuntimeError("INVALID_RESPONSE", message);
100185
- }
100186
- }
100187
- function validateAssessmentResponses(assessment, responses) {
100188
- const message = assessmentResponseValidationMessage(assessment, responses);
100189
- if (message) {
100190
- throw new AssessmentRuntimeError("INVALID_RESPONSE", message);
100191
- }
100192
- }
100193
- function validateAssessmentDraftResponses(assessment, responses) {
100194
- const message = assessmentDraftResponseValidationMessage(assessment, responses);
100195
- if (message) {
100196
- throw new AssessmentRuntimeError("INVALID_RESPONSE", message);
100197
- }
100198
- }
100199
- function assessmentScoringRequests(assessment, responses) {
100200
- return assessment.items.map((item) => ({
100201
- itemIdentifier: item.identifier,
100202
- maxScore: item.maxScore,
100203
- requests: item.interactions.map((interaction) => ({
100204
- identifier: interaction.responseIdentifier,
100205
- response: responses[item.identifier]?.[interaction.responseIdentifier] ?? ""
100206
- }))
100207
- }));
100208
- }
100209
- function buildAssessmentResultMetadata(input) {
100210
- return {
100211
- [PLAYCADEMY_ASSESSMENT_RESULT_METADATA_KEY]: input.metadata,
100212
- assessmentPurpose: input.metadata.purpose,
100213
- ...assessmentPurposeClassificationFields2(input.metadata.purpose, input)
100214
- };
100215
- }
100216
- function mergeAssessmentRuntimeMetadata(resultMetadata, metadata2) {
100217
- return {
100218
- ...resultMetadata,
100219
- [PLAYCADEMY_ASSESSMENT_RESULT_METADATA_KEY]: metadata2
100220
- };
100221
- }
100222
- function aggregateAssessmentScore(itemScores, possible) {
100223
- const earned = itemScores.reduce((sum, itemScore) => sum + (Number.isFinite(itemScore) ? itemScore : 0), 0);
100224
- return {
100225
- earned,
100226
- possible,
100227
- normalized: possible === 0 ? 0 : Math.min(1, Math.max(0, earned / possible))
100228
- };
100229
- }
100230
- function countCorrectQuestions(verdicts) {
100231
- return verdicts.filter((verdict) => verdict === true).length;
100232
- }
100233
- function assessmentScoreFromPercentage(percentage, possible) {
100234
- const normalized = Math.min(1, Math.max(0, (percentage ?? 0) / 100));
100235
- const earned = Math.round(normalized * possible * 1e6) / 1e6;
100236
- return { earned, possible, normalized };
100237
- }
100238
- function buildAssessmentResultSubmission(input) {
100239
- const metadata2 = {
100240
- ...input.metadata,
100241
- submissionId: input.submissionId,
100242
- score: input.score,
100243
- completion: {
100244
- testName: input.testName,
100245
- courseName: input.courseName,
100246
- totalQuestions: input.totalQuestions,
100247
- correctQuestions: input.correctQuestions,
100248
- ...input.session ? { session: input.session } : {},
100249
- ...input.itemOutcomes ? { itemOutcomes: input.itemOutcomes } : {},
100250
- ...input.diagnosticTracks ? { diagnosticTracks: input.diagnosticTracks } : {}
100251
- },
100252
- updatedAt: input.timestamp
100253
- };
100254
- return {
100255
- metadata: metadata2,
100256
- resultUpdate: {
100257
- status: input.result.status,
100258
- assessmentLineItem: input.result.assessmentLineItem,
100259
- student: input.result.student,
100260
- score: input.score.normalized * 100,
100261
- scoreDate: input.timestamp,
100262
- ...ASSESSMENT_ATTEMPT_AWAITING_AWARD,
100263
- metadata: mergeAssessmentRuntimeMetadata(input.result.metadata, metadata2)
100264
- }
100265
- };
100266
- }
100267
- function buildAssessmentResultAwardFinalization(input) {
100268
- const completion = input.metadata.completion;
100269
- if (!completion || completion.totalQuestions === undefined) {
100270
- throw new Error("A scored assessment must retain its completion facts before finalization");
100271
- }
100272
- const metadata2 = {
100273
- ...input.metadata,
100274
- finalizedAt: input.timestamp,
100275
- updatedAt: input.timestamp,
100276
- award: input.award
100277
- };
100278
- const masteryRequested = input.award.masteredUnits !== undefined || input.award.masteredUnitsAbsolute !== undefined;
100279
- return {
100280
- metadata: metadata2,
100281
- resultUpdate: {
100282
- status: input.result.status,
100283
- assessmentLineItem: input.result.assessmentLineItem,
100284
- student: input.result.student,
100285
- score: input.result.score ?? 0,
100286
- scoreDate: input.result.scoreDate,
100287
- ...ASSESSMENT_ATTEMPT_COMPLETED,
100288
- metadata: {
100289
- ...mergeAssessmentRuntimeMetadata(input.result.metadata, metadata2),
100290
- xp: input.award.xpAwarded,
100291
- ...masteryRequested ? { masteredUnits: input.award.masteredUnitsApplied } : {},
100292
- totalQuestions: completion.totalQuestions,
100293
- correctQuestions: completion.correctQuestions,
100294
- appName: input.appName
100295
- }
100296
- }
100297
- };
100298
- }
100299
- var PLAYABLE_SHAPE_SET;
100300
- var init_timeback_assessment_runtime_util = __esm(() => {
100301
- init_src();
100302
- init_assessment_runtime2();
100303
- init_qti();
100304
- init_types2();
100305
- init_utils6();
100306
- init_timeback4();
100307
- init_uuid();
100308
- init_errors2();
100309
- PLAYABLE_SHAPE_SET = new Set(PLAYABLE_ASSESSMENT_SHAPES);
100310
- });
100311
-
100312
101452
  // ../api-core/src/utils/timeback-review-mapping.util.ts
100313
101453
  function reviewBankTestItemIdentifiers(test) {
100314
101454
  return qtiTestParts(test).flatMap((part) => part["qti-assessment-section"].flatMap((section) => (section["qti-assessment-item-ref"] ?? []).map((reference) => reference.identifier)));
@@ -100562,7 +101702,7 @@ async function prepareReviewMappingUpdate(test, questions) {
100562
101702
  }
100563
101703
  };
100564
101704
  }
100565
- var PLAYCADEMY_REVIEW_BANK_SINK_METADATA_KEY = "playcademyReviewBankSink", PLAYCADEMY_REVIEW_BANK_SINK_VERSION = 1, StoredSinkWarningSchema, ReviewBankSinkMarkerSchema;
101705
+ var PLAYCADEMY_REVIEW_BANK_TITLE = "Review Bank", PLAYCADEMY_REVIEW_BANK_SINK_METADATA_KEY = "playcademyReviewBankSink", PLAYCADEMY_REVIEW_BANK_SINK_VERSION = 1, StoredSinkWarningSchema, ReviewBankSinkMarkerSchema;
100566
101706
  var init_timeback_review_mapping_util = __esm(() => {
100567
101707
  init_esm();
100568
101708
  init_assessment_runtime2();
@@ -100605,6 +101745,259 @@ var init_timeback_review_mapping_util = __esm(() => {
100605
101745
  });
100606
101746
  });
100607
101747
 
101748
+ // ../api-core/src/utils/timeback-assessment-artifacts.util.ts
101749
+ async function storeAndVerifyCompiledAssessmentArtifacts(input) {
101750
+ await input.store(input.compiled);
101751
+ const assessment = await input.loadPlayable(input.compiled.identity);
101752
+ const scoring = assessment ? await input.loadMatchedScoring(assessment, input.compiled.identity) : null;
101753
+ if (!assessment || !scoring) {
101754
+ throw new ServiceUnavailableError(`Prepared ${input.label} could not be verified.`);
101755
+ }
101756
+ return assessment;
101757
+ }
101758
+ function assertSourceIdentity(source) {
101759
+ if (source.test.identifier !== source.questions.assessmentTest) {
101760
+ throw new ValidationError(`QTI questions for ${source.questions.assessmentTest} do not describe ${source.test.identifier}`);
101761
+ }
101762
+ }
101763
+ function sourceItemIdentifiers(source) {
101764
+ return source.questions.questions.map(({ question }) => question.identifier);
101765
+ }
101766
+ function compileScoringArtifact(questions) {
101767
+ try {
101768
+ return compileQtiScoringArtifact(questions);
101769
+ } catch (error88) {
101770
+ if (error88 instanceof QtiScoringArtifactValidationError) {
101771
+ throw new ValidationError(error88.message);
101772
+ }
101773
+ throw error88;
101774
+ }
101775
+ }
101776
+ function qtiFallbackItemCount(compiled) {
101777
+ return compiled.scoringPayload.items.filter((item) => item.strategy === "qti").length;
101778
+ }
101779
+ async function compileManagedAssessmentArtifacts(input) {
101780
+ assertSourceIdentity(input.source);
101781
+ const playablePayload = await buildPlayableAssessment(input.source.test, input.source.questions);
101782
+ const scoring = compileScoringArtifact(input.source.questions);
101783
+ const diagnostic = input.diagnosticRoutingManifest ? {
101784
+ routingRevision: await diagnosticRoutingRevision(input.diagnosticRoutingManifest),
101785
+ routingManifest: input.diagnosticRoutingManifest
101786
+ } : undefined;
101787
+ return {
101788
+ identity: {
101789
+ kind: "assessment",
101790
+ qtiTestIdentifier: input.source.test.identifier,
101791
+ contentRevision: playablePayload.contentRevision
101792
+ },
101793
+ playablePayload,
101794
+ scoringPayload: {
101795
+ ...scoring,
101796
+ ...diagnostic ? { diagnostic } : {}
101797
+ }
101798
+ };
101799
+ }
101800
+ async function compileReviewBankArtifacts(input) {
101801
+ assertSourceIdentity(input.source);
101802
+ if (input.source.test.identifier !== input.bank.bankIdentifier) {
101803
+ throw new ValidationError(`Review bank ${input.bank.bankIdentifier} does not describe ${input.source.test.identifier}`);
101804
+ }
101805
+ const itemIdentifiers = sourceItemIdentifiers(input.source);
101806
+ const indexedItemIdentifiers = input.bank.items.map((item) => item.itemIdentifier);
101807
+ if (itemIdentifiers.length !== indexedItemIdentifiers.length || itemIdentifiers.some((identifier, index2) => identifier !== indexedItemIdentifiers[index2])) {
101808
+ throw new ValidationError("Review artifact content does not match its revisioned index");
101809
+ }
101810
+ const compiledPlayable = await buildPlayableAssessment({ ...input.source.test, title: PLAYCADEMY_REVIEW_BANK_TITLE }, input.source.questions);
101811
+ const playablePayload = {
101812
+ ...compiledPlayable,
101813
+ contentRevision: input.bank.sourceContentRevision
101814
+ };
101815
+ return {
101816
+ identity: {
101817
+ kind: "review-bank",
101818
+ qtiTestIdentifier: input.bank.bankIdentifier,
101819
+ sourceContentRevision: input.bank.sourceContentRevision,
101820
+ bankRevision: input.bank.bankRevision
101821
+ },
101822
+ playablePayload,
101823
+ scoringPayload: {
101824
+ ...compileScoringArtifact(input.source.questions),
101825
+ reviewBank: {
101826
+ sourceContentRevision: input.bank.sourceContentRevision,
101827
+ bankRevision: input.bank.bankRevision,
101828
+ items: input.bank.items
101829
+ }
101830
+ }
101831
+ };
101832
+ }
101833
+ var init_timeback_assessment_artifacts_util = __esm(() => {
101834
+ init_assessment_runtime2();
101835
+ init_qti();
101836
+ init_errors2();
101837
+ init_timeback_assessment_runtime_util();
101838
+ init_timeback_review_mapping_util();
101839
+ });
101840
+
101841
+ // ../api-core/src/utils/timeback-assessment-preparation.util.ts
101842
+ class AssessmentPreparationFlights {
101843
+ limit;
101844
+ pending = new Map;
101845
+ constructor(limit) {
101846
+ if (!Number.isInteger(limit) || limit <= 0) {
101847
+ throw new Error("Assessment preparation flight limit must be a positive integer");
101848
+ }
101849
+ this.limit = limit;
101850
+ }
101851
+ claim(key, operation) {
101852
+ const existing = this.pending.get(key);
101853
+ if (existing) {
101854
+ return { status: "coalesced", promise: existing };
101855
+ }
101856
+ if (this.pending.size >= this.limit) {
101857
+ return { status: "at_capacity" };
101858
+ }
101859
+ const pending = Promise.resolve().then(operation).finally(() => {
101860
+ if (this.pending.get(key) === pending) {
101861
+ this.pending.delete(key);
101862
+ }
101863
+ });
101864
+ this.pending.set(key, pending);
101865
+ return { status: "started", promise: pending };
101866
+ }
101867
+ }
101868
+ function assessmentPreparationRequestHash(input) {
101869
+ return sha256Hex(canonicalJson2(input));
101870
+ }
101871
+ function assessmentPreparationFlightKey(input) {
101872
+ return canonicalJson2(input);
101873
+ }
101874
+ async function assessmentPreparationReceiptMatches(plan, request) {
101875
+ 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);
101876
+ }
101877
+ function resolveAssessmentPreparationSelection(decision, candidateCourseCount) {
101878
+ if (decision?.kind === "resume") {
101879
+ return {
101880
+ status: "blocked",
101881
+ result: { status: "canonical_required", reason: "resumable_attempt" }
101882
+ };
101883
+ }
101884
+ if (candidateCourseCount !== 1) {
101885
+ return {
101886
+ status: "blocked",
101887
+ result: { status: "unprepared", reason: "prerequisite_unavailable" }
101888
+ };
101889
+ }
101890
+ return decision ? { status: "ready", decision } : { status: "unavailable" };
101891
+ }
101892
+ function preparedAssessmentIntegration(plan, candidates) {
101893
+ if (candidates.rows.length !== 1) {
101894
+ return null;
101895
+ }
101896
+ const integration = candidates.rows[0];
101897
+ const enrollment = candidates.enrollmentByCourse.get(integration.courseId);
101898
+ if (integration.id !== plan.integrationId || integration.courseId !== plan.courseId || enrollment?.id !== plan.enrollmentId) {
101899
+ return null;
101900
+ }
101901
+ return integration;
101902
+ }
101903
+ function preparedAssessmentTestIsLive(plan, tests) {
101904
+ if (plan.purpose === "review" && tests.length !== 1) {
101905
+ return false;
101906
+ }
101907
+ return tests.some((test) => test.assessmentKey === plan.selectedTest.assessmentKey && test.qtiTestIdentifier === plan.selectedTest.identifier && (plan.purpose !== "diagnostic" || test.id === plan.diagnostic.definitionId));
101908
+ }
101909
+ function preparedAssessmentArtifactIdentity(plan) {
101910
+ return plan.purpose === "review" ? {
101911
+ kind: "review-bank",
101912
+ qtiTestIdentifier: plan.selectedTest.identifier,
101913
+ sourceContentRevision: plan.selectedTest.contentRevision,
101914
+ bankRevision: plan.review.bankRevision
101915
+ } : {
101916
+ kind: "assessment",
101917
+ qtiTestIdentifier: plan.selectedTest.identifier,
101918
+ contentRevision: plan.selectedTest.contentRevision
101919
+ };
101920
+ }
101921
+ function preparedAssessmentScoringIsCompatible(plan, scoring) {
101922
+ return Boolean(scoring && (plan.purpose !== "diagnostic" || scoring.diagnostic?.routingRevision === plan.diagnostic.routingRevision));
101923
+ }
101924
+ function preparedReviewBankIndex(plan) {
101925
+ return {
101926
+ bankIdentifier: plan.selectedTest.identifier,
101927
+ sourceContentRevision: plan.selectedTest.contentRevision,
101928
+ bankRevision: plan.review.bankRevision,
101929
+ items: plan.review.selections.map((selection) => ({
101930
+ itemIdentifier: selection.itemIdentifier,
101931
+ standards: [selection.standard]
101932
+ }))
101933
+ };
101934
+ }
101935
+ function nextAssessmentAttemptNumber(attempts) {
101936
+ return Math.max(0, ...[...attempts].map((entry) => entry.metadata.attemptNumber)) + 1;
101937
+ }
101938
+ function buildPreparedAssessmentMetadata(plan, timestamp6, testName) {
101939
+ const base = {
101940
+ version: PLAYCADEMY_ASSESSMENT_RESULT_METADATA_VERSION,
101941
+ activityId: plan.activityId,
101942
+ courseId: plan.courseId,
101943
+ integrationId: plan.integrationId,
101944
+ enrollmentId: plan.enrollmentId,
101945
+ selectedTest: plan.selectedTest,
101946
+ attemptNumber: plan.attemptNumber,
101947
+ responseVersion: 0,
101948
+ responses: {},
101949
+ itemSubmissions: [],
101950
+ startedAt: timestamp6,
101951
+ updatedAt: timestamp6
101952
+ };
101953
+ if (plan.purpose === "mastery") {
101954
+ return {
101955
+ ...base,
101956
+ purpose: plan.purpose,
101957
+ mastery: {
101958
+ requestFingerprint: masteryRequestFingerprint(plan.mastery),
101959
+ standard: plan.mastery.standard
101960
+ }
101961
+ };
101962
+ }
101963
+ if (plan.purpose === "diagnostic") {
101964
+ return {
101965
+ ...base,
101966
+ purpose: plan.purpose,
101967
+ diagnostic: {
101968
+ definitionId: plan.diagnostic.definitionId,
101969
+ diagnosticKey: plan.diagnostic.diagnosticKey,
101970
+ testName,
101971
+ routingRevision: plan.diagnostic.routingRevision,
101972
+ ledger: [],
101973
+ state: plan.diagnostic.initialState
101974
+ }
101975
+ };
101976
+ }
101977
+ if (plan.purpose === "review") {
101978
+ return {
101979
+ ...base,
101980
+ purpose: plan.purpose,
101981
+ review: {
101982
+ requestFingerprint: reviewRequestFingerprint({
101983
+ standards: plan.review.standards,
101984
+ candidateItemsPerStandard: plan.review.candidateItemsPerStandard
101985
+ }, DEFAULT_REVIEW_SELECTION_POLICY),
101986
+ bankRevision: plan.review.bankRevision,
101987
+ standards: plan.review.standards,
101988
+ candidateItemsPerStandard: plan.review.candidateItemsPerStandard,
101989
+ selections: plan.review.selections,
101990
+ fulfillment: plan.review.fulfillment
101991
+ }
101992
+ };
101993
+ }
101994
+ return { ...base, purpose: plan.purpose };
101995
+ }
101996
+ var init_timeback_assessment_preparation_util = __esm(() => {
101997
+ init_src();
101998
+ init_assessment_runtime2();
101999
+ });
102000
+
100608
102001
  // ../api-core/src/utils/timeback-assessment-publication.util.ts
100609
102002
  function assessmentKeyFromManagedQtiIdentifier(identifier) {
100610
102003
  const assessmentKey = /^playcademy-test\.(.+)\.[a-f0-9]{64}$/.exec(identifier)?.[1];
@@ -100726,7 +102119,7 @@ async function prepareManagedAssessmentPublication(input) {
100726
102119
  sourceItemIdentifier: question.identifier,
100727
102120
  qtiItemIdentifier,
100728
102121
  contentHash,
100729
- xml: canonicalManagedAssetUrls(rewriteQtiItemIdentity(declarationNeutralQtiXml(question.rawXml), qtiItemIdentifier, title)),
102122
+ xml: canonicalManagedAssetUrls(rewriteQtiItemIdentity(declarationNeutralQtiXml(question.rawXml).trim(), qtiItemIdentifier, title)),
100730
102123
  metadata: {
100731
102124
  ...metadata2,
100732
102125
  ownerSystem: PLAYCADEMY_QTI_OWNER_SYSTEM,
@@ -100815,7 +102208,11 @@ async function prepareManagedAssessmentPublication(input) {
100815
102208
  qtiTestIdentifier,
100816
102209
  testInput,
100817
102210
  items,
100818
- diagnosticRoutingManifest: compiledRouting
102211
+ diagnosticRoutingManifest: compiledRouting,
102212
+ artifactSource: {
102213
+ test: testInput,
102214
+ questions: physicalQuestions
102215
+ }
100819
102216
  };
100820
102217
  }
100821
102218
  function assertManagedAssessmentPublication(test, expected) {
@@ -100923,9 +102320,6 @@ function buildAssessmentAssociationUpdates(row, input) {
100923
102320
  updates.standardFramework = null;
100924
102321
  updates.standardIdentifier = null;
100925
102322
  }
100926
- if (row.status === "live" && input.purpose !== row.purpose) {
100927
- updates.sortOrder = null;
100928
- }
100929
102323
  }
100930
102324
  if (input.standard !== undefined) {
100931
102325
  updates.standardFramework = input.standard.framework;
@@ -100933,9 +102327,6 @@ function buildAssessmentAssociationUpdates(row, input) {
100933
102327
  }
100934
102328
  if (input.status !== undefined) {
100935
102329
  updates.status = input.status;
100936
- if (input.status === "archived") {
100937
- updates.sortOrder = null;
100938
- }
100939
102330
  }
100940
102331
  return updates;
100941
102332
  }
@@ -100944,25 +102335,9 @@ function assessmentStandardForRow(row) {
100944
102335
  }
100945
102336
  function validateUniqueAssessmentIdentifiers(testIdentifiers) {
100946
102337
  if (new Set(testIdentifiers).size !== testIdentifiers.length) {
100947
- throw new ValidationError("Assessment order must contain unique identifiers");
102338
+ throw new ValidationError("Assessment manifests must contain unique QTI test identifiers");
100948
102339
  }
100949
102340
  }
100950
- function assertAssessmentOrderUpdateSucceeded(updatedRow) {
100951
- if (!updatedRow) {
100952
- throw new ValidationError("Assessment order changed while it was being saved. Refresh and try again.");
100953
- }
100954
- return updatedRow;
100955
- }
100956
- function lockOrderAssessmentRows(rows) {
100957
- return rows.toSorted((left, right) => left.id.localeCompare(right.id));
100958
- }
100959
- function orderLiveAssessmentRows(liveRows, purpose, testIdentifiers) {
100960
- const rowsByIdentifier = new Map(liveRows.map((row) => [row.qtiTestIdentifier, row]));
100961
- if (liveRows.length !== testIdentifiers.length || testIdentifiers.some((identifier) => !rowsByIdentifier.has(identifier))) {
100962
- throw new ValidationError(`Assessment order must include every live ${purpose} assessment exactly once`);
100963
- }
100964
- return testIdentifiers.map((identifier) => rowsByIdentifier.get(identifier));
100965
- }
100966
102341
  var init_timeback_assessment_rules_util = __esm(() => {
100967
102342
  init_errors2();
100968
102343
  });
@@ -101035,7 +102410,7 @@ var init_timeback_qti_hydration_util = __esm(() => {
101035
102410
  });
101036
102411
 
101037
102412
  // ../api-core/src/services/timeback-assessment-runtime.service.ts
101038
- var TimebackAssessmentRuntimeService;
102413
+ var AssessmentPreparationPrerequisiteError, TimebackAssessmentRuntimeService;
101039
102414
  var init_timeback_assessment_runtime_service = __esm(async () => {
101040
102415
  init_drizzle_orm();
101041
102416
  init_src();
@@ -101047,17 +102422,30 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
101047
102422
  init_types2();
101048
102423
  init_uuid();
101049
102424
  init_errors2();
102425
+ init_assessment_artifact_runtime_util();
101050
102426
  init_assessment_runtime_lock_util();
102427
+ init_timeback_assessment_artifacts_util();
102428
+ init_timeback_assessment_preparation_util();
101051
102429
  init_timeback_assessment_publication_util();
101052
102430
  init_timeback_assessment_rules_util();
101053
102431
  init_timeback_assessment_runtime_util();
101054
102432
  init_timeback_qti_hydration_util();
101055
102433
  await __promiseAll([
101056
102434
  init_dist5(),
101057
- init_errors8()
102435
+ init_errors8(),
102436
+ init_assessment_artifact_loader_util()
101058
102437
  ]);
102438
+ AssessmentPreparationPrerequisiteError = class AssessmentPreparationPrerequisiteError extends Error {
102439
+ original;
102440
+ constructor(original) {
102441
+ super("Assessment preparation prerequisite provisioning failed");
102442
+ this.name = "AssessmentPreparationPrerequisiteError";
102443
+ this.original = original;
102444
+ }
102445
+ };
101059
102446
  TimebackAssessmentRuntimeService = class TimebackAssessmentRuntimeService {
101060
102447
  static ASSESSMENT_CACHE_LIMIT = 32;
102448
+ static PREPARATION_IN_FLIGHT_LIMIT = 32;
101061
102449
  static ASSESSMENT_CACHE_TTL_MS = 60000;
101062
102450
  static EXPORT_CONCURRENCY = 4;
101063
102451
  static ITEM_SUBMISSION_MAX_PREPARATION_ATTEMPTS = 3;
@@ -101077,16 +102465,65 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
101077
102465
  defaultTTL: TimebackAssessmentRuntimeService.ASSESSMENT_CACHE_TTL_MS,
101078
102466
  maxSize: TimebackAssessmentRuntimeService.ASSESSMENT_CACHE_LIMIT
101079
102467
  });
102468
+ artifactLoader;
102469
+ preparations = new AssessmentPreparationFlights(TimebackAssessmentRuntimeService.PREPARATION_IN_FLIGHT_LIMIT);
101080
102470
  constructor(deps) {
101081
102471
  this.deps = deps;
102472
+ this.artifactLoader = new AssessmentArtifactRuntimeLoader(deps.assessmentArtifacts, {
102473
+ maxSize: TimebackAssessmentRuntimeService.ASSESSMENT_CACHE_LIMIT
102474
+ });
101082
102475
  }
101083
- async start({
102476
+ async prepare(params) {
102477
+ await this.deps.validateDeveloperAccess(params.user, params.gameId);
102478
+ const startedAt = Date.now();
102479
+ const requestHash = await assessmentPreparationRequestHash(params.input);
102480
+ const key = assessmentPreparationFlightKey({
102481
+ gameId: params.gameId,
102482
+ studentId: params.studentId,
102483
+ requestHash
102484
+ });
102485
+ const flight = this.preparations.claim(key, () => this.prepareNewAttempt(params, requestHash).catch((error88) => {
102486
+ if (error88 instanceof ValidationError || error88 instanceof AssessmentRuntimeError) {
102487
+ throw error88;
102488
+ }
102489
+ this.recordPreparation("unprepared", params.input.purpose, startedAt, {
102490
+ reason: "prerequisite_unavailable",
102491
+ error: error88
102492
+ });
102493
+ return {
102494
+ status: "unprepared",
102495
+ reason: "prerequisite_unavailable"
102496
+ };
102497
+ }));
102498
+ if (flight.status === "coalesced") {
102499
+ addEvent("assessment.preparation_coalesced", {
102500
+ "app.assessment.purpose": params.input.purpose
102501
+ });
102502
+ }
102503
+ if (flight.status === "at_capacity") {
102504
+ this.recordPreparation("unprepared", params.input.purpose, Date.now(), {
102505
+ reason: "local_capacity"
102506
+ });
102507
+ return { status: "unprepared", reason: "prerequisite_unavailable" };
102508
+ }
102509
+ return flight.promise;
102510
+ }
102511
+ async start(params) {
102512
+ await this.deps.validateDeveloperAccess(params.user, params.gameId);
102513
+ if (params.preparationReceipt) {
102514
+ const prepared = await this.tryPreparedStart(params, params.preparationReceipt);
102515
+ if (prepared) {
102516
+ return prepared;
102517
+ }
102518
+ }
102519
+ return this.startCanonical(params);
102520
+ }
102521
+ async startCanonical({
101084
102522
  gameId,
101085
102523
  studentId,
101086
102524
  input,
101087
102525
  user
101088
102526
  }) {
101089
- await this.deps.validateDeveloperAccess(user, gameId);
101090
102527
  if (input.purpose === "diagnostic") {
101091
102528
  return this.startDiagnostic({ gameId, studentId, input, user });
101092
102529
  }
@@ -101195,6 +102632,285 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
101195
102632
  return this.snapshot(result, metadata2, assessment, context2.integration);
101196
102633
  });
101197
102634
  }
102635
+ async prepareNewAttempt(params, requestHash) {
102636
+ const startedAt = Date.now();
102637
+ const [candidates, results] = await Promise.all([
102638
+ this.candidateIntegrations(params.gameId, params.studentId, params.input),
102639
+ this.studentAssessmentResults(params.studentId)
102640
+ ]);
102641
+ const context2 = await this.selectIntegration(candidates, params.studentId, params.input, results);
102642
+ const attemptScope = {
102643
+ studentId: params.studentId,
102644
+ activityId: params.input.activityId,
102645
+ purpose: params.input.purpose,
102646
+ courseId: context2.integration.courseId,
102647
+ integrationId: context2.integration.id,
102648
+ enrollmentId: context2.enrollment.id
102649
+ };
102650
+ 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);
102651
+ const listing = this.attemptListing(results, attemptScope);
102652
+ const decision = selectRuntimeAssessment(tests, [
102653
+ ...this.attemptsMatchingRequest(listing.attempts, this.startRequestFingerprint(params.input)).values()
102654
+ ].map((entry) => entry.selection));
102655
+ const selection = resolveAssessmentPreparationSelection(decision, candidates.rows.length);
102656
+ if (selection.status === "blocked") {
102657
+ this.recordPreparation(selection.result.status, params.input.purpose, startedAt, {
102658
+ reason: selection.result.status === "canonical_required" ? selection.result.reason : "multiple_candidate_courses"
102659
+ });
102660
+ return selection.result;
102661
+ }
102662
+ if (selection.status === "unavailable") {
102663
+ throw AssessmentRuntimeError.from(assessmentNoEligibleTests(params.input.purpose));
102664
+ }
102665
+ const preparation = {
102666
+ context: context2,
102667
+ tests,
102668
+ listing,
102669
+ decision: selection.decision
102670
+ };
102671
+ let prepared;
102672
+ try {
102673
+ prepared = await this.buildPreparationPlan(params, requestHash, preparation);
102674
+ } catch (error88) {
102675
+ if (error88 instanceof ValidationError || error88 instanceof AssessmentRuntimeError) {
102676
+ throw error88;
102677
+ }
102678
+ const prerequisiteFailure = error88 instanceof AssessmentPreparationPrerequisiteError;
102679
+ const reason = prerequisiteFailure ? "prerequisite_unavailable" : "artifact_unavailable";
102680
+ this.recordPreparation("unprepared", params.input.purpose, startedAt, {
102681
+ reason,
102682
+ error: prerequisiteFailure ? error88.original : error88
102683
+ });
102684
+ return { status: "unprepared", reason };
102685
+ }
102686
+ const minted = await this.deps.mintAssessmentPreparationReceipt(prepared.plan);
102687
+ this.recordPreparation("prepared", params.input.purpose, startedAt, {
102688
+ artifactSource: prepared.artifactSource
102689
+ });
102690
+ return { status: "prepared", ...minted };
102691
+ }
102692
+ async buildPreparationPlan(params, requestHash, preparation) {
102693
+ if (params.input.purpose === "review") {
102694
+ return this.buildReviewPreparationPlan({ ...params, input: params.input }, requestHash, preparation);
102695
+ }
102696
+ const { context: context2, decision, listing } = preparation;
102697
+ let artifact;
102698
+ let diagnostic;
102699
+ if (params.input.purpose === "diagnostic") {
102700
+ const definition = await this.requireDiagnosticDefinition(decision.test.id, context2.integration.id, this.deps.db, true);
102701
+ artifact = await this.loadOrCompileManagedArtifact(definition.qtiTestIdentifier, definition.diagnosticRoutingManifest);
102702
+ diagnostic = {
102703
+ definition,
102704
+ routing: await this.initializeHostedDiagnostic(definition, artifact.assessment)
102705
+ };
102706
+ } else {
102707
+ artifact = await this.loadOrCompileManagedArtifact(decision.test.qtiTestIdentifier);
102708
+ }
102709
+ const plan = await this.provisionPreparationPrerequisites(() => this.finishManagedPreparationPlan({
102710
+ params,
102711
+ requestHash,
102712
+ context: context2,
102713
+ listing,
102714
+ decision,
102715
+ assessment: artifact.assessment,
102716
+ diagnostic
102717
+ }));
102718
+ return { plan, artifactSource: artifact.source };
102719
+ }
102720
+ async provisionPreparationPrerequisites(operation) {
102721
+ try {
102722
+ return await operation();
102723
+ } catch (error88) {
102724
+ throw new AssessmentPreparationPrerequisiteError(error88);
102725
+ }
102726
+ }
102727
+ async finishManagedPreparationPlan(input) {
102728
+ const { params, context: context2, listing, decision, assessment, diagnostic } = input;
102729
+ const diagnosticIdentity = diagnostic && {
102730
+ definitionId: diagnostic.definition.id,
102731
+ diagnosticKey: diagnostic.definition.diagnosticKey,
102732
+ routingRevision: diagnostic.routing.routingRevision
102733
+ };
102734
+ const classId = await this.resolveClassId(context2.integration.courseId, context2.enrollment, params.studentId);
102735
+ const parentLineItemId = await this.ensureLineItem({
102736
+ classId,
102737
+ integration: context2.integration,
102738
+ purpose: params.input.purpose,
102739
+ assessment,
102740
+ ...diagnosticIdentity ? { diagnostic: diagnosticIdentity } : {}
102741
+ });
102742
+ const attempt = await this.claimAttemptId({
102743
+ gameId: params.gameId,
102744
+ studentId: params.studentId,
102745
+ courseId: context2.integration.courseId,
102746
+ activityId: params.input.activityId,
102747
+ purpose: params.input.purpose
102748
+ }, nextAssessmentAttemptNumber(listing.attempts.values()));
102749
+ const common2 = {
102750
+ version: 1,
102751
+ gameId: params.gameId,
102752
+ studentId: params.studentId,
102753
+ activityId: params.input.activityId,
102754
+ requestHash: input.requestHash,
102755
+ preparedAt: Date.now(),
102756
+ integrationId: context2.integration.id,
102757
+ enrollmentId: context2.enrollment.id,
102758
+ courseId: context2.integration.courseId,
102759
+ ...attempt,
102760
+ parentLineItemId,
102761
+ selectedTest: {
102762
+ assessmentKey: decision.test.assessmentKey,
102763
+ identifier: assessment.identifier,
102764
+ contentRevision: assessment.contentRevision
102765
+ }
102766
+ };
102767
+ if (params.input.purpose === "mastery") {
102768
+ return {
102769
+ ...common2,
102770
+ purpose: params.input.purpose,
102771
+ mastery: { standard: normalizeMasteryStandard(params.input.standard) }
102772
+ };
102773
+ }
102774
+ if (params.input.purpose === "diagnostic") {
102775
+ if (!diagnostic || !diagnosticIdentity) {
102776
+ throw new Error("Diagnostic preparation is missing its routing plan");
102777
+ }
102778
+ return {
102779
+ ...common2,
102780
+ purpose: params.input.purpose,
102781
+ diagnostic: { ...diagnosticIdentity, initialState: diagnostic.routing.state }
102782
+ };
102783
+ }
102784
+ if (params.input.purpose !== "end_of_course") {
102785
+ throw new Error(`Unsupported prepared assessment purpose ${params.input.purpose}`);
102786
+ }
102787
+ return { ...common2, purpose: "end_of_course" };
102788
+ }
102789
+ async buildReviewPreparationPlan(params, requestHash, preparation) {
102790
+ const { context: context2, decision, listing, tests } = preparation;
102791
+ if (tests.length !== 1) {
102792
+ throw new ValidationError("Standards review requires exactly one live review-bank assessment.");
102793
+ }
102794
+ const normalized = normalizeReviewRequest(params.input);
102795
+ const catalog = await this.loadReviewBankCatalog(decision.test.qtiTestIdentifier);
102796
+ const selected = selectReviewItems({
102797
+ request: params.input,
102798
+ bank: catalog.bank,
102799
+ exposures: this.reviewExposures(listing.reviewChildren.values(), {
102800
+ bankRevision: catalog.bank.bankRevision
102801
+ })
102802
+ });
102803
+ if (selected.selections.length === 0) {
102804
+ if (selected.fulfillment.status !== "partial") {
102805
+ throw new Error("A complete review selection cannot be empty");
102806
+ }
102807
+ throw AssessmentRuntimeError.from(assessmentReviewBankShortage(selected.fulfillment));
102808
+ }
102809
+ const artifact = await this.loadOrCompileReviewBankArtifact(catalog);
102810
+ const assessment = projectReviewAssessment(artifact.assessment, catalog.bank, selected.selections);
102811
+ const source = { assessment: artifact.assessment, bank: catalog.bank };
102812
+ const parentLineItemId = await this.provisionPreparationPrerequisites(() => this.ensureReviewBankLineItem({
102813
+ integration: context2.integration,
102814
+ activityId: params.input.activityId,
102815
+ source
102816
+ }));
102817
+ await this.provisionPreparationPrerequisites(() => runWithConcurrency(assessment.items, TimebackAssessmentRuntimeService.SCORING_CONCURRENCY, (item) => this.ensureReviewQuestionLineItem({
102818
+ parentLineItemId,
102819
+ integration: context2.integration,
102820
+ bank: catalog.bank,
102821
+ item
102822
+ })));
102823
+ const attempt = await this.provisionPreparationPrerequisites(() => this.claimAttemptId({
102824
+ gameId: params.gameId,
102825
+ studentId: params.studentId,
102826
+ courseId: context2.integration.courseId,
102827
+ activityId: params.input.activityId,
102828
+ purpose: params.input.purpose
102829
+ }, nextAssessmentAttemptNumber(listing.attempts.values())));
102830
+ const plan = {
102831
+ version: 1,
102832
+ gameId: params.gameId,
102833
+ studentId: params.studentId,
102834
+ activityId: params.input.activityId,
102835
+ purpose: params.input.purpose,
102836
+ requestHash,
102837
+ preparedAt: Date.now(),
102838
+ integrationId: context2.integration.id,
102839
+ enrollmentId: context2.enrollment.id,
102840
+ courseId: context2.integration.courseId,
102841
+ ...attempt,
102842
+ parentLineItemId,
102843
+ selectedTest: {
102844
+ assessmentKey: decision.test.assessmentKey,
102845
+ identifier: artifact.assessment.identifier,
102846
+ contentRevision: artifact.assessment.contentRevision
102847
+ },
102848
+ review: {
102849
+ bankRevision: catalog.bank.bankRevision,
102850
+ standards: [...normalized.standards],
102851
+ candidateItemsPerStandard: normalized.candidateItemsPerStandard,
102852
+ selections: [...selected.selections],
102853
+ fulfillment: selected.fulfillment
102854
+ }
102855
+ };
102856
+ return { plan, artifactSource: artifact.source };
102857
+ }
102858
+ async tryPreparedStart(params, receipt) {
102859
+ const startedAt = Date.now();
102860
+ let plan;
102861
+ try {
102862
+ const verified = await this.deps.verifyAssessmentPreparationReceipt(receipt);
102863
+ if (!verified || !await assessmentPreparationReceiptMatches(verified, params)) {
102864
+ this.recordPreparedStart("fallback", params.input.purpose, startedAt, {
102865
+ reason: "receipt_mismatch"
102866
+ });
102867
+ return null;
102868
+ }
102869
+ plan = verified;
102870
+ const assessment = await this.loadPreparedPlanAssessment(verified);
102871
+ if (!assessment) {
102872
+ this.recordPreparedStart("fallback", params.input.purpose, startedAt, {
102873
+ reason: "artifact_unavailable",
102874
+ plan
102875
+ });
102876
+ return null;
102877
+ }
102878
+ const outcome = await this.deps.assessmentRuntimeLock(this.deps.db, [
102879
+ assessmentRuntimeStartLockKey({
102880
+ studentId: params.studentId,
102881
+ activityId: params.input.activityId,
102882
+ purpose: params.input.purpose,
102883
+ courseId: verified.courseId
102884
+ })
102885
+ ], async (db2) => {
102886
+ const candidates = await this.candidateIntegrations(verified.gameId, verified.studentId, params.input, db2);
102887
+ const integration = preparedAssessmentIntegration(verified, candidates);
102888
+ if (!integration || !await this.preparedPlanTestIsLive(verified, db2)) {
102889
+ return null;
102890
+ }
102891
+ if (await this.attemptIdOccupied(verified.attemptId)) {
102892
+ return null;
102893
+ }
102894
+ return this.createPreparedAttempt(verified, assessment, integration);
102895
+ });
102896
+ if (!outcome) {
102897
+ this.recordPreparedStart("fallback", params.input.purpose, startedAt, {
102898
+ reason: "stale_plan",
102899
+ plan
102900
+ });
102901
+ return null;
102902
+ }
102903
+ this.recordPreparedStart("started", params.input.purpose, startedAt, { plan });
102904
+ return outcome;
102905
+ } catch (error88) {
102906
+ this.recordPreparedStart("fallback", params.input.purpose, startedAt, {
102907
+ reason: "prepared_start_failed",
102908
+ error: error88,
102909
+ ...plan ? { plan } : {}
102910
+ });
102911
+ return null;
102912
+ }
102913
+ }
101198
102914
  async startDiagnostic({
101199
102915
  gameId,
101200
102916
  studentId,
@@ -101849,7 +103565,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
101849
103565
  let scoring = null;
101850
103566
  if (loaded.state.status === "in-progress" && expected?.nodeKey === input.routingNodeKey && expected.itemIdentifier === input.itemIdentifier && attempt.metadata.responseVersion === input.expectedResponseVersion) {
101851
103567
  const responses = prepareDiagnosticAssessmentResponses(assessment, attempt.metadata.responses, input);
101852
- scoring = await this.scoreItem(assessment, responses, input.itemIdentifier);
103568
+ scoring = await this.scoreItem(assessment, responses, input.itemIdentifier, assessmentArtifactIdentityFromAttempt(attempt.metadata));
101853
103569
  }
101854
103570
  return {
101855
103571
  responseVersion: attempt.metadata.responseVersion,
@@ -101860,7 +103576,8 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
101860
103576
  scoring
101861
103577
  };
101862
103578
  }
101863
- async scoreItems(assessment, responses, itemIdentifiers) {
103579
+ async scoreItems(assessment, responses, artifactIdentity, itemIdentifiers) {
103580
+ await this.artifactLoader.loadMatchedScoring(assessment, artifactIdentity);
101864
103581
  const client2 = this.requireClient();
101865
103582
  const scoringItems = assessmentScoringRequests(assessment, responses).filter((item) => !itemIdentifiers || itemIdentifiers.has(item.itemIdentifier));
101866
103583
  const scoringTasks = scoringItems.flatMap((item, itemIndex) => item.requests.map((request) => ({
@@ -101890,8 +103607,8 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
101890
103607
  };
101891
103608
  });
101892
103609
  }
101893
- async scoreItem(assessment, responses, itemIdentifier) {
101894
- const [scoring] = await this.scoreItems(assessment, responses, new Set([itemIdentifier]));
103610
+ async scoreItem(assessment, responses, itemIdentifier, artifactIdentity) {
103611
+ const [scoring] = await this.scoreItems(assessment, responses, artifactIdentity, new Set([itemIdentifier]));
101895
103612
  return scoring;
101896
103613
  }
101897
103614
  async prepareItemSubmission(params, input) {
@@ -101919,7 +103636,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
101919
103636
  itemSubmissions: attempt.metadata.itemSubmissions,
101920
103637
  assessment
101921
103638
  }, input);
101922
- const scoring = transition.action === "commit" ? await this.scoreItem(assessment, transition.responses, input.itemIdentifier) : null;
103639
+ const scoring = transition.action === "commit" ? await this.scoreItem(assessment, transition.responses, input.itemIdentifier, assessmentArtifactIdentityFromAttempt(attempt.metadata)) : null;
101923
103640
  return {
101924
103641
  responseVersion: attempt.metadata.responseVersion,
101925
103642
  submissionId: input.submissionId,
@@ -101932,7 +103649,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
101932
103649
  const assessment = await this.loadAttemptAssessment(attempt.metadata);
101933
103650
  validateAssessmentResponses(assessment, attempt.metadata.responses);
101934
103651
  const administeredItems = attempt.metadata.purpose === "review" ? administeredItemIdentifiers(attempt.metadata.itemSubmissions) : undefined;
101935
- const itemResults = await this.scoreItems(assessment, attempt.metadata.responses, administeredItems);
103652
+ const itemResults = await this.scoreItems(assessment, attempt.metadata.responses, assessmentArtifactIdentityFromAttempt(attempt.metadata), administeredItems);
101936
103653
  return {
101937
103654
  responseVersion: attempt.metadata.responseVersion,
101938
103655
  assessment,
@@ -102424,23 +104141,24 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
102424
104141
  liveTestIntegrationIds: integrationIdsWithLiveTests
102425
104142
  };
102426
104143
  }
102427
- async selectIntegration(candidates, studentId, input) {
104144
+ async selectIntegration(candidates, studentId, input, results) {
102428
104145
  const {
102429
104146
  rows: candidateRows,
102430
104147
  enrollmentByCourse,
102431
104148
  liveTestIntegrationIds: integrationIdsWithLiveTests
102432
104149
  } = candidates;
102433
104150
  const needsOpenAnnotation = candidateRows.length > 1 || candidateRows.some((row) => !integrationIdsWithLiveTests.has(row.id));
102434
- const openAttemptCourseIds = needsOpenAnnotation ? await this.openAttemptCourseIds({
104151
+ const openAttemptScope = {
102435
104152
  studentId,
102436
104153
  activityId: input.activityId,
102437
104154
  purpose: input.purpose,
102438
- enrollmentIdByCourse: new Map(candidateRows.map((row) => [
102439
- row.courseId,
102440
- enrollmentByCourse.get(row.courseId).id
102441
- ])),
104155
+ enrollmentIdByCourse: new Map(candidateRows.map((row) => [row.courseId, enrollmentByCourse.get(row.courseId).id])),
102442
104156
  requestFingerprint: this.startRequestFingerprint(input)
102443
- }) : new Set;
104157
+ };
104158
+ let openAttemptCourseIds = new Set;
104159
+ if (needsOpenAnnotation) {
104160
+ openAttemptCourseIds = results ? this.openAttemptCourseIdsFromResults(results, openAttemptScope) : await this.openAttemptCourseIds(openAttemptScope);
104161
+ }
102444
104162
  const annotated = candidateRows.map((row) => ({
102445
104163
  ...row,
102446
104164
  hasLiveTests: integrationIdsWithLiveTests.has(row.id),
@@ -102470,20 +104188,28 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
102470
104188
  id: row.id,
102471
104189
  assessmentKey: row.assessmentKey,
102472
104190
  qtiTestIdentifier: row.qtiTestIdentifier,
102473
- sortOrder: row.sortOrder,
102474
- updatedAt: row.updatedAt.toISOString(),
102475
104191
  standard: assessmentStandardForRow(row)
102476
104192
  })).filter((test) => !matchesMasteryStandard || matchesMasteryStandard(test.standard));
102477
104193
  }
102478
104194
  async openAttemptCourseIds(scope) {
102479
- const results = this.requireClient().api.oneroster.assessmentResults.stream({
104195
+ return this.openAttemptCourseIdsFromResults(await this.studentAssessmentResults(scope.studentId), scope);
104196
+ }
104197
+ async studentAssessmentResults(studentId) {
104198
+ const stream = this.requireClient().api.oneroster.assessmentResults.stream({
102480
104199
  where: {
102481
- "student.sourcedId": scope.studentId,
104200
+ "student.sourcedId": studentId,
102482
104201
  status: ONEROSTER_STATUS2.active
102483
104202
  }
102484
104203
  });
104204
+ const results = [];
104205
+ for await (const result of stream) {
104206
+ results.push(result);
104207
+ }
104208
+ return results;
104209
+ }
104210
+ openAttemptCourseIdsFromResults(results, scope) {
102485
104211
  const courseIds = new Set;
102486
- for await (const result of results) {
104212
+ for (const result of results) {
102487
104213
  const metadata2 = playcademyAssessmentResultMetadata(result.metadata);
102488
104214
  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))) {
102489
104215
  courseIds.add(metadata2.courseId);
@@ -102492,15 +104218,12 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
102492
104218
  return courseIds;
102493
104219
  }
102494
104220
  async listAttempts(scope) {
102495
- const results = this.requireClient().api.oneroster.assessmentResults.stream({
102496
- where: {
102497
- "student.sourcedId": scope.studentId,
102498
- status: ONEROSTER_STATUS2.active
102499
- }
102500
- });
104221
+ return this.attemptListing(await this.studentAssessmentResults(scope.studentId), scope);
104222
+ }
104223
+ attemptListing(results, scope) {
102501
104224
  const attempts = new Map;
102502
104225
  const reviewChildren = new Map;
102503
- for await (const result of results) {
104226
+ for (const result of results) {
102504
104227
  const metadata2 = playcademyAssessmentResultMetadata(result.metadata);
102505
104228
  const childMetadata = scope.purpose === "review" ? playcademyAssessmentItemResultMetadata(result.metadata) : null;
102506
104229
  if (childMetadata && childMetadata.activityId === scope.activityId && childMetadata.integrationId === scope.integrationId && childMetadata.enrollmentId === scope.enrollmentId && result.student.sourcedId === scope.studentId) {
@@ -102712,6 +104435,57 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
102712
104435
  ...counts.selectedCount === undefined ? {} : { "app.assessment.selected_count": counts.selectedCount }
102713
104436
  });
102714
104437
  }
104438
+ async loadOrCompileManagedArtifact(identifier, diagnosticManifest) {
104439
+ const playable = await this.artifactLoader.loadManagedPlayable(identifier);
104440
+ if (playable) {
104441
+ const identity = {
104442
+ kind: "assessment",
104443
+ qtiTestIdentifier: identifier,
104444
+ contentRevision: playable.contentRevision
104445
+ };
104446
+ const scoring = await this.artifactLoader.loadMatchedScoring(playable, identity);
104447
+ const expectedRoutingRevision = diagnosticManifest ? await diagnosticRoutingRevision(diagnosticManifest) : undefined;
104448
+ if (scoring && (expectedRoutingRevision === undefined || scoring.diagnostic?.routingRevision === expectedRoutingRevision)) {
104449
+ return { assessment: playable, source: "existing" };
104450
+ }
104451
+ }
104452
+ const source = await loadHydratedQtiTest(this.requireClient(), identifier);
104453
+ const compiled = await compileManagedAssessmentArtifacts({
104454
+ source,
104455
+ diagnosticRoutingManifest: diagnosticManifest
104456
+ });
104457
+ return this.storeCompiledArtifact(compiled, `assessment artifacts for ${identifier}`);
104458
+ }
104459
+ async loadOrCompileReviewBankArtifact(catalog) {
104460
+ const identity = {
104461
+ kind: "review-bank",
104462
+ qtiTestIdentifier: catalog.bank.bankIdentifier,
104463
+ sourceContentRevision: catalog.bank.sourceContentRevision,
104464
+ bankRevision: catalog.bank.bankRevision
104465
+ };
104466
+ const playable = await this.artifactLoader.loadPlayable(identity);
104467
+ const scoring = playable ? await this.artifactLoader.loadMatchedScoring(playable, identity) : null;
104468
+ if (playable && scoring) {
104469
+ return { assessment: playable, source: "existing" };
104470
+ }
104471
+ const itemIdentifiers = catalog.bank.items.map((item) => item.itemIdentifier);
104472
+ const questions = await hydrateQtiTestQuestionSelection(this.requireClient(), catalog.references, itemIdentifiers);
104473
+ const compiled = await compileReviewBankArtifacts({
104474
+ source: { test: catalog.test, questions },
104475
+ bank: catalog.bank
104476
+ });
104477
+ return this.storeCompiledArtifact(compiled, `review-bank artifacts for ${catalog.bank.bankIdentifier}`);
104478
+ }
104479
+ async storeCompiledArtifact(compiled, label) {
104480
+ const assessment = await storeAndVerifyCompiledAssessmentArtifacts({
104481
+ compiled,
104482
+ label,
104483
+ store: (pair) => this.deps.assessmentArtifacts.storePair(pair),
104484
+ loadPlayable: (identity) => this.artifactLoader.loadPlayable(identity),
104485
+ loadMatchedScoring: (playable, identity) => this.artifactLoader.loadMatchedScoring(playable, identity)
104486
+ });
104487
+ return { assessment, source: "compiled" };
104488
+ }
102715
104489
  async loadReviewBankCatalog(identifier, expectedSourceRevision) {
102716
104490
  if (expectedSourceRevision) {
102717
104491
  const cached3 = this.reviewBankCache.get(`${identifier}\x00${expectedSourceRevision}`);
@@ -102764,6 +104538,16 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
102764
104538
  return assessment;
102765
104539
  }
102766
104540
  async hydrateReviewBankSelection(catalog, itemIdentifiers) {
104541
+ const identity = {
104542
+ kind: "review-bank",
104543
+ qtiTestIdentifier: catalog.bank.bankIdentifier,
104544
+ sourceContentRevision: catalog.bank.sourceContentRevision,
104545
+ bankRevision: catalog.bank.bankRevision
104546
+ };
104547
+ const artifact = await this.artifactLoader.loadPlayableContainingItems(identity, itemIdentifiers);
104548
+ if (artifact) {
104549
+ return { assessment: artifact, bank: catalog.bank };
104550
+ }
102767
104551
  const assessment = await this.cachedReviewSelection({
102768
104552
  identifier: catalog.test.identifier,
102769
104553
  contentRevision: catalog.bank.sourceContentRevision,
@@ -102777,6 +104561,14 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
102777
104561
  }
102778
104562
  async loadPinnedReviewBankSource(metadata2) {
102779
104563
  const itemIdentifiers = metadata2.review.selections.map((selection) => selection.itemIdentifier);
104564
+ const identity = assessmentArtifactIdentityFromAttempt(metadata2);
104565
+ const artifact = await this.artifactLoader.loadPlayableContainingItems(identity, itemIdentifiers);
104566
+ if (artifact) {
104567
+ return {
104568
+ assessment: artifact,
104569
+ bank: pinnedReviewBankFromAttempt(metadata2, artifact.identifier)
104570
+ };
104571
+ }
102780
104572
  let assessment;
102781
104573
  try {
102782
104574
  assessment = await this.cachedReviewSelection({
@@ -102802,18 +104594,19 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
102802
104594
  }
102803
104595
  return {
102804
104596
  assessment,
102805
- bank: {
102806
- bankIdentifier: assessment.identifier,
102807
- sourceContentRevision: metadata2.selectedTest.contentRevision,
102808
- bankRevision: metadata2.review.bankRevision,
102809
- items: metadata2.review.selections.map((selection) => ({
102810
- itemIdentifier: selection.itemIdentifier,
102811
- standards: [selection.standard]
102812
- }))
102813
- }
104597
+ bank: pinnedReviewBankFromAttempt(metadata2, assessment.identifier)
102814
104598
  };
102815
104599
  }
102816
104600
  async loadAssessment(identifier, expectedRevision) {
104601
+ const artifact = expectedRevision ? await this.artifactLoader.loadPlayable({
104602
+ kind: "assessment",
104603
+ qtiTestIdentifier: identifier,
104604
+ contentRevision: expectedRevision
104605
+ }) : await this.artifactLoader.loadManagedPlayable(identifier);
104606
+ if (artifact) {
104607
+ return artifact;
104608
+ }
104609
+ this.artifactLoader.recordFallback("playable", "assessment");
102817
104610
  if (expectedRevision) {
102818
104611
  const cached3 = this.assessmentCache.get(`${identifier}\x00${expectedRevision}`);
102819
104612
  if (cached3) {
@@ -103581,6 +105374,72 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
103581
105374
  scoreFromResult(result, possible, metadata2) {
103582
105375
  return metadata2.score ?? assessmentScoreFromPercentage(result.score, possible);
103583
105376
  }
105377
+ recordPreparation(outcome, purpose, startedAt, details = {}) {
105378
+ addEvent("assessment.preparation", {
105379
+ "app.assessment.purpose": purpose,
105380
+ "app.assessment.preparation_outcome": outcome,
105381
+ "app.assessment.duration_ms": Date.now() - startedAt,
105382
+ ...details.reason ? { "app.assessment.reason": details.reason } : {},
105383
+ ...details.artifactSource ? { "app.assessment.artifact_source": details.artifactSource } : {},
105384
+ ...details.error ? {
105385
+ "exception.type": errorType(details.error),
105386
+ "app.error.message": errorMessage(details.error)
105387
+ } : {}
105388
+ });
105389
+ }
105390
+ recordPreparedStart(outcome, purpose, startedAt, details = {}) {
105391
+ addEvent("assessment.prepared_start", {
105392
+ "app.assessment.purpose": purpose,
105393
+ "app.assessment.prepared_start_outcome": outcome,
105394
+ "app.assessment.duration_ms": Date.now() - startedAt,
105395
+ ...details.reason ? { "app.assessment.reason": details.reason } : {},
105396
+ ...details.plan ? {
105397
+ "app.assessment.preparation_age_ms": Math.max(0, Date.now() - details.plan.preparedAt)
105398
+ } : {},
105399
+ ...details.error ? {
105400
+ "exception.type": errorType(details.error),
105401
+ "app.error.message": errorMessage(details.error)
105402
+ } : {}
105403
+ });
105404
+ }
105405
+ async preparedPlanTestIsLive(plan, db2) {
105406
+ const tests = await this.liveTests(plan.integrationId, plan.purpose, db2, plan.purpose === "mastery" ? plan.mastery.standard : undefined, plan.purpose === "diagnostic" ? plan.diagnostic.diagnosticKey : undefined);
105407
+ return preparedAssessmentTestIsLive(plan, tests);
105408
+ }
105409
+ async loadPreparedPlanAssessment(plan) {
105410
+ const identity = preparedAssessmentArtifactIdentity(plan);
105411
+ const playable = plan.purpose === "review" ? await this.artifactLoader.loadPlayableContainingItems(identity, plan.review.selections.map((selection) => selection.itemIdentifier)) : await this.artifactLoader.loadPlayable(identity);
105412
+ if (!playable) {
105413
+ return null;
105414
+ }
105415
+ const scoring = await this.artifactLoader.loadMatchedScoring(playable, identity);
105416
+ if (!preparedAssessmentScoringIsCompatible(plan, scoring)) {
105417
+ return null;
105418
+ }
105419
+ if (plan.purpose !== "review") {
105420
+ return playable;
105421
+ }
105422
+ return projectReviewAssessment(playable, preparedReviewBankIndex(plan), plan.review.selections);
105423
+ }
105424
+ async createPreparedAttempt(plan, assessment, integration) {
105425
+ const timestamp6 = new Date().toISOString();
105426
+ const metadata2 = buildPreparedAssessmentMetadata(plan, timestamp6, assessment.title);
105427
+ const result = await this.requireClient().api.oneroster.assessmentResults.upsert(plan.attemptId, {
105428
+ status: ONEROSTER_STATUS2.active,
105429
+ assessmentLineItem: { sourcedId: plan.parentLineItemId },
105430
+ student: { sourcedId: plan.studentId },
105431
+ score: 0,
105432
+ scoreDate: timestamp6,
105433
+ scoreStatus: ASSESSMENT_ATTEMPT_OPEN.scoreStatus,
105434
+ inProgress: ASSESSMENT_ATTEMPT_OPEN.inProgress,
105435
+ metadata: buildAssessmentResultMetadata({
105436
+ metadata: metadata2,
105437
+ testName: assessment.title,
105438
+ grade: integration.grade
105439
+ })
105440
+ });
105441
+ return this.isPlatformRoutedDiagnosticMetadata(metadata2) ? this.diagnosticSnapshot(result, metadata2, assessment, integration, metadata2.diagnostic.state) : this.snapshot(result, metadata2, assessment, integration);
105442
+ }
103584
105443
  async claimAttemptId(scope, from) {
103585
105444
  for (let attemptNumber = from;attemptNumber < from + 20; attemptNumber += 1) {
103586
105445
  const attemptId = await assessmentAttemptId({ ...scope, attemptNumber });
@@ -103736,6 +105595,35 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
103736
105595
  };
103737
105596
  });
103738
105597
 
105598
+ // ../api-core/src/utils/assessment-artifact-fallback.util.ts
105599
+ function assessmentArtifactFailureFallback(error88) {
105600
+ if (error88 instanceof ValidationError || error88 instanceof ConflictError || error88 instanceof AssessmentRuntimeError) {
105601
+ throw error88;
105602
+ }
105603
+ return false;
105604
+ }
105605
+ function assessmentArtifactPublicationWarning(artifactsAvailable) {
105606
+ return artifactsAvailable ? undefined : ASSESSMENT_ARTIFACTS_UNAVAILABLE_WARNING;
105607
+ }
105608
+ function assessmentArtifactImportMessage(message, artifactsAvailable) {
105609
+ return artifactsAvailable ? message : `${message} Optimized ${ARTIFACTS_UNAVAILABLE_DETAIL}`;
105610
+ }
105611
+ function assessmentArtifactReviewBankWarnings(warnings, artifactsAvailable) {
105612
+ return artifactsAvailable ? warnings : [
105613
+ ...warnings,
105614
+ {
105615
+ code: "ASSESSMENT_ARTIFACTS_UNAVAILABLE",
105616
+ message: REVIEW_BANK_ARTIFACTS_UNAVAILABLE_WARNING
105617
+ }
105618
+ ];
105619
+ }
105620
+ var ARTIFACTS_UNAVAILABLE_DETAIL = "assessment artifacts are unavailable. Students will use the slower QTI fallback until compilation is repaired.", ASSESSMENT_ARTIFACTS_UNAVAILABLE_WARNING, REVIEW_BANK_ARTIFACTS_UNAVAILABLE_WARNING;
105621
+ var init_assessment_artifact_fallback_util = __esm(() => {
105622
+ init_errors2();
105623
+ ASSESSMENT_ARTIFACTS_UNAVAILABLE_WARNING = `Published successfully, but optimized ${ARTIFACTS_UNAVAILABLE_DETAIL}`;
105624
+ REVIEW_BANK_ARTIFACTS_UNAVAILABLE_WARNING = `Review bank synchronized successfully, but optimized ${ARTIFACTS_UNAVAILABLE_DETAIL}`;
105625
+ });
105626
+
103739
105627
  // ../api-core/src/utils/timeback-assessment-diagnostic.util.ts
103740
105628
  function diagnosticRoutingCapabilities(assessment, scoring) {
103741
105629
  return assessment.items.map((item) => ({
@@ -104076,7 +105964,7 @@ class TimebackAssessmentsService {
104076
105964
  assertManagedAssessmentIdentity(loaded.test, entry.assessmentKey);
104077
105965
  assertAssessmentHasQuestions(loaded.questions.questions);
104078
105966
  assertPlayableAssessmentImportQuestions(targetStatus, loaded.questions.questions.map(({ question }) => question));
104079
- return { entry, test: loaded.test };
105967
+ return { entry, source: loaded };
104080
105968
  } catch (error88) {
104081
105969
  return { entry, error: error88 };
104082
105970
  }
@@ -104089,7 +105977,6 @@ class TimebackAssessmentsService {
104089
105977
  ...test ? { title: test.title } : {},
104090
105978
  purpose: entry.purpose,
104091
105979
  ...entry.standard ? { standard: entry.standard } : {},
104092
- sourceSortOrder: entry.sortOrder,
104093
105980
  ...test && gameSlug ? { editable: isQtiTestOwnedByGame(test, gameSlug) } : {}
104094
105981
  };
104095
105982
  }
@@ -104097,7 +105984,7 @@ class TimebackAssessmentsService {
104097
105984
  const pendingInserts = [];
104098
105985
  for (const validation of input.validated) {
104099
105986
  const { entry } = validation;
104100
- const base = this.associationImportResultBase(entry, "test" in validation ? validation.test : undefined, input.gameSlug);
105987
+ const base = this.associationImportResultBase(entry, "source" in validation ? validation.source.test : undefined, input.gameSlug);
104101
105988
  if ("error" in validation) {
104102
105989
  input.results[entry.index] = {
104103
105990
  ...base,
@@ -104108,7 +105995,7 @@ class TimebackAssessmentsService {
104108
105995
  const existingForKey = input.existingByKey.get(entry.assessmentKey);
104109
105996
  const decision = planAssessmentAssociationImport(entry, existingForKey, input.existingByIdentifier.get(entry.qtiTestIdentifier));
104110
105997
  if (decision.kind === "insert") {
104111
- pendingInserts.push({ entry, test: validation.test });
105998
+ pendingInserts.push({ entry, source: validation.source });
104112
105999
  } else {
104113
106000
  input.results[entry.index] = {
104114
106001
  ...base,
@@ -104140,16 +106027,19 @@ class TimebackAssessmentsService {
104140
106027
  }
104141
106028
  }
104142
106029
  async insertImportedAssessments(input) {
104143
- for (const { entry, test } of input.pending) {
104144
- const base = this.associationImportResultBase(entry, test, input.gameSlug);
106030
+ for (const { entry, source } of input.pending) {
106031
+ const base = this.associationImportResultBase(entry, source.test, input.gameSlug);
104145
106032
  try {
106033
+ let artifactsAvailable = true;
106034
+ if (input.targetStatus === "live") {
106035
+ artifactsAvailable = await this.storeManagedArtifacts(source, managedAssessmentDiagnosticRoutingManifest(source.test, entry.assessmentKey));
106036
+ }
104146
106037
  const [row] = await input.db.insert(gameTimebackAssessmentTests).values({
104147
106038
  integrationId: input.integrationId,
104148
106039
  assessmentKey: entry.assessmentKey,
104149
106040
  qtiTestIdentifier: entry.qtiTestIdentifier,
104150
106041
  purpose: entry.purpose,
104151
106042
  status: input.targetStatus,
104152
- sortOrder: entry.sortOrder,
104153
106043
  standardFramework: entry.standard?.framework,
104154
106044
  standardIdentifier: entry.standard?.identifier
104155
106045
  }).onConflictDoNothing().returning();
@@ -104166,11 +106056,12 @@ class TimebackAssessmentsService {
104166
106056
  throw new Error("Assessment association insert conflicted");
104167
106057
  }
104168
106058
  } else {
106059
+ const message = attachedAssessmentImportMessage(input.targetStatus, base.editable ?? false);
104169
106060
  input.results[entry.index] = {
104170
106061
  ...base,
104171
106062
  status: "created",
104172
106063
  association: this.associationImportSummary(row),
104173
- message: attachedAssessmentImportMessage(input.targetStatus, base.editable ?? false)
106064
+ message: assessmentArtifactImportMessage(message, artifactsAvailable)
104174
106065
  };
104175
106066
  }
104176
106067
  } catch (error88) {
@@ -104236,6 +106127,7 @@ class TimebackAssessmentsService {
104236
106127
  assertQtiTestOwnedByGame(test, ownership.gameSlug);
104237
106128
  await client2.qtiApi.assessmentTests.update(qtiTestIdentifier, buildQtiTestUpdateInput(test, input.title));
104238
106129
  }
106130
+ let publicationWarning;
104239
106131
  if (publishing) {
104240
106132
  if (nextPurpose === "diagnostic" && !nextDiagnostic) {
104241
106133
  throw new ValidationError("Platform-routed diagnostics require a diagnostic key and routing manifest before publication.");
@@ -104251,6 +106143,7 @@ class TimebackAssessmentsService {
104251
106143
  if (publishing) {
104252
106144
  const publication = await this.publishManagedAssessment(row, nextPurpose, nextDiagnostic?.routingManifest ?? null, loaded);
104253
106145
  updates.qtiTestIdentifier = publication.qtiTestIdentifier;
106146
+ publicationWarning = publication.warning;
104254
106147
  if (nextPurpose === "diagnostic") {
104255
106148
  updates.diagnosticRoutingManifest = publication.diagnosticRoutingManifest;
104256
106149
  }
@@ -104262,7 +106155,7 @@ class TimebackAssessmentsService {
104262
106155
  updated = updatedRow ?? row;
104263
106156
  }
104264
106157
  setAttribute("app.assessment.operation", input.title !== undefined ? "update_test" : "update_association");
104265
- return this.associationSummary(updated);
106158
+ return this.associationSummary(updated, publicationWarning);
104266
106159
  });
104267
106160
  } catch (error88) {
104268
106161
  if (isUniqueViolation(error88)) {
@@ -104282,7 +106175,7 @@ class TimebackAssessmentsService {
104282
106175
  if (plan.kind === "delete") {
104283
106176
  await tx.delete(gameTimebackAssessmentTests).where(eq(gameTimebackAssessmentTests.id, row.id));
104284
106177
  } else if (plan.kind === "archive") {
104285
- await tx.update(gameTimebackAssessmentTests).set({ status: "archived", sortOrder: null, updatedAt: new Date }).where(eq(gameTimebackAssessmentTests.id, row.id));
106178
+ await tx.update(gameTimebackAssessmentTests).set({ status: "archived", updatedAt: new Date }).where(eq(gameTimebackAssessmentTests.id, row.id));
104286
106179
  }
104287
106180
  if (plan.kind !== "none") {
104288
106181
  setAttribute("app.assessment.operation", plan.operation);
@@ -104290,27 +106183,6 @@ class TimebackAssessmentsService {
104290
106183
  return { action: plan.action };
104291
106184
  });
104292
106185
  }
104293
- async reorderAssessments(integrationId, purpose, testIdentifiers) {
104294
- if (purpose === "review") {
104295
- throw new ValidationError("The review bank is system-managed and cannot be reordered.");
104296
- }
104297
- await this.requireIntegration(integrationId);
104298
- validateUniqueAssessmentIdentifiers(testIdentifiers);
104299
- const updatedAt = new Date;
104300
- await this.deps.db.transaction(async (tx) => {
104301
- const liveRows = await tx.query.gameTimebackAssessmentTests.findMany({
104302
- where: and(eq(gameTimebackAssessmentTests.integrationId, integrationId), eq(gameTimebackAssessmentTests.purpose, purpose), eq(gameTimebackAssessmentTests.status, "live"))
104303
- });
104304
- const orderedRows = orderLiveAssessmentRows(liveRows, purpose, testIdentifiers);
104305
- const positionByRowId = new Map(orderedRows.map((row, index2) => [row.id, index2 + 1]));
104306
- for (const row of lockOrderAssessmentRows(orderedRows)) {
104307
- const testIdentifier = row.qtiTestIdentifier;
104308
- const [updatedRow] = await tx.update(gameTimebackAssessmentTests).set({ sortOrder: positionByRowId.get(row.id), updatedAt }).where(and(eq(gameTimebackAssessmentTests.id, row.id), eq(gameTimebackAssessmentTests.integrationId, integrationId), eq(gameTimebackAssessmentTests.qtiTestIdentifier, testIdentifier), eq(gameTimebackAssessmentTests.purpose, purpose), eq(gameTimebackAssessmentTests.status, "live"))).returning({ id: gameTimebackAssessmentTests.id });
104309
- assertAssessmentOrderUpdateSucceeded(updatedRow);
104310
- }
104311
- });
104312
- setAttribute("app.assessment.operation", "reorder_live_assessments");
104313
- }
104314
106186
  async listQuestions(integrationId, qtiTestIdentifier) {
104315
106187
  const client2 = this.requireClient();
104316
106188
  await this.requireAssessmentRow(integrationId, qtiTestIdentifier);
@@ -104366,7 +106238,7 @@ class TimebackAssessmentsService {
104366
106238
  assertReviewBankSinkOwnership(existingSink, integrationId);
104367
106239
  }
104368
106240
  let unchangedResult = null;
104369
- if (existingSink) {
106241
+ if (existingSink?.title === PLAYCADEMY_REVIEW_BANK_TITLE) {
104370
106242
  try {
104371
106243
  unchangedResult = await unchangedReviewBankSynchronizationResult({
104372
106244
  sink: existingSink,
@@ -104400,8 +106272,17 @@ class TimebackAssessmentsService {
104400
106272
  const diff = reviewBankSynchronizationDiff({ existingSink, plan });
104401
106273
  addedItemCount = diff.addedItemCount;
104402
106274
  removedItemCount = diff.removedItemCount;
104403
- catalogUnchanged = diff.unchanged;
106275
+ catalogUnchanged = false;
104404
106276
  }
106277
+ const artifactTest = catalogUnchanged ? existingSink : buildReviewBankSinkInput({
106278
+ identifier: plan.bankIdentifier,
106279
+ title: PLAYCADEMY_REVIEW_BANK_TITLE,
106280
+ metadata: reviewBankSinkMetadata(integrationId, plan),
106281
+ itemIdentifiers: plan.itemIdentifiers,
106282
+ itemHref: (identifier) => qtiItemHref(client2, identifier),
106283
+ includeIdentifier: true
106284
+ });
106285
+ const artifactsAvailable = await this.storeReviewBankGeneration(client2, artifactTest, catalogUnchanged ? reviewBankTestItemIdentifiers(existingSink) : plan.itemIdentifiers);
104405
106286
  const committed = await this.withLockedIntegrationAssessmentRows(integrationId, async (tx, lockedAssociations) => {
104406
106287
  const lockedSources = this.reviewBankMasterySources(lockedAssociations);
104407
106288
  const lockedFingerprint = await reviewBankContributorFingerprint(lockedSources);
@@ -104434,7 +106315,10 @@ class TimebackAssessmentsService {
104434
106315
  unchanged,
104435
106316
  warnings: plan.warnings
104436
106317
  };
104437
- return this.reviewMappingResult(result);
106318
+ return this.reviewMappingResult({
106319
+ ...result,
106320
+ warnings: assessmentArtifactReviewBankWarnings(result.warnings, artifactsAvailable)
106321
+ });
104438
106322
  });
104439
106323
  if (committed) {
104440
106324
  return committed;
@@ -104736,6 +106620,36 @@ class TimebackAssessmentsService {
104736
106620
  };
104737
106621
  });
104738
106622
  }
106623
+ async storeReviewBankGeneration(client2, test, itemIdentifiers) {
106624
+ const startedAt = Date.now();
106625
+ const bank = await reviewBankIndexFromManifest(test.metadata, {
106626
+ bankIdentifier: test.identifier,
106627
+ membershipItemIdentifiers: itemIdentifiers
106628
+ });
106629
+ if (!bank) {
106630
+ throw new ValidationError(`Review bank ${test.identifier} has no revisioned mapping to compile`);
106631
+ }
106632
+ const identity = {
106633
+ kind: "review-bank",
106634
+ qtiTestIdentifier: bank.bankIdentifier,
106635
+ sourceContentRevision: bank.sourceContentRevision,
106636
+ bankRevision: bank.bankRevision
106637
+ };
106638
+ try {
106639
+ if (await this.deps.assessmentArtifacts.loadPair(identity)) {
106640
+ return true;
106641
+ }
106642
+ } catch (error88) {
106643
+ return this.handleArtifactFailure(test.identifier, "review-bank", startedAt, error88);
106644
+ }
106645
+ return this.compileAndStoreArtifacts(test.identifier, "review-bank", async () => compileReviewBankArtifacts({
106646
+ source: {
106647
+ test,
106648
+ questions: await hydrateQtiItemSelection(client2, test, itemIdentifiers)
106649
+ },
106650
+ bank
106651
+ }));
106652
+ }
104739
106653
  async writeReviewBankSink(input) {
104740
106654
  function sinkInput(title, includeIdentifier = false) {
104741
106655
  return buildReviewBankSinkInput({
@@ -104748,11 +106662,11 @@ class TimebackAssessmentsService {
104748
106662
  });
104749
106663
  }
104750
106664
  if (input.existingSink) {
104751
- await input.client.qtiApi.assessmentTests.update(input.plan.bankIdentifier, sinkInput(input.existingSink.title));
106665
+ await input.client.qtiApi.assessmentTests.update(input.plan.bankIdentifier, sinkInput(PLAYCADEMY_REVIEW_BANK_TITLE));
104752
106666
  return;
104753
106667
  }
104754
106668
  try {
104755
- const createInput = sinkInput(REVIEW_BANK_TITLE, true);
106669
+ const createInput = sinkInput(PLAYCADEMY_REVIEW_BANK_TITLE, true);
104756
106670
  await input.client.qtiApi.assessmentTests.create(createInput);
104757
106671
  } catch (error88) {
104758
106672
  if (!isApiError(error88) || error88.statusCode !== 409) {
@@ -104760,7 +106674,7 @@ class TimebackAssessmentsService {
104760
106674
  }
104761
106675
  const concurrentlyCreated = await input.client.qtiApi.assessmentTests.get(input.plan.bankIdentifier);
104762
106676
  assertReviewBankSinkOwnership(concurrentlyCreated, input.integrationId);
104763
- await input.client.qtiApi.assessmentTests.update(input.plan.bankIdentifier, sinkInput(concurrentlyCreated.title));
106677
+ await input.client.qtiApi.assessmentTests.update(input.plan.bankIdentifier, sinkInput(PLAYCADEMY_REVIEW_BANK_TITLE));
104764
106678
  }
104765
106679
  }
104766
106680
  async installReviewBankAssociation(input) {
@@ -104773,15 +106687,14 @@ class TimebackAssessmentsService {
104773
106687
  if (reservedKeyOwner && reservedKeyOwner.purpose !== "review") {
104774
106688
  throw new ValidationError("The reserved review-bank assessment key is already in use.");
104775
106689
  }
104776
- const singletonCurrent = Boolean(singleton && singleton.assessmentKey === input.sinkIdentifier && singleton.status === "live" && singleton.sortOrder === null && singleton.standardFramework === null && singleton.standardIdentifier === null && singleton.diagnosticKey === null && singleton.diagnosticRoutingManifest === null);
104777
- const legacyCurrent = legacyReviews.every((association) => association.status === "archived" && association.sortOrder === null && association.assessmentKey !== input.sinkIdentifier);
106690
+ const singletonCurrent = Boolean(singleton && singleton.assessmentKey === input.sinkIdentifier && singleton.status === "live" && singleton.standardFramework === null && singleton.standardIdentifier === null && singleton.diagnosticKey === null && singleton.diagnosticRoutingManifest === null);
106691
+ const legacyCurrent = legacyReviews.every((association) => association.status === "archived" && association.assessmentKey !== input.sinkIdentifier);
104778
106692
  const changed = !singletonCurrent || !legacyCurrent;
104779
106693
  await input.beforeInstall();
104780
106694
  for (const legacy of legacyReviews) {
104781
- if (legacy.status !== "archived" || legacy.sortOrder !== null || legacy.assessmentKey === input.sinkIdentifier) {
106695
+ if (legacy.status !== "archived" || legacy.assessmentKey === input.sinkIdentifier) {
104782
106696
  await input.tx.update(gameTimebackAssessmentTests).set({
104783
106697
  status: "archived",
104784
- sortOrder: null,
104785
106698
  ...legacy.assessmentKey === input.sinkIdentifier ? { assessmentKey: null } : {},
104786
106699
  updatedAt: new Date
104787
106700
  }).where(eq(gameTimebackAssessmentTests.id, legacy.id));
@@ -104795,7 +106708,6 @@ class TimebackAssessmentsService {
104795
106708
  assessmentKey: input.sinkIdentifier,
104796
106709
  purpose: "review",
104797
106710
  status: "live",
104798
- sortOrder: null,
104799
106711
  standardFramework: null,
104800
106712
  standardIdentifier: null,
104801
106713
  diagnosticKey: null,
@@ -104810,7 +106722,6 @@ class TimebackAssessmentsService {
104810
106722
  qtiTestIdentifier: input.sinkIdentifier,
104811
106723
  purpose: "review",
104812
106724
  status: "live",
104813
- sortOrder: null,
104814
106725
  standardFramework: null,
104815
106726
  standardIdentifier: null,
104816
106727
  diagnosticKey: null,
@@ -104948,15 +106859,64 @@ class TimebackAssessmentsService {
104948
106859
  return get();
104949
106860
  }
104950
106861
  async ensureManagedQtiPublication(client2, plan) {
104951
- await runWithConcurrency(plan.items, QTI_HYDRATION_CONCURRENCY, async (plannedItem) => {
106862
+ const existingItems = await runWithConcurrency(plan.items, QTI_HYDRATION_CONCURRENCY, async (plannedItem) => {
104952
106863
  const existing = await this.ensureImmutableQtiResource(async () => await client2.qtiApi.assessmentItems.get(plannedItem.qtiItemIdentifier), () => client2.course.createAssessmentItemXml({
104953
106864
  xml: plannedItem.xml,
104954
106865
  metadata: plannedItem.metadata
104955
106866
  }));
104956
106867
  await assertManagedAssessmentItem(existing, plannedItem);
106868
+ return existing;
104957
106869
  });
104958
106870
  const existingTest = await this.ensureImmutableQtiResource(async () => await client2.qtiApi.assessmentTests.get(plan.qtiTestIdentifier), () => client2.qtiApi.assessmentTests.create(plan.testInput));
104959
106871
  assertManagedAssessmentPublication(existingTest, plan);
106872
+ return {
106873
+ test: existingTest,
106874
+ questions: {
106875
+ ...plan.artifactSource.questions,
106876
+ assessmentTest: existingTest.identifier,
106877
+ title: existingTest.title,
106878
+ totalQuestions: existingItems.length,
106879
+ questions: plan.artifactSource.questions.questions.map((entry, index2) => ({
106880
+ ...entry,
106881
+ reference: {
106882
+ ...entry.reference,
106883
+ identifier: existingItems[index2].identifier
106884
+ },
106885
+ question: existingItems[index2]
106886
+ }))
106887
+ }
106888
+ };
106889
+ }
106890
+ async compileAndStoreArtifacts(qtiTestIdentifier, identityKind, compile) {
106891
+ const startedAt = Date.now();
106892
+ try {
106893
+ const compiled = await compile();
106894
+ const stored = await this.deps.assessmentArtifacts.storePair(compiled);
106895
+ addEvent("assessment.artifacts_compiled", {
106896
+ "app.assessment.qti_test_identifier": compiled.identity.qtiTestIdentifier,
106897
+ "app.assessment.artifact_identity_kind": compiled.identity.kind,
106898
+ "app.assessment.artifact_duration_ms": Date.now() - startedAt,
106899
+ "app.assessment.artifact_fallback_item_count": qtiFallbackItemCount(compiled),
106900
+ "app.assessment.playable_artifact_status": stored.playable.status,
106901
+ "app.assessment.scoring_artifact_status": stored.scoring.status
106902
+ });
106903
+ } catch (error88) {
106904
+ return this.handleArtifactFailure(qtiTestIdentifier, identityKind, startedAt, error88);
106905
+ }
106906
+ return true;
106907
+ }
106908
+ handleArtifactFailure(qtiTestIdentifier, identityKind, startedAt, error88) {
106909
+ addEvent("assessment.artifact_compilation_failed", {
106910
+ "app.assessment.qti_test_identifier": qtiTestIdentifier,
106911
+ "app.assessment.artifact_identity_kind": identityKind,
106912
+ "app.assessment.artifact_duration_ms": Date.now() - startedAt,
106913
+ "exception.type": errorType(error88),
106914
+ "app.error.message": errorMessage(error88)
106915
+ });
106916
+ return assessmentArtifactFailureFallback(error88);
106917
+ }
106918
+ async storeManagedArtifacts(source, diagnosticRoutingManifest) {
106919
+ return this.compileAndStoreArtifacts(source.test.identifier, "assessment", () => compileManagedAssessmentArtifacts({ source, diagnosticRoutingManifest }));
104960
106920
  }
104961
106921
  async publishManagedAssessment(row, purpose, diagnosticRoutingManifest, loaded) {
104962
106922
  if (!row.assessmentKey) {
@@ -104971,8 +106931,10 @@ class TimebackAssessmentsService {
104971
106931
  diagnosticRoutingManifest,
104972
106932
  itemHref: (identifier) => qtiItemHref(client2, identifier)
104973
106933
  });
104974
- await this.ensureManagedQtiPublication(client2, plan);
104975
- return plan;
106934
+ const artifactSource = await this.ensureManagedQtiPublication(client2, plan);
106935
+ const artifactsAvailable = await this.storeManagedArtifacts(artifactSource, managedAssessmentDiagnosticRoutingManifest(artifactSource.test, plan.assessmentKey));
106936
+ const warning = assessmentArtifactPublicationWarning(artifactsAvailable);
106937
+ return { ...plan, ...warning ? { warning } : {} };
104976
106938
  }
104977
106939
  async cleanupQtiAssessmentCopy(client2, testIdentifier, itemIdentifiers) {
104978
106940
  let testDeleted = !testIdentifier;
@@ -105005,7 +106967,7 @@ class TimebackAssessmentsService {
105005
106967
  }
105006
106968
  }
105007
106969
  }
105008
- associationSummary(row) {
106970
+ associationSummary(row, warning) {
105009
106971
  return {
105010
106972
  id: row.id,
105011
106973
  integrationId: row.integrationId,
@@ -105013,11 +106975,11 @@ class TimebackAssessmentsService {
105013
106975
  qtiTestIdentifier: row.qtiTestIdentifier,
105014
106976
  purpose: row.purpose,
105015
106977
  status: row.status,
105016
- sortOrder: row.sortOrder,
105017
106978
  standard: assessmentStandardForRow(row),
105018
106979
  diagnostic: diagnosticDefinitionForRow(row),
105019
106980
  createdAt: row.createdAt,
105020
- updatedAt: row.updatedAt
106981
+ updatedAt: row.updatedAt,
106982
+ ...warning ? { warning } : {}
105021
106983
  };
105022
106984
  }
105023
106985
  associationImportSummary(row) {
@@ -105085,7 +107047,7 @@ class TimebackAssessmentsService {
105085
107047
  };
105086
107048
  }
105087
107049
  }
105088
- var REVIEW_BANK_TITLE = "Review Bank", REVIEW_BANK_SYNC_ATTEMPTS = 2;
107050
+ var REVIEW_BANK_SYNC_ATTEMPTS = 2;
105089
107051
  var init_timeback_assessments_service = __esm(async () => {
105090
107052
  init_drizzle_orm();
105091
107053
  init_helpers_index();
@@ -105094,6 +107056,8 @@ var init_timeback_assessments_service = __esm(async () => {
105094
107056
  init_assessment_runtime2();
105095
107057
  init_timeback3();
105096
107058
  init_errors2();
107059
+ init_assessment_artifact_fallback_util();
107060
+ init_timeback_assessment_artifacts_util();
105097
107061
  init_timeback_assessment_diagnostic_util();
105098
107062
  init_timeback_assessment_import_util();
105099
107063
  init_timeback_assessment_publication_util();
@@ -107117,6 +109081,8 @@ function createPlatformServices(deps) {
107117
109081
  timebackClient,
107118
109082
  alerts,
107119
109083
  mintPlatformServiceToken,
109084
+ mintAssessmentPreparationReceipt,
109085
+ verifyAssessmentPreparationReceipt,
107120
109086
  validateDeveloperAccessBySlug,
107121
109087
  validateDeveloperAccess,
107122
109088
  validateGameManagementAccess
@@ -107132,6 +109098,10 @@ function createPlatformServices(deps) {
107132
109098
  environment: getPlatformEnvironment(config5),
107133
109099
  storage
107134
109100
  });
109101
+ const assessmentArtifacts = new AssessmentArtifactsService({
109102
+ bucketName: config5.assessmentArtifactBucket,
109103
+ storage
109104
+ });
107135
109105
  const upload = new UploadService({
107136
109106
  uploadBucket: config5.uploadBucket,
107137
109107
  generatePresignedPutUrl: storage.generatePresignedPutUrl.bind(storage),
@@ -107177,16 +109147,21 @@ function createPlatformServices(deps) {
107177
109147
  const timebackAssessments = new TimebackAssessmentsService({
107178
109148
  db: db2,
107179
109149
  timeback: timebackClient,
109150
+ assessmentArtifacts,
107180
109151
  validateGameManagementAccess
107181
109152
  });
107182
109153
  const timebackAssessmentRuntime = new TimebackAssessmentRuntimeService({
107183
109154
  db: db2,
107184
109155
  timeback: timebackClient,
109156
+ assessmentArtifacts,
107185
109157
  validateDeveloperAccess,
107186
- assessmentRuntimeLock
109158
+ assessmentRuntimeLock,
109159
+ mintAssessmentPreparationReceipt,
109160
+ verifyAssessmentPreparationReceipt
107187
109161
  });
107188
109162
  return {
107189
109163
  assessmentAssets,
109164
+ assessmentArtifacts,
107190
109165
  bucket,
107191
109166
  upload,
107192
109167
  kv,
@@ -107203,6 +109178,7 @@ function createPlatformServices(deps) {
107203
109178
  }
107204
109179
  var init_platform2 = __esm(async () => {
107205
109180
  init_config2();
109181
+ init_assessment_artifacts_service();
107206
109182
  init_assessment_assets_service();
107207
109183
  init_bucket_service();
107208
109184
  init_database_service();
@@ -107929,6 +109905,8 @@ function createServices(ctx) {
107929
109905
  timebackClient: timeback2,
107930
109906
  alerts: infra2.alerts,
107931
109907
  mintPlatformServiceToken: auth2.mintPlatformServiceToken.bind(auth2),
109908
+ mintAssessmentPreparationReceipt: auth2.mintAssessmentPreparationReceipt.bind(auth2),
109909
+ verifyAssessmentPreparationReceipt: auth2.verifyAssessmentPreparationReceipt.bind(auth2),
107932
109910
  ...gaming.validators
107933
109911
  });
107934
109912
  const standalone = createStandaloneServices({ db: db2, auth: auth2, timeback: timeback2 });
@@ -108068,6 +110046,15 @@ function defineControllerNames(namespace, handlers) {
108068
110046
  return handlers;
108069
110047
  }
108070
110048
 
110049
+ // ../api-core/src/utils/assessment-artifact-backfill.util.ts
110050
+ var init_assessment_artifact_backfill_util = __esm(() => {
110051
+ init_tables_index();
110052
+ init_timeback_assessment_artifacts_util();
110053
+ init_timeback_assessment_publication_util();
110054
+ init_timeback_qti_hydration_util();
110055
+ init_timeback_review_mapping_util();
110056
+ });
110057
+
108071
110058
  // ../api-core/src/utils/lti.util.ts
108072
110059
  function generateUsername(email5) {
108073
110060
  const baseUsername = (email5.split("@")[0] || "user").toLowerCase();
@@ -108273,6 +110260,9 @@ var init_validation_util = __esm(() => {
108273
110260
  var init_utils7 = __esm(() => {
108274
110261
  init_auth_util();
108275
110262
  init_assessment_runtime_lock_util();
110263
+ init_assessment_artifact_envelope_util();
110264
+ init_assessment_artifact_backfill_util();
110265
+ init_timeback_assessment_artifacts_util();
108276
110266
  init_dashboard_util();
108277
110267
  init_deployment_util();
108278
110268
  init_leaderboard_util();
@@ -108430,6 +110420,32 @@ function createSandboxAuthProvider() {
108430
110420
  }));
108431
110421
  return `${header}.${payload}.sandbox`;
108432
110422
  },
110423
+ async mintAssessmentPreparationReceipt(plan) {
110424
+ const now2 = Date.now();
110425
+ for (const [receipt2, prepared] of sandboxAssessmentPreparationReceipts) {
110426
+ if (prepared.expiresAt <= now2) {
110427
+ sandboxAssessmentPreparationReceipts.delete(receipt2);
110428
+ }
110429
+ }
110430
+ const receipt = `sandbox_assessment_preparation_${crypto.randomUUID()}`;
110431
+ const expiresAt = now2 + ASSESSMENT_PREPARATION_RECEIPT_LIFETIME_MS;
110432
+ sandboxAssessmentPreparationReceipts.set(receipt, {
110433
+ expiresAt,
110434
+ plan: structuredClone(plan)
110435
+ });
110436
+ return { receipt, expiresAt: new Date(expiresAt).toISOString() };
110437
+ },
110438
+ async verifyAssessmentPreparationReceipt(receipt) {
110439
+ const prepared = sandboxAssessmentPreparationReceipts.get(receipt);
110440
+ if (!prepared) {
110441
+ return null;
110442
+ }
110443
+ if (prepared.expiresAt <= Date.now()) {
110444
+ sandboxAssessmentPreparationReceipts.delete(receipt);
110445
+ return null;
110446
+ }
110447
+ return structuredClone(prepared.plan);
110448
+ },
108433
110449
  async mintLogStreamToken(userId, workerId) {
108434
110450
  const jti = crypto.randomUUID();
108435
110451
  const payload = {
@@ -108471,11 +110487,13 @@ function createSandboxAuthProvider() {
108471
110487
  }
108472
110488
  };
108473
110489
  }
108474
- var sandboxApiKeys;
110490
+ var sandboxApiKeys, sandboxAssessmentPreparationReceipts, ASSESSMENT_PREPARATION_RECEIPT_LIFETIME_MS;
108475
110491
  var init_auth_provider = __esm(() => {
108476
110492
  init_src6();
108477
110493
  init_constants();
108478
110494
  sandboxApiKeys = new Map;
110495
+ sandboxAssessmentPreparationReceipts = new Map;
110496
+ ASSESSMENT_PREPARATION_RECEIPT_LIFETIME_MS = 5 * 60 * 1000;
108479
110497
  });
108480
110498
 
108481
110499
  // src/infrastructure/api/providers/cache.provider.ts
@@ -108650,6 +110668,7 @@ function buildConfig(options) {
108650
110668
  baseUrl,
108651
110669
  gameDomain: "localhost",
108652
110670
  uploadBucket: "sandbox-uploads",
110671
+ assessmentArtifactBucket: "sandbox-assessment-artifacts",
108653
110672
  ltiTestMode: true,
108654
110673
  secretsManifestPepper: "sandbox-secrets-manifest-pepper",
108655
110674
  ...options.config
@@ -165107,7 +167126,7 @@ function parseQtiLibraryParams(searchParams) {
165107
167126
  limit
165108
167127
  };
165109
167128
  }
165110
- var populateStudent, getUser, getUserEnrollments, getUserById, setupIntegration, getIntegrations, getRemovedIntegrations, createIntegration, updateIntegration, deactivateCourse, reactivateCourse, getIntegrationConfig, verifyIntegration, getConfig, deleteIntegrations, endActivity, heartbeat, advanceCourse, unenrollCourse, startRuntimeAssessment, getRuntimeAssessment, getLatestRuntimeAssessment, saveRuntimeAssessment, submitRuntimeAssessmentItem, submitRuntimeAssessment, finalizeRuntimeAssessment, exportRuntimeAssessmentFixture, getStudentXp, getStudentMastery, getStudentHighestGradeMastered, getRoster, getStudentOverview, getGameMetrics, getStudentActivity, getGradeLevelTestResults, getGradeLevelTestReview, getActivityDetail, listMetricDiscrepancies, verifyMetricDiscrepancy, grantXp, adjustTime, adjustMastery, reconcileMasteryForConfigChange, searchStudents, enrollStudent, unenrollStudent, reactivateEnrollment, listAssessments, createAssessment, attachExistingAssessments, updateAssessment, synchronizeReviewBank, reorderAssessments, reorderQuestions, removeAssessment, listQuestions, listQuestionLibrary, listTestLibrary, copyAssessment, createQuestion, updateQuestion, removeQuestion, timeback2;
167129
+ var populateStudent, getUser, getUserEnrollments, getUserById, setupIntegration, getIntegrations, getRemovedIntegrations, createIntegration, updateIntegration, deactivateCourse, reactivateCourse, getIntegrationConfig, verifyIntegration, getConfig, deleteIntegrations, endActivity, heartbeat, advanceCourse, unenrollCourse, prepareRuntimeAssessment, startRuntimeAssessment, getRuntimeAssessment, getLatestRuntimeAssessment, saveRuntimeAssessment, submitRuntimeAssessmentItem, submitRuntimeAssessment, finalizeRuntimeAssessment, exportRuntimeAssessmentFixture, getStudentXp, getStudentMastery, getStudentHighestGradeMastered, getRoster, getStudentOverview, getGameMetrics, getStudentActivity, getGradeLevelTestResults, getGradeLevelTestReview, getActivityDetail, listMetricDiscrepancies, verifyMetricDiscrepancy, grantXp, adjustTime, adjustMastery, reconcileMasteryForConfigChange, searchStudents, enrollStudent, unenrollStudent, reactivateEnrollment, listAssessments, createAssessment, attachExistingAssessments, updateAssessment, synchronizeReviewBank, reorderQuestions, removeAssessment, listQuestions, listQuestionLibrary, listTestLibrary, copyAssessment, createQuestion, updateQuestion, removeQuestion, timeback2;
165111
167130
  var init_timeback_controller = __esm(() => {
165112
167131
  init_esm();
165113
167132
  init_src();
@@ -165351,13 +167370,24 @@ var init_timeback_controller = __esm(() => {
165351
167370
  user: ctx.user
165352
167371
  });
165353
167372
  });
167373
+ prepareRuntimeAssessment = requireDeveloper(async (ctx) => {
167374
+ const body2 = await parseRequestBody(ctx.request, PrepareRuntimeAssessmentRequestSchema);
167375
+ const { gameId, studentId, ...input } = body2;
167376
+ return ctx.services.timebackAssessmentRuntime.prepare({
167377
+ gameId,
167378
+ studentId,
167379
+ input,
167380
+ user: ctx.user
167381
+ });
167382
+ });
165354
167383
  startRuntimeAssessment = requireDeveloper(async (ctx) => {
165355
167384
  const body2 = await parseRequestBody(ctx.request, StartRuntimeAssessmentRequestSchema);
165356
- const { gameId, studentId, ...input } = body2;
167385
+ const { gameId, studentId, preparationReceipt, ...input } = body2;
165357
167386
  return ctx.services.timebackAssessmentRuntime.start({
165358
167387
  gameId,
165359
167388
  studentId,
165360
167389
  input,
167390
+ preparationReceipt,
165361
167391
  user: ctx.user
165362
167392
  });
165363
167393
  });
@@ -165813,16 +167843,6 @@ var init_timeback_controller = __esm(() => {
165813
167843
  const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
165814
167844
  return ctx.services.timebackAssessments.synchronizeReviewBank(integrationId);
165815
167845
  });
165816
- reorderAssessments = requireDeveloper(async (ctx) => {
165817
- const { gameId, courseId } = ctx.params;
165818
- if (!gameId || !courseId) {
165819
- throw ApiError.badRequest("Missing gameId or courseId parameter");
165820
- }
165821
- const body2 = await parseRequestBody(ctx.request, ReorderAssessmentsRequestSchema);
165822
- const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
165823
- await ctx.services.timebackAssessments.reorderAssessments(integrationId, body2.purpose, body2.testIdentifiers);
165824
- return { success: true };
165825
- });
165826
167846
  reorderQuestions = requireDeveloper(async (ctx) => {
165827
167847
  const { gameId, courseId, testIdentifier } = ctx.params;
165828
167848
  if (!gameId || !courseId || !testIdentifier) {
@@ -165930,6 +167950,7 @@ var init_timeback_controller = __esm(() => {
165930
167950
  heartbeat,
165931
167951
  advanceCourse,
165932
167952
  unenrollCourse,
167953
+ prepareRuntimeAssessment,
165933
167954
  startRuntimeAssessment,
165934
167955
  getLatestRuntimeAssessment,
165935
167956
  getRuntimeAssessment,
@@ -165963,7 +167984,6 @@ var init_timeback_controller = __esm(() => {
165963
167984
  attachExistingAssessments,
165964
167985
  updateAssessment,
165965
167986
  synchronizeReviewBank,
165966
- reorderAssessments,
165967
167987
  removeAssessment,
165968
167988
  reorderQuestions,
165969
167989
  listQuestions,
@@ -166923,6 +168943,7 @@ var init_timeback8 = __esm(async () => {
166923
168943
  timebackRouter.post("/heartbeat", handle2(timeback2.heartbeat));
166924
168944
  timebackRouter.post("/advance-course", handle2(timeback2.advanceCourse));
166925
168945
  timebackRouter.post("/unenroll-course", handle2(timeback2.unenrollCourse));
168946
+ timebackRouter.post("/assessments/prepare", handle2(timeback2.prepareRuntimeAssessment));
166926
168947
  timebackRouter.post("/assessments/start", handle2(timeback2.startRuntimeAssessment));
166927
168948
  timebackRouter.get("/assessments/latest", handle2(timeback2.getLatestRuntimeAssessment));
166928
168949
  timebackRouter.get("/assessments/:attemptId", handle2(timeback2.getRuntimeAssessment));