@playcademy/sandbox 0.7.1-beta.22 → 0.7.1-beta.23

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 +563 -184
  2. package/dist/server.js +563 -184
  3. package/package.json +1 -1
package/dist/server.js CHANGED
@@ -1122,7 +1122,7 @@ var package_default;
1122
1122
  var init_package = __esm(() => {
1123
1123
  package_default = {
1124
1124
  name: "@playcademy/sandbox",
1125
- version: "0.7.1-beta.22",
1125
+ version: "0.7.1-beta.23",
1126
1126
  description: "Local development server for Playcademy game development",
1127
1127
  type: "module",
1128
1128
  exports: {
@@ -9455,6 +9455,9 @@ function isAssessmentAttemptOpen(attempt) {
9455
9455
  function isAssessmentAttemptCompleted(attempt) {
9456
9456
  return attempt.inProgress === ASSESSMENT_ATTEMPT_COMPLETED.inProgress && attempt.scoreStatus === ASSESSMENT_ATTEMPT_COMPLETED.scoreStatus;
9457
9457
  }
9458
+ function isAssessmentAttemptSuperseded(attempt) {
9459
+ return attempt.inProgress === ASSESSMENT_ATTEMPT_SUPERSEDED.inProgress && attempt.scoreStatus === ASSESSMENT_ATTEMPT_SUPERSEDED.scoreStatus;
9460
+ }
9458
9461
  function classifyAssessmentSubmission(attempt, submissionId) {
9459
9462
  if (isAssessmentAttemptCompleted(attempt)) {
9460
9463
  return attempt.submissionId === submissionId ? "replay" : "reject";
@@ -9512,6 +9515,15 @@ function assessmentReviewBankShortage(fulfillment) {
9512
9515
  function assessmentFlowForPurpose(purpose) {
9513
9516
  return purpose === "review" ? "item-submit" : "attempt-submit";
9514
9517
  }
9518
+ function compareCodeUnits(left, right) {
9519
+ if (left < right) {
9520
+ return -1;
9521
+ }
9522
+ if (left > right) {
9523
+ return 1;
9524
+ }
9525
+ return 0;
9526
+ }
9515
9527
  function canonicalJson(value) {
9516
9528
  if (value === null || typeof value !== "object") {
9517
9529
  return JSON.stringify(value) ?? "null";
@@ -9519,18 +9531,9 @@ function canonicalJson(value) {
9519
9531
  if (Array.isArray(value)) {
9520
9532
  return `[${value.map((item) => canonicalJson(item)).join(",")}]`;
9521
9533
  }
9522
- const entries = Object.entries(value).filter(([, entryValue]) => entryValue !== undefined).toSorted(([left], [right]) => compareCanonicalKeys(left, right)).map(([key, entryValue]) => `${JSON.stringify(key)}:${canonicalJson(entryValue)}`);
9534
+ const entries = Object.entries(value).filter(([, entryValue]) => entryValue !== undefined).toSorted(([left], [right]) => compareCodeUnits(left, right)).map(([key, entryValue]) => `${JSON.stringify(key)}:${canonicalJson(entryValue)}`);
9523
9535
  return `{${entries.join(",")}}`;
9524
9536
  }
9525
- function compareCanonicalKeys(left, right) {
9526
- if (left < right) {
9527
- return -1;
9528
- }
9529
- if (left > right) {
9530
- return 1;
9531
- }
9532
- return 0;
9533
- }
9534
9537
  async function diagnosticRoutingRevision(manifest) {
9535
9538
  const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(canonicalJson(manifest)));
9536
9539
  return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
@@ -10830,15 +10833,6 @@ function assessmentPresentationForAttempt(assessment, attemptId) {
10830
10833
  });
10831
10834
  return { ...assessment, items };
10832
10835
  }
10833
- function compareCodeUnits(left, right) {
10834
- if (left < right) {
10835
- return -1;
10836
- }
10837
- if (left > right) {
10838
- return 1;
10839
- }
10840
- return 0;
10841
- }
10842
10836
  function reviewStandardFieldsWithinLimits(input) {
10843
10837
  return input.framework.trim().length <= TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.standardFieldLength && input.identifier.trim().length <= TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.standardFieldLength;
10844
10838
  }
@@ -10849,6 +10843,19 @@ function canonicalReviewStandardRef(input) {
10849
10843
  }
10850
10844
  return standard.framework && standard.identifier ? standard : null;
10851
10845
  }
10846
+ function isKnownDescriptiveCurriculumFramework(framework) {
10847
+ return framework === "CCSS" || framework.startsWith("CCSS.");
10848
+ }
10849
+ function reviewRoutingStandardRefsFromMetadata(metadata2, options = {}) {
10850
+ const refs = new Map;
10851
+ for (const authored of qtiAssessmentStandardRefsFromMetadata(metadata2)) {
10852
+ const standard = canonicalReviewStandardRef(authored);
10853
+ if (standard && (!options.customOnly || !isKnownDescriptiveCurriculumFramework(standard.framework))) {
10854
+ refs.set(assessmentStandardRefKey(standard), standard);
10855
+ }
10856
+ }
10857
+ return [...refs.values()];
10858
+ }
10852
10859
  function canonicalRequestStandards(standards) {
10853
10860
  if (standards.length > TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.standards) {
10854
10861
  throw new RangeError(`A review request may contain at most ${TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.standards} standards`);
@@ -10904,42 +10911,172 @@ function qtiQuestionByIdentifier(questions) {
10904
10911
  }
10905
10912
  return byIdentifier;
10906
10913
  }
10907
- async function buildReviewBankIndex(assessment, questions) {
10914
+ async function reviewBankRevision(input) {
10915
+ const alignmentSignature = input.items.map((item) => ({
10916
+ itemIdentifier: item.itemIdentifier,
10917
+ standardKeys: item.standards.map(assessmentStandardRefKey)
10918
+ }));
10919
+ const bankRevisionUuid = await deterministicUUID(`playcademy:review-bank:v1:${JSON.stringify({
10920
+ bankIdentifier: input.bankIdentifier,
10921
+ contentRevision: input.sourceContentRevision,
10922
+ alignments: alignmentSignature
10923
+ })}`);
10924
+ return `review-bank-v1:${bankRevisionUuid}`;
10925
+ }
10926
+ async function reviewBankIndex(input) {
10927
+ return {
10928
+ ...input,
10929
+ bankRevision: await reviewBankRevision(input)
10930
+ };
10931
+ }
10932
+ async function reviewBankSourceFingerprint(input) {
10933
+ const fingerprintUuid = await deterministicUUID(`playcademy:review-bank-source:v1:${JSON.stringify({
10934
+ bankIdentifier: input.bankIdentifier,
10935
+ items: input.items.map((item) => ({
10936
+ itemIdentifier: item.itemIdentifier,
10937
+ standardKeys: item.standards.map(assessmentStandardRefKey)
10938
+ }))
10939
+ })}`);
10940
+ return `review-bank-source-v1:${fingerprintUuid}`;
10941
+ }
10942
+ async function buildReviewBankIndex(assessment, questions, options = {}) {
10908
10943
  if (assessment.identifier !== questions.assessmentTest) {
10909
10944
  throw new Error(`QTI questions for ${questions.assessmentTest} do not describe assessment ${assessment.identifier}`);
10910
10945
  }
10911
10946
  const questionByIdentifier = qtiQuestionByIdentifier(questions);
10912
10947
  const items = assessment.items.map((item) => {
10913
10948
  const question = questionByIdentifier.get(item.identifier);
10914
- const canonical = new Map;
10915
- for (const authored of qtiAssessmentStandardRefsFromMetadata(question?.metadata)) {
10916
- const standard = canonicalReviewStandardRef(authored);
10917
- if (standard) {
10918
- canonical.set(assessmentStandardRefKey(standard), standard);
10919
- }
10920
- }
10921
- const standards = [...canonical.values()].toSorted((left, right) => compareCodeUnits(assessmentStandardRefKey(left), assessmentStandardRefKey(right)));
10949
+ const standards = reviewRoutingStandardRefsFromMetadata(question?.metadata, options).toSorted((left, right) => compareCodeUnits(assessmentStandardRefKey(left), assessmentStandardRefKey(right)));
10922
10950
  return {
10923
10951
  itemIdentifier: item.identifier,
10924
10952
  standards
10925
10953
  };
10926
10954
  });
10927
- const alignmentSignature = items.map((item) => ({
10928
- itemIdentifier: item.itemIdentifier,
10929
- standardKeys: item.standards.map(assessmentStandardRefKey)
10930
- }));
10931
- const bankRevisionUuid = await deterministicUUID(`playcademy:review-bank:v1:${JSON.stringify({
10932
- bankIdentifier: assessment.identifier,
10933
- contentRevision: assessment.contentRevision,
10934
- alignments: alignmentSignature
10935
- })}`);
10936
- return {
10955
+ return reviewBankIndex({
10937
10956
  bankIdentifier: assessment.identifier,
10938
10957
  sourceContentRevision: assessment.contentRevision,
10939
- bankRevision: `review-bank-v1:${bankRevisionUuid}`,
10940
10958
  items
10959
+ });
10960
+ }
10961
+ function emptyReviewBankManifest(bankIdentifier) {
10962
+ return {
10963
+ version: PLAYCADEMY_REVIEW_BANK_MANIFEST_VERSION,
10964
+ bankIdentifier,
10965
+ sourceFingerprint: null,
10966
+ sourceContentRevision: null,
10967
+ bankRevision: null,
10968
+ itemsByStandard: {}
10941
10969
  };
10942
10970
  }
10971
+ function reviewBankMappingIssues(bank) {
10972
+ return {
10973
+ missingOrInvalidItemIdentifiers: bank.items.filter((item) => item.standards.length === 0).map((item) => item.itemIdentifier),
10974
+ multipleStandardItemIdentifiers: bank.items.filter((item) => item.standards.length > 1).map((item) => item.itemIdentifier)
10975
+ };
10976
+ }
10977
+ async function buildReviewBankManifest(bank) {
10978
+ const itemsByStandard = new Map;
10979
+ for (const item of bank.items) {
10980
+ for (const standard of item.standards) {
10981
+ const key = assessmentStandardRefKey(standard);
10982
+ const identifiers = itemsByStandard.get(key) ?? [];
10983
+ identifiers.push(item.itemIdentifier);
10984
+ itemsByStandard.set(key, identifiers);
10985
+ }
10986
+ }
10987
+ return {
10988
+ version: PLAYCADEMY_REVIEW_BANK_MANIFEST_VERSION,
10989
+ bankIdentifier: bank.bankIdentifier,
10990
+ sourceFingerprint: await reviewBankSourceFingerprint(bank),
10991
+ sourceContentRevision: bank.sourceContentRevision,
10992
+ bankRevision: bank.bankRevision,
10993
+ itemsByStandard: Object.fromEntries([...itemsByStandard.entries()].toSorted(([left], [right]) => compareCodeUnits(left, right)))
10994
+ };
10995
+ }
10996
+ function requiredManifestString(record, field) {
10997
+ const value = record[field];
10998
+ if (typeof value !== "string" || !value.trim()) {
10999
+ throw new Error(`Review-bank manifest requires ${field}`);
11000
+ }
11001
+ return value.trim();
11002
+ }
11003
+ function standardFromManifestKey(key) {
11004
+ try {
11005
+ const parsed = JSON.parse(key);
11006
+ if (!Array.isArray(parsed) || parsed.length !== 2 || typeof parsed[0] !== "string" || typeof parsed[1] !== "string") {
11007
+ return null;
11008
+ }
11009
+ const standard = canonicalReviewStandardRef({
11010
+ framework: parsed[0],
11011
+ identifier: parsed[1]
11012
+ });
11013
+ return standard && assessmentStandardRefKey(standard) === key ? standard : null;
11014
+ } catch {
11015
+ return null;
11016
+ }
11017
+ }
11018
+ async function reviewBankIndexFromManifest(metadata2, input) {
11019
+ const raw = isRecord(metadata2) ? metadata2[PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY] : undefined;
11020
+ if (raw === undefined) {
11021
+ return null;
11022
+ }
11023
+ if (!isRecord(raw) || raw.version !== PLAYCADEMY_REVIEW_BANK_MANIFEST_VERSION) {
11024
+ throw new Error("Review-bank manifest has an unsupported version or shape");
11025
+ }
11026
+ const manifest = raw;
11027
+ const bankIdentifier = requiredManifestString(manifest, "bankIdentifier");
11028
+ if (bankIdentifier !== input.bankIdentifier) {
11029
+ throw new Error(`Review-bank manifest for ${bankIdentifier} does not describe ${input.bankIdentifier}`);
11030
+ }
11031
+ if (manifest.sourceFingerprint === null && manifest.sourceContentRevision === null && manifest.bankRevision === null) {
11032
+ return null;
11033
+ }
11034
+ const sourceFingerprint = requiredManifestString(manifest, "sourceFingerprint");
11035
+ const sourceContentRevision = requiredManifestString(manifest, "sourceContentRevision");
11036
+ const declaredBankRevision = requiredManifestString(manifest, "bankRevision");
11037
+ const itemsByStandard = manifest.itemsByStandard;
11038
+ if (!isRecord(itemsByStandard)) {
11039
+ throw new Error("Review-bank manifest requires an itemsByStandard mapping");
11040
+ }
11041
+ const membership = new Set(input.membershipItemIdentifiers);
11042
+ const standardsByItem = new Map;
11043
+ for (const [standardKey, rawItemIdentifiers] of Object.entries(itemsByStandard)) {
11044
+ const standard = standardFromManifestKey(standardKey);
11045
+ if (!standard || !Array.isArray(rawItemIdentifiers)) {
11046
+ throw new Error("Review-bank manifest contains an invalid standard mapping");
11047
+ }
11048
+ for (const rawItemIdentifier of rawItemIdentifiers) {
11049
+ if (typeof rawItemIdentifier !== "string" || !rawItemIdentifier.trim()) {
11050
+ throw new Error("Review-bank manifest contains an invalid item identifier");
11051
+ }
11052
+ const itemIdentifier = rawItemIdentifier.trim();
11053
+ if (membership.has(itemIdentifier)) {
11054
+ const standards = standardsByItem.get(itemIdentifier) ?? new Map;
11055
+ standards.set(assessmentStandardRefKey(standard), standard);
11056
+ standardsByItem.set(itemIdentifier, standards);
11057
+ }
11058
+ }
11059
+ }
11060
+ const index = await reviewBankIndex({
11061
+ bankIdentifier,
11062
+ sourceContentRevision,
11063
+ items: input.membershipItemIdentifiers.map((itemIdentifier) => ({
11064
+ itemIdentifier,
11065
+ standards: [...standardsByItem.get(itemIdentifier)?.values() ?? []].toSorted((left, right) => compareCodeUnits(assessmentStandardRefKey(left), assessmentStandardRefKey(right)))
11066
+ }))
11067
+ });
11068
+ const issues = reviewBankMappingIssues(index);
11069
+ if (issues.missingOrInvalidItemIdentifiers.length > 0 || issues.multipleStandardItemIdentifiers.length > 0) {
11070
+ throw new Error("Review-bank manifest must map every member to exactly one standard/node key");
11071
+ }
11072
+ if (await reviewBankSourceFingerprint(index) !== sourceFingerprint) {
11073
+ throw new Error("Review-bank manifest source fingerprint is stale");
11074
+ }
11075
+ if (index.bankRevision !== declaredBankRevision) {
11076
+ throw new Error("Review-bank manifest revision does not match its candidate index");
11077
+ }
11078
+ return index;
11079
+ }
10943
11080
  function exposureTimestamp(value) {
10944
11081
  const timestamp = Date.parse(value);
10945
11082
  return Number.isFinite(timestamp) ? timestamp : 0;
@@ -11390,7 +11527,7 @@ function playcademyDiagnosticAssessmentItemResultMetadata(value) {
11390
11527
  const normalized = metadata2.responses === undefined ? { ...metadata2, responses: {} } : metadata2;
11391
11528
  return isPlaycademyDiagnosticAssessmentItemResultMetadataV1(normalized) ? normalized : null;
11392
11529
  }
11393
- 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_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", DEFAULT_REVIEW_SELECTION_POLICY, RuntimeSubjectSchema, RuntimeGradeSchema, OptionalQueryGradeSchema, AssessmentResponseValueSchema, AssessmentRuntimeIdentitySchema, ResponseKeySchema, StartAssessmentBaseSchema, AssessmentStandardRefSchema, LatestAssessmentFilterBaseSchema, LatestAssessmentFiltersSchema, LatestRuntimeAssessmentQuerySchema, StartAssessmentBodySchema, SaveAssessmentBodySchema, SubmitAssessmentItemBodySchema, SubmitAssessmentBodySchema, StartRuntimeAssessmentRequestSchema, SaveRuntimeAssessmentRequestSchema, SubmitAssessmentItemRuntimeRequestSchema, SubmitRuntimeAssessmentRequestSchema;
11530
+ 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_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, RuntimeSubjectSchema, RuntimeGradeSchema, OptionalQueryGradeSchema, AssessmentResponseValueSchema, AssessmentRuntimeIdentitySchema, ResponseKeySchema, StartAssessmentBaseSchema, AssessmentStandardRefSchema, LatestAssessmentFilterBaseSchema, LatestAssessmentFiltersSchema, LatestRuntimeAssessmentQuerySchema, StartAssessmentBodySchema, SaveAssessmentBodySchema, SubmitAssessmentItemBodySchema, SubmitAssessmentBodySchema, StartRuntimeAssessmentRequestSchema, SaveRuntimeAssessmentRequestSchema, SubmitAssessmentItemRuntimeRequestSchema, SubmitRuntimeAssessmentRequestSchema;
11394
11531
  var init_assessment_runtime2 = __esm(() => {
11395
11532
  init_timeback3();
11396
11533
  init_timeback3();
@@ -11575,6 +11712,10 @@ var init_assessment_runtime2 = __esm(() => {
11575
11712
  inProgress: "false",
11576
11713
  scoreStatus: "fully graded"
11577
11714
  };
11715
+ ASSESSMENT_ATTEMPT_SUPERSEDED = {
11716
+ inProgress: "false",
11717
+ scoreStatus: "not submitted"
11718
+ };
11578
11719
  ASSESSMENT_RUNTIME_ERROR_STATUS = {
11579
11720
  NO_ELIGIBLE_TESTS: 404,
11580
11721
  REVIEW_BANK_SHORTAGE: 422,
@@ -89177,73 +89318,6 @@ function stringField2(value) {
89177
89318
  function firstStringField2(...values) {
89178
89319
  return values.map(stringField2).find(Boolean) ?? "";
89179
89320
  }
89180
- function normalizedWhitespace2(value) {
89181
- return value.normalize("NFKC").trim().replace(/\s+/g, " ");
89182
- }
89183
- function normalizedIdentityCase2(value) {
89184
- return value.toLocaleUpperCase("en-US");
89185
- }
89186
- function frameworkAliasKey2(value) {
89187
- return value.normalize("NFKC").toLocaleLowerCase("en-US").replace(/[^a-z0-9]+/g, "");
89188
- }
89189
- function isUuidShaped2(value) {
89190
- return UUID_PATTERN2.test(value.trim());
89191
- }
89192
- function isCommonCoreMathIdentifier2(identifier) {
89193
- const canonical = normalizedIdentityCase2(normalizedWhitespace2(identifier));
89194
- return canonical.startsWith("CCSS.MATH.") || /^(?:K|[1-8])\.(?:CC|OA|NBT|NF|MD|RP|NS|EE|F|G|SP)\./.test(canonical) || /^(?:HS)?(?:N|A|F|G|S)-[A-Z]+\./.test(canonical) || /^MP\.?\d/.test(canonical);
89195
- }
89196
- function isCommonCoreElaIdentifier2(identifier) {
89197
- const canonical = normalizedIdentityCase2(normalizedWhitespace2(identifier));
89198
- return canonical.startsWith("CCSS.ELA-LITERACY.") || /^(?:RL|RI|RF|W|SL|L)\.(?:K|[1-9]|1[0-2])\./.test(canonical) || /^(?:RH|RST|WHST)\.(?:6-8|9-10|11-12)\./.test(canonical) || /^CCRA\.(?:R|W|SL|L)\./.test(canonical);
89199
- }
89200
- function canonicalFramework2(authoredFramework, identifier) {
89201
- const aliasKey = frameworkAliasKey2(authoredFramework);
89202
- if (COMMON_CORE_MATH_FRAMEWORK_ALIASES2.has(aliasKey) || COMMON_CORE_FRAMEWORK_ALIASES2.has(aliasKey) && isCommonCoreMathIdentifier2(identifier)) {
89203
- return "CCSS.Math";
89204
- }
89205
- if (COMMON_CORE_ELA_FRAMEWORK_ALIASES2.has(aliasKey) || COMMON_CORE_FRAMEWORK_ALIASES2.has(aliasKey) && isCommonCoreElaIdentifier2(identifier)) {
89206
- return "CCSS.ELA-Literacy";
89207
- }
89208
- if (COMMON_CORE_FRAMEWORK_ALIASES2.has(aliasKey)) {
89209
- return "CCSS";
89210
- }
89211
- return normalizedIdentityCase2(authoredFramework);
89212
- }
89213
- function canonicalIdentifier2(framework, authoredIdentifier) {
89214
- const identifier = normalizedIdentityCase2(normalizedWhitespace2(authoredIdentifier));
89215
- if (framework === "CCSS.Math") {
89216
- return identifier.replace(/^CCSS\.MATH\.CONTENT\./, "").replace(/^CCSS\.MATH\.PRACTICE\./, "").replace(/^CCSS\.MATH\./, "");
89217
- }
89218
- if (framework === "CCSS.ELA-Literacy") {
89219
- return identifier.replace(/^CCSS\.ELA-LITERACY\./, "");
89220
- }
89221
- return identifier;
89222
- }
89223
- function readableAlignmentIdentifier2(alignment) {
89224
- const identifier = alignment.identifier?.trim() ?? "";
89225
- if (identifier && !isUuidShaped2(identifier)) {
89226
- return identifier;
89227
- }
89228
- const id = alignment.id.trim();
89229
- return id && !isUuidShaped2(id) ? id : "";
89230
- }
89231
- function canonicalAssessmentStandardRef2(standard) {
89232
- const authoredFramework = normalizedWhitespace2(standard.framework);
89233
- const framework = canonicalFramework2(authoredFramework, standard.identifier);
89234
- const identifier = canonicalIdentifier2(framework, standard.identifier);
89235
- return {
89236
- framework,
89237
- identifier
89238
- };
89239
- }
89240
- function assessmentStandardRefKey2(standard) {
89241
- const canonical = canonicalAssessmentStandardRef2(standard);
89242
- return JSON.stringify([
89243
- normalizedIdentityCase2(canonical.framework),
89244
- normalizedIdentityCase2(canonical.identifier)
89245
- ]);
89246
- }
89247
89321
  function dedupeStandards2(standards) {
89248
89322
  const deduped = new Map;
89249
89323
  for (const standard of standards) {
@@ -89334,23 +89408,7 @@ function qtiStandardsFromMetadata(metadata2) {
89334
89408
  standards.push(...qtiAlignmentStandardsFromMetadata2(metadata2));
89335
89409
  return dedupeStandards2(standards);
89336
89410
  }
89337
- function qtiAssessmentStandardRefsFromMetadata2(metadata2) {
89338
- const refs = new Map;
89339
- for (const alignment of qtiAlignmentStandardsFromMetadata2(metadata2)) {
89340
- const identifier = readableAlignmentIdentifier2(alignment);
89341
- if (identifier) {
89342
- const canonical = canonicalAssessmentStandardRef2({
89343
- framework: alignment.source,
89344
- identifier
89345
- });
89346
- if (canonical.framework && canonical.identifier) {
89347
- refs.set(assessmentStandardRefKey2(canonical), canonical);
89348
- }
89349
- }
89350
- }
89351
- return [...refs.values()];
89352
- }
89353
- 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, UUID_PATTERN2, COMMON_CORE_MATH_FRAMEWORK_ALIASES2, COMMON_CORE_ELA_FRAMEWORK_ALIASES2, COMMON_CORE_FRAMEWORK_ALIASES2;
89411
+ 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;
89354
89412
  var init_qti = __esm(() => {
89355
89413
  init_timeback3();
89356
89414
  init_timeback3();
@@ -89600,7 +89658,6 @@ var init_qti = __esm(() => {
89600
89658
  "qti-sum",
89601
89659
  "qti-base-value"
89602
89660
  ]);
89603
- UUID_PATTERN2 = /^(?:urn:uuid:)?\{?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}?$/i;
89604
89661
  COMMON_CORE_MATH_FRAMEWORK_ALIASES2 = new Set([
89605
89662
  "ccssm",
89606
89663
  "ccssmath",
@@ -92006,8 +92063,8 @@ function assertAssessmentHasQuestions(questions) {
92006
92063
  }
92007
92064
  }
92008
92065
  function assertReviewAssessmentHasStandards(standardCounts) {
92009
- if (standardCounts.some((count) => count <= 0)) {
92010
- throw new ValidationError("Every question in a review assessment must have a standards alignment");
92066
+ if (standardCounts.some((count) => count !== 1)) {
92067
+ throw new ValidationError("Every question in a review assessment must have exactly one standards alignment");
92011
92068
  }
92012
92069
  }
92013
92070
  function planAssessmentRemoval(status) {
@@ -92072,6 +92129,41 @@ var init_timeback_assessment_rules_util = __esm(() => {
92072
92129
  });
92073
92130
 
92074
92131
  // ../api-core/src/utils/timeback-assessment-runtime.util.ts
92132
+ function stageAssessmentAttemptSupersession(attempt) {
92133
+ Object.assign(attempt.result, ASSESSMENT_ATTEMPT_SUPERSEDED);
92134
+ Object.assign(attempt.selection, ASSESSMENT_ATTEMPT_SUPERSEDED);
92135
+ }
92136
+ function shouldConfirmAssessmentVersionUnavailable(details) {
92137
+ return typeof details === "object" && details !== null && "expectedRevision" in details && typeof details.expectedRevision === "string" && !("currentRevision" in details);
92138
+ }
92139
+ function buildAssessmentAttemptSupersessionUpdate(result) {
92140
+ if (result.status !== "active" || !isAssessmentAttemptOpen({
92141
+ inProgress: result.inProgress ?? "",
92142
+ scoreStatus: result.scoreStatus
92143
+ })) {
92144
+ return null;
92145
+ }
92146
+ return {
92147
+ status: "active",
92148
+ assessmentLineItem: result.assessmentLineItem,
92149
+ student: result.student,
92150
+ score: result.score ?? 0,
92151
+ scoreDate: result.scoreDate,
92152
+ ...ASSESSMENT_ATTEMPT_SUPERSEDED,
92153
+ metadata: result.metadata ?? {}
92154
+ };
92155
+ }
92156
+ async function resolveAvailableAssessmentAttempt(initialDecision, select3, resume) {
92157
+ let decision = initialDecision;
92158
+ while (decision?.kind === "resume") {
92159
+ const snapshot = await resume(decision);
92160
+ if (snapshot !== null) {
92161
+ return { kind: "resumed", snapshot };
92162
+ }
92163
+ decision = select3();
92164
+ }
92165
+ return { kind: "continue", decision };
92166
+ }
92075
92167
  function buildReviewItemResultMetadata(input) {
92076
92168
  const administration = itemAdministration(input.metadata.itemSubmissions, input.selection.itemIdentifier);
92077
92169
  if (!administration) {
@@ -92516,8 +92608,8 @@ function parsePlayableItem(question) {
92516
92608
  function assessmentContentRevision(test, questions) {
92517
92609
  return sha256Hex(canonicalJson2({
92518
92610
  identifier: test.identifier,
92611
+ title: test.title,
92519
92612
  qtiVersion: test.qtiVersion,
92520
- rawXml: test.rawXml,
92521
92613
  parts: test["qti-test-part"],
92522
92614
  questions: questions.questions.map(({ reference, question }) => ({
92523
92615
  identifier: reference.identifier,
@@ -92675,11 +92767,30 @@ async function hydrateQtiTestQuestions(client2, references) {
92675
92767
  }));
92676
92768
  return { ...references, questions };
92677
92769
  }
92678
- async function loadHydratedQtiTest(client2, identifier) {
92770
+ async function hydrateQtiTestQuestionSelection(client2, references, itemIdentifiers) {
92771
+ const referenceByIdentifier = new Map(references.questions.map((reference) => [reference.reference.identifier, reference]));
92772
+ const selectedReferences = itemIdentifiers.map((itemIdentifier) => {
92773
+ const reference = referenceByIdentifier.get(itemIdentifier);
92774
+ if (!reference) {
92775
+ throw new Error(`QTI assessment ${references.assessmentTest} does not contain item ${itemIdentifier}`);
92776
+ }
92777
+ return reference;
92778
+ });
92779
+ return hydrateQtiTestQuestions(client2, {
92780
+ ...references,
92781
+ totalQuestions: selectedReferences.length,
92782
+ questions: selectedReferences
92783
+ });
92784
+ }
92785
+ async function loadQtiTestReferences(client2, identifier) {
92679
92786
  const [test, references] = await Promise.all([
92680
92787
  client2.qtiApi.assessmentTests.get(identifier),
92681
92788
  client2.qtiApi.assessmentTests.getQuestions(identifier)
92682
92789
  ]);
92790
+ return { test, references };
92791
+ }
92792
+ async function loadHydratedQtiTest(client2, identifier) {
92793
+ const { test, references } = await loadQtiTestReferences(client2, identifier);
92683
92794
  return {
92684
92795
  test,
92685
92796
  questions: await hydrateQtiTestQuestions(client2, references)
@@ -92694,6 +92805,7 @@ class TimebackAssessmentRuntimeService {
92694
92805
  static ASSESSMENT_CACHE_TTL_MS = 60000;
92695
92806
  static EXPORT_CONCURRENCY = 4;
92696
92807
  static ITEM_SUBMISSION_MAX_PREPARATION_ATTEMPTS = 3;
92808
+ static ASSESSMENT_VERSION_CONFIRMATION_DELAY_MS = 25;
92697
92809
  static PROJECTION_BARRIER_MAX_ATTEMPTS = 5;
92698
92810
  static PROJECTION_BARRIER_RETRY_DELAY_MS = 25;
92699
92811
  static SCORING_CONCURRENCY = 4;
@@ -92706,6 +92818,10 @@ class TimebackAssessmentRuntimeService {
92706
92818
  defaultTTL: TimebackAssessmentRuntimeService.ASSESSMENT_CACHE_TTL_MS,
92707
92819
  maxSize: TimebackAssessmentRuntimeService.ASSESSMENT_CACHE_LIMIT
92708
92820
  });
92821
+ reviewSelectionCache = new TimebackCache({
92822
+ defaultTTL: TimebackAssessmentRuntimeService.ASSESSMENT_CACHE_TTL_MS,
92823
+ maxSize: TimebackAssessmentRuntimeService.ASSESSMENT_CACHE_LIMIT
92824
+ });
92709
92825
  constructor(deps) {
92710
92826
  this.deps = deps;
92711
92827
  }
@@ -92724,6 +92840,7 @@ class TimebackAssessmentRuntimeService {
92724
92840
  }
92725
92841
  const masteryStandard = input.purpose === "mastery" ? normalizeMasteryStandard(input.standard) : undefined;
92726
92842
  const requestFingerprint = this.startRequestFingerprint(input);
92843
+ const pendingSupersessions = new Set;
92727
92844
  const candidates = await this.candidateIntegrations(gameId, studentId, input);
92728
92845
  const lockKeys = candidates.rows.map((row) => assessmentRuntimeStartLockKey({
92729
92846
  studentId,
@@ -92746,20 +92863,21 @@ class TimebackAssessmentRuntimeService {
92746
92863
  this.listAttempts(attemptScope)
92747
92864
  ]);
92748
92865
  const attempts = listing.attempts;
92749
- const compatibleAttempts = this.attemptsMatchingRequest(attempts, requestFingerprint);
92750
- let decision = this.requireDecision(selectRuntimeAssessment(tests, [...compatibleAttempts.values()].map((entry) => entry.selection)), input.purpose);
92751
- if (decision.kind === "resume") {
92752
- return this.resumeSnapshot(decision, attempts, context2.integration);
92866
+ const initialResolution = await this.resumeAvailableAttempt(tests, attempts, requestFingerprint, pendingSupersessions, (resumeDecision) => this.resumeSnapshot(resumeDecision, attempts, context2.integration));
92867
+ if (initialResolution.kind === "resumed") {
92868
+ await this.persistPendingSupersessionsBestEffort(pendingSupersessions);
92869
+ return initialResolution.snapshot;
92753
92870
  }
92871
+ let decision = this.requireDecision(initialResolution.decision, input.purpose);
92754
92872
  let assessment = await this.loadAssessment(decision.test.qtiTestIdentifier);
92755
92873
  const refreshedListing = await this.listAttempts(attemptScope);
92756
92874
  const refreshed = refreshedListing.attempts;
92757
- const refreshedCompatible = this.attemptsMatchingRequest(refreshed, requestFingerprint);
92758
- const refreshedDecision = this.requireDecision(selectRuntimeAssessment(tests, [...refreshedCompatible.values()].map((entry) => entry.selection)), input.purpose);
92759
- if (refreshedDecision.kind === "resume") {
92760
- return this.resumeSnapshot(refreshedDecision, refreshed, context2.integration);
92875
+ const refreshedResolution = await this.resumeAvailableAttempt(tests, refreshed, requestFingerprint, pendingSupersessions, (resumeDecision) => this.resumeSnapshot(resumeDecision, refreshed, context2.integration));
92876
+ if (refreshedResolution.kind === "resumed") {
92877
+ await this.persistPendingSupersessionsBestEffort(pendingSupersessions);
92878
+ return refreshedResolution.snapshot;
92761
92879
  }
92762
- decision = refreshedDecision;
92880
+ decision = this.requireDecision(refreshedResolution.decision, input.purpose);
92763
92881
  if (assessment.identifier !== decision.test.qtiTestIdentifier) {
92764
92882
  assessment = await this.loadAssessment(decision.test.qtiTestIdentifier);
92765
92883
  }
@@ -92817,6 +92935,7 @@ class TimebackAssessmentRuntimeService {
92817
92935
  grade: context2.integration.grade
92818
92936
  })
92819
92937
  });
92938
+ await this.persistPendingSupersessionsBestEffort(pendingSupersessions);
92820
92939
  return this.snapshot(result, metadata2, assessment, context2.integration);
92821
92940
  });
92822
92941
  }
@@ -92826,6 +92945,7 @@ class TimebackAssessmentRuntimeService {
92826
92945
  input
92827
92946
  }) {
92828
92947
  const requestFingerprint = this.startRequestFingerprint(input);
92948
+ const pendingSupersessions = new Set;
92829
92949
  const candidates = await this.candidateIntegrations(gameId, studentId, input);
92830
92950
  const lockKeys = candidates.rows.map((row) => assessmentRuntimeStartLockKey({
92831
92951
  studentId,
@@ -92847,22 +92967,23 @@ class TimebackAssessmentRuntimeService {
92847
92967
  this.liveTests(context2.integration.id, input.purpose, db2, undefined, input.diagnosticKey),
92848
92968
  this.listAttempts(attemptScope)
92849
92969
  ]);
92850
- const compatible = this.attemptsMatchingRequest(listing.attempts, requestFingerprint);
92851
- let decision = this.requireDecision(selectRuntimeAssessment(tests, [...compatible.values()].map((entry) => entry.selection)), input.purpose);
92852
- if (decision.kind === "resume") {
92853
- return this.resumeDiagnosticSnapshot(decision, listing.attempts, context2.integration, db2);
92970
+ const initialResolution = await this.resumeAvailableAttempt(tests, listing.attempts, requestFingerprint, pendingSupersessions, (resumeDecision) => this.resumeDiagnosticSnapshot(resumeDecision, listing.attempts, context2.integration, db2));
92971
+ if (initialResolution.kind === "resumed") {
92972
+ await this.persistPendingSupersessionsBestEffort(pendingSupersessions);
92973
+ return initialResolution.snapshot;
92854
92974
  }
92975
+ let decision = this.requireDecision(initialResolution.decision, input.purpose);
92855
92976
  let definition = await this.requireDiagnosticDefinition(decision.test.id, context2.integration.id, db2, true);
92856
92977
  let assessment = await this.loadAssessment(definition.qtiTestIdentifier);
92857
92978
  let routing = await this.initializeHostedDiagnostic(definition, assessment);
92858
92979
  const refreshedListing = await this.listAttempts(attemptScope);
92859
92980
  const refreshed = refreshedListing.attempts;
92860
- const refreshedCompatible = this.attemptsMatchingRequest(refreshed, requestFingerprint);
92861
- const refreshedDecision = this.requireDecision(selectRuntimeAssessment(tests, [...refreshedCompatible.values()].map((entry) => entry.selection)), input.purpose);
92862
- if (refreshedDecision.kind === "resume") {
92863
- return this.resumeDiagnosticSnapshot(refreshedDecision, refreshed, context2.integration, db2);
92981
+ const refreshedResolution = await this.resumeAvailableAttempt(tests, refreshed, requestFingerprint, pendingSupersessions, (resumeDecision) => this.resumeDiagnosticSnapshot(resumeDecision, refreshed, context2.integration, db2));
92982
+ if (refreshedResolution.kind === "resumed") {
92983
+ await this.persistPendingSupersessionsBestEffort(pendingSupersessions);
92984
+ return refreshedResolution.snapshot;
92864
92985
  }
92865
- decision = refreshedDecision;
92986
+ decision = this.requireDecision(refreshedResolution.decision, input.purpose);
92866
92987
  if (definition.id !== decision.test.id) {
92867
92988
  definition = await this.requireDiagnosticDefinition(decision.test.id, context2.integration.id, db2, true);
92868
92989
  assessment = await this.loadAssessment(definition.qtiTestIdentifier);
@@ -92931,6 +93052,7 @@ class TimebackAssessmentRuntimeService {
92931
93052
  grade: context2.integration.grade
92932
93053
  })
92933
93054
  });
93055
+ await this.persistPendingSupersessionsBestEffort(pendingSupersessions);
92934
93056
  return this.diagnosticSnapshot(result, metadata2, assessment, context2.integration, routing.state);
92935
93057
  });
92936
93058
  }
@@ -92939,7 +93061,9 @@ class TimebackAssessmentRuntimeService {
92939
93061
  studentId,
92940
93062
  input
92941
93063
  }) {
93064
+ const preparationStartedAt = Date.now();
92942
93065
  const requestFingerprint = reviewRequestFingerprint(input, DEFAULT_REVIEW_SELECTION_POLICY);
93066
+ const pendingSupersessions = new Set;
92943
93067
  const normalizedRequest = normalizeReviewRequest(input);
92944
93068
  const candidates = await this.candidateIntegrations(gameId, studentId, input);
92945
93069
  const lockKeys = candidates.rows.map((row) => assessmentRuntimeStartLockKey({
@@ -92963,38 +93087,44 @@ class TimebackAssessmentRuntimeService {
92963
93087
  this.listAttempts(attemptScope)
92964
93088
  ]);
92965
93089
  const attempts = listing.attempts;
92966
- const compatible = this.attemptsMatchingRequest(attempts, requestFingerprint);
92967
- let decision = selectRuntimeAssessment(tests, [...compatible.values()].map((entry) => entry.selection));
92968
- if (decision?.kind === "resume") {
92969
- return this.resumeReviewSnapshot(decision, compatible, listing.reviewChildren, context2);
93090
+ const initialResolution = await this.resumeAvailableAttempt(tests, attempts, requestFingerprint, pendingSupersessions, (resumeDecision) => this.resumeReviewSnapshot(resumeDecision, attempts, listing.reviewChildren, context2));
93091
+ if (initialResolution.kind === "resumed") {
93092
+ await this.persistPendingSupersessionsBestEffort(pendingSupersessions);
93093
+ return initialResolution.snapshot;
92970
93094
  }
93095
+ let decision = initialResolution.decision;
92971
93096
  if (tests.length === 0 || !decision) {
92972
93097
  throw AssessmentRuntimeError.from(assessmentNoEligibleTests(input.purpose));
92973
93098
  }
92974
93099
  if (tests.length > 1) {
92975
93100
  throw new ValidationError("Standards review requires exactly one live review-bank assessment.");
92976
93101
  }
92977
- let source = await this.loadReviewBank(decision.test.qtiTestIdentifier);
93102
+ const manifestStartedAt = Date.now();
93103
+ let catalog = await this.loadReviewBankCatalog(decision.test.qtiTestIdentifier);
93104
+ this.recordReviewPreparationPhase("manifest_load", manifestStartedAt, {
93105
+ candidateCount: catalog.bank.items.length
93106
+ });
92978
93107
  const refreshedListing = await this.listAttempts(attemptScope);
92979
93108
  const refreshed = refreshedListing.attempts;
92980
- const refreshedCompatible = this.attemptsMatchingRequest(refreshed, requestFingerprint);
92981
- const refreshedDecision = selectRuntimeAssessment(tests, [...refreshedCompatible.values()].map((entry) => entry.selection));
92982
- if (refreshedDecision?.kind === "resume") {
92983
- return this.resumeReviewSnapshot(refreshedDecision, refreshedCompatible, refreshedListing.reviewChildren, context2);
93109
+ const refreshedResolution = await this.resumeAvailableAttempt(tests, refreshed, requestFingerprint, pendingSupersessions, (resumeDecision) => this.resumeReviewSnapshot(resumeDecision, refreshed, refreshedListing.reviewChildren, context2));
93110
+ if (refreshedResolution.kind === "resumed") {
93111
+ await this.persistPendingSupersessionsBestEffort(pendingSupersessions);
93112
+ return refreshedResolution.snapshot;
92984
93113
  }
93114
+ const refreshedDecision = refreshedResolution.decision;
92985
93115
  if (!refreshedDecision || refreshedDecision.kind !== "start") {
92986
93116
  throw AssessmentRuntimeError.from(assessmentNoEligibleTests(input.purpose));
92987
93117
  }
92988
93118
  decision = refreshedDecision;
92989
- if (source.assessment.identifier !== decision.test.qtiTestIdentifier) {
92990
- source = await this.loadReviewBank(decision.test.qtiTestIdentifier);
93119
+ if (catalog.test.identifier !== decision.test.qtiTestIdentifier) {
93120
+ catalog = await this.loadReviewBankCatalog(decision.test.qtiTestIdentifier);
92991
93121
  }
92992
93122
  const exposures = this.reviewExposures(refreshedListing.reviewChildren.values(), {
92993
- bankRevision: source.bank.bankRevision
93123
+ bankRevision: catalog.bank.bankRevision
92994
93124
  });
92995
93125
  const selected = selectReviewItems({
92996
93126
  request: input,
92997
- bank: source.bank,
93127
+ bank: catalog.bank,
92998
93128
  exposures
92999
93129
  });
93000
93130
  if (selected.selections.length === 0) {
@@ -93003,7 +93133,13 @@ class TimebackAssessmentRuntimeService {
93003
93133
  }
93004
93134
  throw AssessmentRuntimeError.from(assessmentReviewBankShortage(selected.fulfillment));
93005
93135
  }
93136
+ const hydrationStartedAt = Date.now();
93137
+ const source = await this.hydrateReviewBankSelection(catalog, selected.selections.map((selection) => selection.itemIdentifier));
93138
+ this.recordReviewPreparationPhase("selected_hydration", hydrationStartedAt, {
93139
+ selectedCount: selected.selections.length
93140
+ });
93006
93141
  const assessment = projectReviewAssessment(source.assessment, source.bank, selected.selections);
93142
+ const provisioningStartedAt = Date.now();
93007
93143
  const parentLineItemId = await this.ensureReviewBankLineItem({
93008
93144
  integration: context2.integration,
93009
93145
  activityId: input.activityId,
@@ -93066,6 +93202,12 @@ class TimebackAssessmentRuntimeService {
93066
93202
  existingChildResults: refreshedListing.reviewChildren,
93067
93203
  lookupMissingResults: false
93068
93204
  });
93205
+ await this.persistPendingSupersessionsBestEffort(pendingSupersessions);
93206
+ this.recordReviewPreparationPhase("provisioning", provisioningStartedAt);
93207
+ this.recordReviewPreparationPhase("total", preparationStartedAt, {
93208
+ candidateCount: catalog.bank.items.length,
93209
+ selectedCount: selected.selections.length
93210
+ });
93069
93211
  return this.snapshot(result, metadata2, assessment, context2.integration);
93070
93212
  });
93071
93213
  }
@@ -93419,7 +93561,7 @@ class TimebackAssessmentRuntimeService {
93419
93561
  preparationAttempts += 1;
93420
93562
  preview = await this.scoreDiagnosticItemSubmission(params, input);
93421
93563
  } else {
93422
- await this.projectDiagnosticItemResponse(committed.projection);
93564
+ await this.projectDiagnosticItemResponse(committed.projection, preview?.assessment);
93423
93565
  return committed.response;
93424
93566
  }
93425
93567
  }
@@ -93833,7 +93975,7 @@ class TimebackAssessmentRuntimeService {
93833
93975
  where: and(eq(gameTimebackAssessmentTests.id, test.id), eq(gameTimebackAssessmentTests.integrationId, integration.id))
93834
93976
  }) : undefined;
93835
93977
  const diagnostic = definition?.diagnosticKey ? await this.initializeHostedDiagnostic(definition, loaded.assessment) : null;
93836
- const reviewBank = purpose === "review" ? await buildReviewBankIndex(loaded.assessment, loaded.questions) : null;
93978
+ const reviewBank = purpose === "review" ? await buildReviewBankIndex(loaded.assessment, loaded.questions, { customOnly: true }) : null;
93837
93979
  return {
93838
93980
  ...test,
93839
93981
  live: true,
@@ -93896,7 +94038,13 @@ class TimebackAssessmentRuntimeService {
93896
94038
  const enrollmentByCourse = new Map(enrollments.map((enrollment) => [enrollment.course.id, enrollment]));
93897
94039
  const candidateRows = integrations.filter((integration) => enrollmentByCourse.has(integration.courseId));
93898
94040
  const liveTestRows = candidateRows.length === 0 ? [] : await db2.query.gameTimebackAssessmentTests.findMany({
93899
- where: and(inArray(gameTimebackAssessmentTests.integrationId, candidateRows.map((row) => row.id)), eq(gameTimebackAssessmentTests.purpose, input.purpose), eq(gameTimebackAssessmentTests.status, "live"))
94041
+ where: and(inArray(gameTimebackAssessmentTests.integrationId, candidateRows.map((row) => row.id)), eq(gameTimebackAssessmentTests.purpose, input.purpose), eq(gameTimebackAssessmentTests.status, "live")),
94042
+ columns: {
94043
+ integrationId: true,
94044
+ standardFramework: true,
94045
+ standardIdentifier: true,
94046
+ diagnosticKey: true
94047
+ }
93900
94048
  });
93901
94049
  const matchesMasteryStandard = input.purpose === "mastery" ? masteryStandardMatcher(input.standard) : undefined;
93902
94050
  const integrationIdsWithLiveTests = new Set(liveTestRows.filter((row) => (!matchesMasteryStandard || matchesMasteryStandard(assessmentStandardForRow(row))) && (input.purpose !== "diagnostic" || row.diagnosticKey === input.diagnosticKey)).map((row) => row.integrationId));
@@ -94089,6 +94237,75 @@ class TimebackAssessmentRuntimeService {
94089
94237
  });
94090
94238
  return this.diagnosticSnapshot(resumed.result, resumed.metadata, assessment, integration, routing.state);
94091
94239
  }
94240
+ async resumeAvailableAttempt(tests, attempts, requestFingerprint, pendingSupersessions, resume) {
94241
+ const select3 = () => selectRuntimeAssessment(tests, [...this.attemptsMatchingRequest(attempts, requestFingerprint).values()].map((entry) => entry.selection));
94242
+ return resolveAvailableAssessmentAttempt(select3(), select3, (decision) => this.resumeSnapshotOrStageSupersession(decision, attempts, pendingSupersessions, () => resume(decision)));
94243
+ }
94244
+ async resumeSnapshotOrStageSupersession(decision, attempts, pendingSupersessions, resume) {
94245
+ try {
94246
+ return await resume();
94247
+ } catch (error88) {
94248
+ if (!(error88 instanceof AssessmentRuntimeError) || error88.runtimeCode !== "SELECTED_TEST_VERSION_UNAVAILABLE") {
94249
+ throw error88;
94250
+ }
94251
+ if (shouldConfirmAssessmentVersionUnavailable(error88.details)) {
94252
+ await sleep(TimebackAssessmentRuntimeService.ASSESSMENT_VERSION_CONFIRMATION_DELAY_MS);
94253
+ try {
94254
+ return await resume();
94255
+ } catch (confirmationError) {
94256
+ if (!(confirmationError instanceof AssessmentRuntimeError) || confirmationError.runtimeCode !== "SELECTED_TEST_VERSION_UNAVAILABLE") {
94257
+ throw confirmationError;
94258
+ }
94259
+ }
94260
+ }
94261
+ const staged = attempts.get(decision.attempt.attemptId);
94262
+ pendingSupersessions.add(staged.result.sourcedId);
94263
+ stageAssessmentAttemptSupersession(staged);
94264
+ addEvent("assessment.attempt_supersession_staged", {
94265
+ "app.assessment.attempt_id": decision.attempt.attemptId,
94266
+ "app.assessment.reason": "selected_test_version_unavailable"
94267
+ });
94268
+ return null;
94269
+ }
94270
+ }
94271
+ async persistPendingSupersessions(attemptIds) {
94272
+ for (const attemptId of [...attemptIds].toSorted()) {
94273
+ await this.deps.assessmentRuntimeLock(this.deps.db, assessmentRuntimeAttemptLockKey(attemptId), async () => {
94274
+ let result;
94275
+ try {
94276
+ result = await this.requireClient().api.oneroster.assessmentResults.get(attemptId);
94277
+ } catch (error88) {
94278
+ if (isApiError(error88) && error88.statusCode === 404) {
94279
+ return;
94280
+ }
94281
+ throw error88;
94282
+ }
94283
+ const update2 = buildAssessmentAttemptSupersessionUpdate(result);
94284
+ if (!update2) {
94285
+ return;
94286
+ }
94287
+ await this.requireClient().api.oneroster.assessmentResults.upsert(attemptId, update2);
94288
+ addEvent("assessment.attempt_superseded", {
94289
+ "app.assessment.attempt_id": attemptId,
94290
+ "app.assessment.reason": "selected_test_version_unavailable"
94291
+ });
94292
+ });
94293
+ }
94294
+ }
94295
+ async persistPendingSupersessionsBestEffort(attemptIds) {
94296
+ if (attemptIds.size === 0) {
94297
+ return;
94298
+ }
94299
+ try {
94300
+ await this.persistPendingSupersessions(attemptIds);
94301
+ } catch (error88) {
94302
+ addEvent("assessment.attempt_supersession_persist_failed", {
94303
+ "app.assessment.attempt_ids": [...attemptIds].toSorted().join(","),
94304
+ "exception.type": errorType(error88),
94305
+ "app.error.message": errorMessage(error88)
94306
+ });
94307
+ }
94308
+ }
94092
94309
  async resumeReviewSnapshot(decision, attempts, existingChildResults, context2) {
94093
94310
  if (decision.additionalResumableAttemptIds.length > 0) {
94094
94311
  addEvent("assessment.multiple_resumable_attempts", {
@@ -94100,7 +94317,8 @@ class TimebackAssessmentRuntimeService {
94100
94317
  if (resumed.metadata.purpose !== "review") {
94101
94318
  throw new Error("A standards-review request selected a non-review attempt");
94102
94319
  }
94103
- const currentSource = await this.loadReviewBank(resumed.metadata.selectedTest.identifier, resumed.metadata.selectedTest.contentRevision);
94320
+ const catalog = await this.loadReviewBankCatalog(resumed.metadata.selectedTest.identifier, resumed.metadata.selectedTest.contentRevision);
94321
+ const currentSource = await this.hydrateReviewBankSelection(catalog, resumed.metadata.review.selections.map((selection) => selection.itemIdentifier));
94104
94322
  const source = this.pinnedReviewBankSource(currentSource, resumed.metadata);
94105
94323
  const assessment = projectReviewAssessment(source.assessment, source.bank, resumed.metadata.review.selections);
94106
94324
  await this.ensureReviewChildResults({
@@ -94114,20 +94332,68 @@ class TimebackAssessmentRuntimeService {
94114
94332
  });
94115
94333
  return this.snapshot(resumed.result, resumed.metadata, assessment, context2.integration);
94116
94334
  }
94117
- async loadReviewBank(identifier, expectedSourceRevision) {
94335
+ recordReviewPreparationPhase(phase, startedAt, counts = {}) {
94336
+ addEvent("assessment.review_preparation_phase", {
94337
+ "app.assessment.review_phase": phase,
94338
+ "app.assessment.duration_ms": Date.now() - startedAt,
94339
+ ...counts.candidateCount === undefined ? {} : { "app.assessment.candidate_count": counts.candidateCount },
94340
+ ...counts.selectedCount === undefined ? {} : { "app.assessment.selected_count": counts.selectedCount }
94341
+ });
94342
+ }
94343
+ async loadReviewBankCatalog(identifier, expectedSourceRevision) {
94118
94344
  if (expectedSourceRevision) {
94119
94345
  const cached3 = this.reviewBankCache.get(`${identifier}\x00${expectedSourceRevision}`);
94120
94346
  if (cached3) {
94121
94347
  return cached3;
94122
94348
  }
94123
94349
  }
94124
- const loaded = await this.loadAssessmentSource(identifier, expectedSourceRevision);
94125
- const source = {
94126
- assessment: loaded.assessment,
94127
- bank: await buildReviewBankIndex(loaded.assessment, loaded.questions)
94128
- };
94129
- this.reviewBankCache.set(`${identifier}\x00${source.assessment.contentRevision}`, source);
94130
- return source;
94350
+ let loaded;
94351
+ try {
94352
+ loaded = await loadQtiTestReferences(this.requireClient(), identifier);
94353
+ } catch (error88) {
94354
+ if (expectedSourceRevision && isApiError(error88) && error88.statusCode === 404) {
94355
+ throw new AssessmentRuntimeError("SELECTED_TEST_VERSION_UNAVAILABLE", `The selected assessment version for ${identifier} is no longer available.`, { identifier, expectedRevision: expectedSourceRevision });
94356
+ }
94357
+ throw error88;
94358
+ }
94359
+ let bank;
94360
+ try {
94361
+ bank = await reviewBankIndexFromManifest(loaded.test.metadata, {
94362
+ bankIdentifier: identifier,
94363
+ membershipItemIdentifiers: loaded.references.questions.map((question) => question.reference.identifier)
94364
+ });
94365
+ } catch (error88) {
94366
+ throw new ServiceUnavailableError(`Review bank ${identifier} has stale or invalid routing metadata. Use Update Mapping in assessment authoring.`, { reason: errorMessage(error88) });
94367
+ }
94368
+ if (!bank) {
94369
+ throw new ServiceUnavailableError(`Review bank ${identifier} needs its authoring mapping updated before it can serve review questions.`);
94370
+ }
94371
+ if (expectedSourceRevision && bank.sourceContentRevision !== expectedSourceRevision) {
94372
+ throw new AssessmentRuntimeError("SELECTED_TEST_VERSION_UNAVAILABLE", `The selected assessment version for ${identifier} is no longer available.`, {
94373
+ identifier,
94374
+ expectedRevision: expectedSourceRevision,
94375
+ currentRevision: bank.sourceContentRevision
94376
+ });
94377
+ }
94378
+ const catalog = { ...loaded, bank };
94379
+ this.reviewBankCache.set(`${identifier}\x00${bank.sourceContentRevision}`, catalog);
94380
+ return catalog;
94381
+ }
94382
+ async hydrateReviewBankSelection(catalog, itemIdentifiers) {
94383
+ const cacheKey2 = [
94384
+ catalog.test.identifier,
94385
+ catalog.bank.sourceContentRevision,
94386
+ ...itemIdentifiers
94387
+ ].join("\x00");
94388
+ const cached3 = this.reviewSelectionCache.get(cacheKey2);
94389
+ if (cached3) {
94390
+ return { assessment: cached3, bank: catalog.bank };
94391
+ }
94392
+ const questions = await hydrateQtiTestQuestionSelection(this.requireClient(), catalog.references, itemIdentifiers);
94393
+ const assessment = await buildPlayableAssessment(catalog.test, questions);
94394
+ assessment.contentRevision = catalog.bank.sourceContentRevision;
94395
+ this.reviewSelectionCache.set(cacheKey2, assessment);
94396
+ return { assessment, bank: catalog.bank };
94131
94397
  }
94132
94398
  pinnedReviewBankSource(source, metadata2) {
94133
94399
  return {
@@ -94158,7 +94424,8 @@ class TimebackAssessmentRuntimeService {
94158
94424
  if (metadata2.purpose !== "review") {
94159
94425
  return this.loadAssessment(metadata2.selectedTest.identifier, metadata2.selectedTest.contentRevision);
94160
94426
  }
94161
- const currentSource = await this.loadReviewBank(metadata2.selectedTest.identifier, metadata2.selectedTest.contentRevision);
94427
+ const catalog = await this.loadReviewBankCatalog(metadata2.selectedTest.identifier, metadata2.selectedTest.contentRevision);
94428
+ const currentSource = await this.hydrateReviewBankSelection(catalog, metadata2.review.selections.map((selection) => selection.itemIdentifier));
94162
94429
  const source = this.pinnedReviewBankSource(currentSource, metadata2);
94163
94430
  return projectReviewAssessment(source.assessment, source.bank, metadata2.review.selections);
94164
94431
  }
@@ -94781,6 +95048,14 @@ class TimebackAssessmentRuntimeService {
94781
95048
  return AssessmentRuntimeError.from(assessmentAttemptUnauthorized(attemptId));
94782
95049
  }
94783
95050
  async snapshotForResult(result, metadata2, integration) {
95051
+ if (isAssessmentAttemptSuperseded({
95052
+ inProgress: result.inProgress ?? "",
95053
+ scoreStatus: result.scoreStatus
95054
+ })) {
95055
+ throw new GoneError("This assessment attempt has been superseded.", {
95056
+ attemptId: result.sourcedId
95057
+ });
95058
+ }
94784
95059
  const assessment = await this.loadAttemptAssessment(metadata2);
94785
95060
  if (this.isPlatformRoutedDiagnosticMetadata(metadata2)) {
94786
95061
  const routing = await this.loadAttemptDiagnosticRouting(metadata2, assessment);
@@ -96129,6 +96404,62 @@ function buildQtiLibraryListPlan(params) {
96129
96404
  }
96130
96405
  var PLAYCADEMY_QTI_SOURCE_PREFIX = "playcademy-test-", PLAYCADEMY_QTI_SOURCE_UPPER_BOUND = "playcademy-test.";
96131
96406
 
96407
+ // ../api-core/src/utils/timeback-review-mapping.util.ts
96408
+ function reviewMappingIssueSummary(label, identifiers) {
96409
+ if (identifiers.length === 0) {
96410
+ return null;
96411
+ }
96412
+ const displayed = identifiers.slice(0, 10).join(", ");
96413
+ const remainder = identifiers.length > 10 ? `, and ${identifiers.length - 10} more` : "";
96414
+ return `${label} (${identifiers.length}): ${displayed}${remainder}`;
96415
+ }
96416
+ async function prepareReviewMappingUpdate(test, questions) {
96417
+ const contentRevision = await assessmentContentRevision(test, questions);
96418
+ const assessment = {
96419
+ identifier: test.identifier,
96420
+ contractVersion: 1,
96421
+ contentRevision,
96422
+ title: test.title,
96423
+ items: questions.questions.map(({ question }) => ({
96424
+ identifier: question.identifier,
96425
+ title: question.title,
96426
+ prompt: "",
96427
+ maxScore: 1,
96428
+ interactions: []
96429
+ }))
96430
+ };
96431
+ const bank = await buildReviewBankIndex(assessment, questions, { customOnly: true });
96432
+ const issues = reviewBankMappingIssues(bank);
96433
+ if (issues.missingOrInvalidItemIdentifiers.length > 0 || issues.multipleStandardItemIdentifiers.length > 0) {
96434
+ const details = [
96435
+ reviewMappingIssueSummary("missing or invalid associations", issues.missingOrInvalidItemIdentifiers),
96436
+ reviewMappingIssueSummary("multiple associations", issues.multipleStandardItemIdentifiers)
96437
+ ].filter((detail) => detail !== null);
96438
+ throw new ValidationError(`Every review question must have exactly one valid standard/node association. ${details.join("; ")}.`);
96439
+ }
96440
+ const manifest = await buildReviewBankManifest(bank);
96441
+ return {
96442
+ update: {
96443
+ ...buildQtiTestUpdateInput(test, test.title),
96444
+ metadata: {
96445
+ ...test.metadata,
96446
+ [PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY]: manifest
96447
+ }
96448
+ },
96449
+ summary: {
96450
+ itemCount: bank.items.length,
96451
+ standardCount: Object.keys(manifest.itemsByStandard).length,
96452
+ sourceFingerprint: manifest.sourceFingerprint
96453
+ }
96454
+ };
96455
+ }
96456
+ var init_timeback_review_mapping_util = __esm(() => {
96457
+ init_assessment_runtime2();
96458
+ init_errors2();
96459
+ init_timeback_assessment_runtime_util();
96460
+ init_timeback_qti_authoring_util();
96461
+ });
96462
+
96132
96463
  // ../api-core/src/services/timeback-assessments.service.ts
96133
96464
  function databaseConstraintName(error88) {
96134
96465
  if (typeof error88 !== "object" || error88 === null) {
@@ -96211,7 +96542,8 @@ class TimebackAssessmentsService {
96211
96542
  ownerGameSlug: ownership.gameSlug,
96212
96543
  integrationId,
96213
96544
  subject: integration.subject,
96214
- grade: String(integration.grade)
96545
+ grade: String(integration.grade),
96546
+ [PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY]: emptyReviewBankManifest(input.qtiTestIdentifier)
96215
96547
  }
96216
96548
  });
96217
96549
  try {
@@ -96280,7 +96612,9 @@ class TimebackAssessmentsService {
96280
96612
  assertAssessmentHasQuestions(loaded.questions.questions);
96281
96613
  assertPlayableAssessmentImportQuestions(manifest.targetStatus, loaded.questions.questions.map(({ question }) => question));
96282
96614
  if (entry.purpose === "review") {
96283
- assertReviewAssessmentHasStandards(loaded.questions.questions.map(({ question }) => qtiAssessmentStandardRefsFromMetadata2(question.metadata).filter((standard) => canonicalReviewStandardRef(standard) !== null).length));
96615
+ assertReviewAssessmentHasStandards(loaded.questions.questions.map(({ question }) => reviewRoutingStandardRefsFromMetadata(question.metadata, {
96616
+ customOnly: true
96617
+ }).length));
96284
96618
  }
96285
96619
  return { entry, test: loaded.test };
96286
96620
  } catch (error88) {
@@ -96432,6 +96766,10 @@ class TimebackAssessmentsService {
96432
96766
  if (input.status !== undefined) {
96433
96767
  validateAssessmentStatusTransition(row.status, input.status);
96434
96768
  }
96769
+ if (!publishing && !activatingReview && nextPurpose === "review" && row.purpose !== "review") {
96770
+ const ownership = await this.requireQtiTestOwnershipContext(integrationId, tx);
96771
+ await this.rebuildReviewMapping(this.requireClient(), qtiTestIdentifier, ownership.gameSlug);
96772
+ }
96435
96773
  if (input.title !== undefined) {
96436
96774
  assertDraftAssessment(row);
96437
96775
  assertAllAssessmentAssociationsDraft(associations);
@@ -96450,7 +96788,8 @@ class TimebackAssessmentsService {
96450
96788
  updates.diagnosticKey = nextDiagnostic.diagnosticKey;
96451
96789
  updates.diagnosticRoutingManifest = nextDiagnostic.routingManifest;
96452
96790
  } else {
96453
- await this.validateAssessmentHasQuestions(qtiTestIdentifier, nextPurpose === "review" && nextStatus === "live");
96791
+ const reviewOwnership = nextPurpose === "review" && nextStatus === "live" ? await this.requireQtiTestOwnershipContext(integrationId, tx) : undefined;
96792
+ await this.validateAssessmentHasQuestions(qtiTestIdentifier, reviewOwnership?.gameSlug);
96454
96793
  }
96455
96794
  }
96456
96795
  let updated = row;
@@ -96545,6 +96884,19 @@ class TimebackAssessmentsService {
96545
96884
  });
96546
96885
  return { ...result, questions };
96547
96886
  }
96887
+ async updateReviewMapping(integrationId, qtiTestIdentifier) {
96888
+ return this.withLockedAssessmentAssociations(integrationId, qtiTestIdentifier, async (row, tx, associations) => {
96889
+ if (!associations.some((association) => association.purpose === "review")) {
96890
+ throw new ValidationError("Review mapping is available only for standards-review assessments.");
96891
+ }
96892
+ const client2 = this.requireClient();
96893
+ const ownership = await this.requireQtiTestOwnershipContext(integrationId, tx);
96894
+ const result = await this.rebuildReviewMapping(client2, qtiTestIdentifier, ownership.gameSlug);
96895
+ setAttribute("app.assessment.operation", "update_review_mapping");
96896
+ setAttribute("app.assessment.review_mapping_item_count", result.itemCount);
96897
+ return result;
96898
+ });
96899
+ }
96548
96900
  async listQuestionLibrary(integrationId, params) {
96549
96901
  const client2 = this.requireClient();
96550
96902
  await this.requireIntegration(integrationId);
@@ -96585,7 +96937,8 @@ class TimebackAssessmentsService {
96585
96937
  integrationId,
96586
96938
  subject: integration.subject,
96587
96939
  grade: String(integration.grade),
96588
- copiedFromTestIdentifier: sourceTestIdentifier
96940
+ copiedFromTestIdentifier: sourceTestIdentifier,
96941
+ [PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY]: emptyReviewBankManifest(targetTestIdentifier)
96589
96942
  }, itemPlan);
96590
96943
  const attemptedItemIdentifiers = [];
96591
96944
  let testCreationAttempted = false;
@@ -96973,16 +97326,20 @@ class TimebackAssessmentsService {
96973
97326
  const plan = buildQtiLibraryListPlan(params);
96974
97327
  return list(plan.params);
96975
97328
  }
96976
- async validateAssessmentHasQuestions(qtiTestIdentifier, requireReviewStandards = false) {
97329
+ async validateAssessmentHasQuestions(qtiTestIdentifier, reviewGameSlug) {
96977
97330
  const client2 = this.requireClient();
96978
- const result = await client2.qtiApi.assessmentTests.getQuestions(qtiTestIdentifier);
97331
+ const [test, result] = await Promise.all([
97332
+ client2.qtiApi.assessmentTests.get(qtiTestIdentifier),
97333
+ client2.qtiApi.assessmentTests.getQuestions(qtiTestIdentifier)
97334
+ ]);
96979
97335
  assertAssessmentHasQuestions(result.questions);
96980
97336
  const hydrated = await hydrateQtiTestQuestions(client2, result);
96981
97337
  for (const { question } of hydrated.questions) {
96982
97338
  assertPlayableQtiQuestion(question);
96983
97339
  }
96984
- if (requireReviewStandards) {
96985
- assertReviewAssessmentHasStandards(hydrated.questions.map(({ question }) => qtiAssessmentStandardRefsFromMetadata2(question.metadata).filter((standard) => canonicalReviewStandardRef(standard) !== null).length));
97340
+ if (reviewGameSlug) {
97341
+ assertQtiTestOwnedByGame(test, reviewGameSlug);
97342
+ await this.writeReviewMapping(client2, test, hydrated);
96986
97343
  }
96987
97344
  }
96988
97345
  async prepareDiagnosticAssociationChanges(row, nextPurpose, requested) {
@@ -97047,6 +97404,19 @@ class TimebackAssessmentsService {
97047
97404
  routingManifest: validation.manifest
97048
97405
  };
97049
97406
  }
97407
+ async rebuildReviewMapping(client2, qtiTestIdentifier, gameSlug) {
97408
+ const [test, references] = await Promise.all([
97409
+ client2.qtiApi.assessmentTests.get(qtiTestIdentifier),
97410
+ client2.qtiApi.assessmentTests.getQuestions(qtiTestIdentifier)
97411
+ ]);
97412
+ assertQtiTestOwnedByGame(test, gameSlug);
97413
+ return this.writeReviewMapping(client2, test, await hydrateQtiTestQuestions(client2, references));
97414
+ }
97415
+ async writeReviewMapping(client2, test, questions) {
97416
+ const prepared = await prepareReviewMappingUpdate(test, questions);
97417
+ await client2.qtiApi.assessmentTests.update(test.identifier, prepared.update);
97418
+ return prepared.summary;
97419
+ }
97050
97420
  }
97051
97421
  var init_timeback_assessments_service = __esm(async () => {
97052
97422
  init_drizzle_orm();
@@ -97054,7 +97424,6 @@ var init_timeback_assessments_service = __esm(async () => {
97054
97424
  init_tables_index();
97055
97425
  init_spans();
97056
97426
  init_assessment_runtime2();
97057
- init_qti();
97058
97427
  init_timeback3();
97059
97428
  init_errors2();
97060
97429
  init_timeback_assessment_import_util();
@@ -97062,6 +97431,7 @@ var init_timeback_assessments_service = __esm(async () => {
97062
97431
  init_timeback_assessment_runtime_util();
97063
97432
  init_timeback_qti_authoring_util();
97064
97433
  init_timeback_qti_hydration_util();
97434
+ init_timeback_review_mapping_util();
97065
97435
  await init_errors8();
97066
97436
  });
97067
97437
 
@@ -157039,7 +157409,7 @@ function parseQtiLibraryParams(searchParams) {
157039
157409
  limit
157040
157410
  };
157041
157411
  }
157042
- 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, 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, reorderAssessments, reorderQuestions, removeAssessment, listQuestions, listQuestionLibrary, listTestLibrary, copyAssessment, createQuestion, updateQuestion, removeQuestion, timeback2;
157412
+ 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, 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, updateReviewMapping, reorderAssessments, reorderQuestions, removeAssessment, listQuestions, listQuestionLibrary, listTestLibrary, copyAssessment, createQuestion, updateQuestion, removeQuestion, timeback2;
157043
157413
  var init_timeback_controller = __esm(() => {
157044
157414
  init_esm();
157045
157415
  init_src();
@@ -157713,6 +158083,14 @@ var init_timeback_controller = __esm(() => {
157713
158083
  const body2 = await parseRequestBody(ctx.request, UpdateAssessmentRequestSchema);
157714
158084
  return ctx.services.timebackAssessments.updateAssessment(integrationId, testIdentifier, body2);
157715
158085
  });
158086
+ updateReviewMapping = requireDeveloper(async (ctx) => {
158087
+ const { gameId, courseId, testIdentifier } = ctx.params;
158088
+ if (!gameId || !courseId || !testIdentifier) {
158089
+ throw ApiError.badRequest("Missing gameId, courseId, or testIdentifier parameter");
158090
+ }
158091
+ const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
158092
+ return ctx.services.timebackAssessments.updateReviewMapping(integrationId, testIdentifier);
158093
+ });
157716
158094
  reorderAssessments = requireDeveloper(async (ctx) => {
157717
158095
  const { gameId, courseId } = ctx.params;
157718
158096
  if (!gameId || !courseId) {
@@ -157855,6 +158233,7 @@ var init_timeback_controller = __esm(() => {
157855
158233
  createAssessment,
157856
158234
  attachExistingAssessments,
157857
158235
  updateAssessment,
158236
+ updateReviewMapping,
157858
158237
  reorderAssessments,
157859
158238
  removeAssessment,
157860
158239
  reorderQuestions,