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