@playcademy/vite-plugin 1.2.1-beta.24 → 1.2.1-beta.25

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (2) hide show
  1. package/dist/index.js +563 -183
  2. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -24315,7 +24315,7 @@ import path2 from "node:path";
24315
24315
  // package.json
24316
24316
  var package_default = {
24317
24317
  name: "@playcademy/vite-plugin",
24318
- version: "1.2.1-beta.24",
24318
+ version: "1.2.1-beta.25",
24319
24319
  type: "module",
24320
24320
  exports: {
24321
24321
  ".": {
@@ -25921,7 +25921,7 @@ var package_default2;
25921
25921
  var init_package = __esm(() => {
25922
25922
  package_default2 = {
25923
25923
  name: "@playcademy/sandbox",
25924
- version: "0.7.1-beta.22",
25924
+ version: "0.7.1-beta.23",
25925
25925
  description: "Local development server for Playcademy game development",
25926
25926
  type: "module",
25927
25927
  exports: {
@@ -34376,6 +34376,9 @@ function isAssessmentAttemptOpen(attempt) {
34376
34376
  function isAssessmentAttemptCompleted(attempt) {
34377
34377
  return attempt.inProgress === ASSESSMENT_ATTEMPT_COMPLETED.inProgress && attempt.scoreStatus === ASSESSMENT_ATTEMPT_COMPLETED.scoreStatus;
34378
34378
  }
34379
+ function isAssessmentAttemptSuperseded(attempt) {
34380
+ return attempt.inProgress === ASSESSMENT_ATTEMPT_SUPERSEDED.inProgress && attempt.scoreStatus === ASSESSMENT_ATTEMPT_SUPERSEDED.scoreStatus;
34381
+ }
34379
34382
  function classifyAssessmentSubmission(attempt, submissionId) {
34380
34383
  if (isAssessmentAttemptCompleted(attempt)) {
34381
34384
  return attempt.submissionId === submissionId ? "replay" : "reject";
@@ -34433,6 +34436,15 @@ function assessmentReviewBankShortage(fulfillment) {
34433
34436
  function assessmentFlowForPurpose(purpose) {
34434
34437
  return purpose === "review" ? "item-submit" : "attempt-submit";
34435
34438
  }
34439
+ function compareCodeUnits(left, right) {
34440
+ if (left < right) {
34441
+ return -1;
34442
+ }
34443
+ if (left > right) {
34444
+ return 1;
34445
+ }
34446
+ return 0;
34447
+ }
34436
34448
  function canonicalJson(value) {
34437
34449
  if (value === null || typeof value !== "object") {
34438
34450
  return JSON.stringify(value) ?? "null";
@@ -34440,18 +34452,9 @@ function canonicalJson(value) {
34440
34452
  if (Array.isArray(value)) {
34441
34453
  return `[${value.map((item) => canonicalJson(item)).join(",")}]`;
34442
34454
  }
34443
- const entries = Object.entries(value).filter(([, entryValue]) => entryValue !== undefined).toSorted(([left], [right]) => compareCanonicalKeys(left, right)).map(([key, entryValue]) => `${JSON.stringify(key)}:${canonicalJson(entryValue)}`);
34455
+ const entries = Object.entries(value).filter(([, entryValue]) => entryValue !== undefined).toSorted(([left], [right]) => compareCodeUnits(left, right)).map(([key, entryValue]) => `${JSON.stringify(key)}:${canonicalJson(entryValue)}`);
34444
34456
  return `{${entries.join(",")}}`;
34445
34457
  }
34446
- function compareCanonicalKeys(left, right) {
34447
- if (left < right) {
34448
- return -1;
34449
- }
34450
- if (left > right) {
34451
- return 1;
34452
- }
34453
- return 0;
34454
- }
34455
34458
  async function diagnosticRoutingRevision(manifest) {
34456
34459
  const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(canonicalJson(manifest)));
34457
34460
  return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
@@ -35751,15 +35754,6 @@ function assessmentPresentationForAttempt(assessment, attemptId) {
35751
35754
  });
35752
35755
  return { ...assessment, items };
35753
35756
  }
35754
- function compareCodeUnits(left, right) {
35755
- if (left < right) {
35756
- return -1;
35757
- }
35758
- if (left > right) {
35759
- return 1;
35760
- }
35761
- return 0;
35762
- }
35763
35757
  function reviewStandardFieldsWithinLimits(input) {
35764
35758
  return input.framework.trim().length <= TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.standardFieldLength && input.identifier.trim().length <= TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.standardFieldLength;
35765
35759
  }
@@ -35770,6 +35764,19 @@ function canonicalReviewStandardRef(input) {
35770
35764
  }
35771
35765
  return standard.framework && standard.identifier ? standard : null;
35772
35766
  }
35767
+ function isKnownDescriptiveCurriculumFramework(framework) {
35768
+ return framework === "CCSS" || framework.startsWith("CCSS.");
35769
+ }
35770
+ function reviewRoutingStandardRefsFromMetadata(metadata2, options = {}) {
35771
+ const refs = new Map;
35772
+ for (const authored of qtiAssessmentStandardRefsFromMetadata(metadata2)) {
35773
+ const standard = canonicalReviewStandardRef(authored);
35774
+ if (standard && (!options.customOnly || !isKnownDescriptiveCurriculumFramework(standard.framework))) {
35775
+ refs.set(assessmentStandardRefKey(standard), standard);
35776
+ }
35777
+ }
35778
+ return [...refs.values()];
35779
+ }
35773
35780
  function canonicalRequestStandards(standards) {
35774
35781
  if (standards.length > TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.standards) {
35775
35782
  throw new RangeError(`A review request may contain at most ${TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.standards} standards`);
@@ -35825,42 +35832,172 @@ function qtiQuestionByIdentifier(questions) {
35825
35832
  }
35826
35833
  return byIdentifier;
35827
35834
  }
35828
- async function buildReviewBankIndex(assessment, questions) {
35835
+ async function reviewBankRevision(input) {
35836
+ const alignmentSignature = input.items.map((item) => ({
35837
+ itemIdentifier: item.itemIdentifier,
35838
+ standardKeys: item.standards.map(assessmentStandardRefKey)
35839
+ }));
35840
+ const bankRevisionUuid = await deterministicUUID(`playcademy:review-bank:v1:${JSON.stringify({
35841
+ bankIdentifier: input.bankIdentifier,
35842
+ contentRevision: input.sourceContentRevision,
35843
+ alignments: alignmentSignature
35844
+ })}`);
35845
+ return `review-bank-v1:${bankRevisionUuid}`;
35846
+ }
35847
+ async function reviewBankIndex(input) {
35848
+ return {
35849
+ ...input,
35850
+ bankRevision: await reviewBankRevision(input)
35851
+ };
35852
+ }
35853
+ async function reviewBankSourceFingerprint(input) {
35854
+ const fingerprintUuid = await deterministicUUID(`playcademy:review-bank-source:v1:${JSON.stringify({
35855
+ bankIdentifier: input.bankIdentifier,
35856
+ items: input.items.map((item) => ({
35857
+ itemIdentifier: item.itemIdentifier,
35858
+ standardKeys: item.standards.map(assessmentStandardRefKey)
35859
+ }))
35860
+ })}`);
35861
+ return `review-bank-source-v1:${fingerprintUuid}`;
35862
+ }
35863
+ async function buildReviewBankIndex(assessment, questions, options = {}) {
35829
35864
  if (assessment.identifier !== questions.assessmentTest) {
35830
35865
  throw new Error(`QTI questions for ${questions.assessmentTest} do not describe assessment ${assessment.identifier}`);
35831
35866
  }
35832
35867
  const questionByIdentifier = qtiQuestionByIdentifier(questions);
35833
35868
  const items = assessment.items.map((item) => {
35834
35869
  const question = questionByIdentifier.get(item.identifier);
35835
- const canonical = new Map;
35836
- for (const authored of qtiAssessmentStandardRefsFromMetadata(question?.metadata)) {
35837
- const standard = canonicalReviewStandardRef(authored);
35838
- if (standard) {
35839
- canonical.set(assessmentStandardRefKey(standard), standard);
35840
- }
35841
- }
35842
- const standards = [...canonical.values()].toSorted((left, right) => compareCodeUnits(assessmentStandardRefKey(left), assessmentStandardRefKey(right)));
35870
+ const standards = reviewRoutingStandardRefsFromMetadata(question?.metadata, options).toSorted((left, right) => compareCodeUnits(assessmentStandardRefKey(left), assessmentStandardRefKey(right)));
35843
35871
  return {
35844
35872
  itemIdentifier: item.identifier,
35845
35873
  standards
35846
35874
  };
35847
35875
  });
35848
- const alignmentSignature = items.map((item) => ({
35849
- itemIdentifier: item.itemIdentifier,
35850
- standardKeys: item.standards.map(assessmentStandardRefKey)
35851
- }));
35852
- const bankRevisionUuid = await deterministicUUID(`playcademy:review-bank:v1:${JSON.stringify({
35853
- bankIdentifier: assessment.identifier,
35854
- contentRevision: assessment.contentRevision,
35855
- alignments: alignmentSignature
35856
- })}`);
35857
- return {
35876
+ return reviewBankIndex({
35858
35877
  bankIdentifier: assessment.identifier,
35859
35878
  sourceContentRevision: assessment.contentRevision,
35860
- bankRevision: `review-bank-v1:${bankRevisionUuid}`,
35861
35879
  items
35880
+ });
35881
+ }
35882
+ function emptyReviewBankManifest(bankIdentifier) {
35883
+ return {
35884
+ version: PLAYCADEMY_REVIEW_BANK_MANIFEST_VERSION,
35885
+ bankIdentifier,
35886
+ sourceFingerprint: null,
35887
+ sourceContentRevision: null,
35888
+ bankRevision: null,
35889
+ itemsByStandard: {}
35890
+ };
35891
+ }
35892
+ function reviewBankMappingIssues(bank) {
35893
+ return {
35894
+ missingOrInvalidItemIdentifiers: bank.items.filter((item) => item.standards.length === 0).map((item) => item.itemIdentifier),
35895
+ multipleStandardItemIdentifiers: bank.items.filter((item) => item.standards.length > 1).map((item) => item.itemIdentifier)
35896
+ };
35897
+ }
35898
+ async function buildReviewBankManifest(bank) {
35899
+ const itemsByStandard = new Map;
35900
+ for (const item of bank.items) {
35901
+ for (const standard of item.standards) {
35902
+ const key = assessmentStandardRefKey(standard);
35903
+ const identifiers = itemsByStandard.get(key) ?? [];
35904
+ identifiers.push(item.itemIdentifier);
35905
+ itemsByStandard.set(key, identifiers);
35906
+ }
35907
+ }
35908
+ return {
35909
+ version: PLAYCADEMY_REVIEW_BANK_MANIFEST_VERSION,
35910
+ bankIdentifier: bank.bankIdentifier,
35911
+ sourceFingerprint: await reviewBankSourceFingerprint(bank),
35912
+ sourceContentRevision: bank.sourceContentRevision,
35913
+ bankRevision: bank.bankRevision,
35914
+ itemsByStandard: Object.fromEntries([...itemsByStandard.entries()].toSorted(([left], [right]) => compareCodeUnits(left, right)))
35862
35915
  };
35863
35916
  }
35917
+ function requiredManifestString(record, field) {
35918
+ const value = record[field];
35919
+ if (typeof value !== "string" || !value.trim()) {
35920
+ throw new Error(`Review-bank manifest requires ${field}`);
35921
+ }
35922
+ return value.trim();
35923
+ }
35924
+ function standardFromManifestKey(key) {
35925
+ try {
35926
+ const parsed = JSON.parse(key);
35927
+ if (!Array.isArray(parsed) || parsed.length !== 2 || typeof parsed[0] !== "string" || typeof parsed[1] !== "string") {
35928
+ return null;
35929
+ }
35930
+ const standard = canonicalReviewStandardRef({
35931
+ framework: parsed[0],
35932
+ identifier: parsed[1]
35933
+ });
35934
+ return standard && assessmentStandardRefKey(standard) === key ? standard : null;
35935
+ } catch {
35936
+ return null;
35937
+ }
35938
+ }
35939
+ async function reviewBankIndexFromManifest(metadata2, input) {
35940
+ const raw = isRecord(metadata2) ? metadata2[PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY] : undefined;
35941
+ if (raw === undefined) {
35942
+ return null;
35943
+ }
35944
+ if (!isRecord(raw) || raw.version !== PLAYCADEMY_REVIEW_BANK_MANIFEST_VERSION) {
35945
+ throw new Error("Review-bank manifest has an unsupported version or shape");
35946
+ }
35947
+ const manifest = raw;
35948
+ const bankIdentifier = requiredManifestString(manifest, "bankIdentifier");
35949
+ if (bankIdentifier !== input.bankIdentifier) {
35950
+ throw new Error(`Review-bank manifest for ${bankIdentifier} does not describe ${input.bankIdentifier}`);
35951
+ }
35952
+ if (manifest.sourceFingerprint === null && manifest.sourceContentRevision === null && manifest.bankRevision === null) {
35953
+ return null;
35954
+ }
35955
+ const sourceFingerprint = requiredManifestString(manifest, "sourceFingerprint");
35956
+ const sourceContentRevision = requiredManifestString(manifest, "sourceContentRevision");
35957
+ const declaredBankRevision = requiredManifestString(manifest, "bankRevision");
35958
+ const itemsByStandard = manifest.itemsByStandard;
35959
+ if (!isRecord(itemsByStandard)) {
35960
+ throw new Error("Review-bank manifest requires an itemsByStandard mapping");
35961
+ }
35962
+ const membership = new Set(input.membershipItemIdentifiers);
35963
+ const standardsByItem = new Map;
35964
+ for (const [standardKey, rawItemIdentifiers] of Object.entries(itemsByStandard)) {
35965
+ const standard = standardFromManifestKey(standardKey);
35966
+ if (!standard || !Array.isArray(rawItemIdentifiers)) {
35967
+ throw new Error("Review-bank manifest contains an invalid standard mapping");
35968
+ }
35969
+ for (const rawItemIdentifier of rawItemIdentifiers) {
35970
+ if (typeof rawItemIdentifier !== "string" || !rawItemIdentifier.trim()) {
35971
+ throw new Error("Review-bank manifest contains an invalid item identifier");
35972
+ }
35973
+ const itemIdentifier = rawItemIdentifier.trim();
35974
+ if (membership.has(itemIdentifier)) {
35975
+ const standards = standardsByItem.get(itemIdentifier) ?? new Map;
35976
+ standards.set(assessmentStandardRefKey(standard), standard);
35977
+ standardsByItem.set(itemIdentifier, standards);
35978
+ }
35979
+ }
35980
+ }
35981
+ const index = await reviewBankIndex({
35982
+ bankIdentifier,
35983
+ sourceContentRevision,
35984
+ items: input.membershipItemIdentifiers.map((itemIdentifier) => ({
35985
+ itemIdentifier,
35986
+ standards: [...standardsByItem.get(itemIdentifier)?.values() ?? []].toSorted((left, right) => compareCodeUnits(assessmentStandardRefKey(left), assessmentStandardRefKey(right)))
35987
+ }))
35988
+ });
35989
+ const issues = reviewBankMappingIssues(index);
35990
+ if (issues.missingOrInvalidItemIdentifiers.length > 0 || issues.multipleStandardItemIdentifiers.length > 0) {
35991
+ throw new Error("Review-bank manifest must map every member to exactly one standard/node key");
35992
+ }
35993
+ if (await reviewBankSourceFingerprint(index) !== sourceFingerprint) {
35994
+ throw new Error("Review-bank manifest source fingerprint is stale");
35995
+ }
35996
+ if (index.bankRevision !== declaredBankRevision) {
35997
+ throw new Error("Review-bank manifest revision does not match its candidate index");
35998
+ }
35999
+ return index;
36000
+ }
35864
36001
  function exposureTimestamp(value) {
35865
36002
  const timestamp = Date.parse(value);
35866
36003
  return Number.isFinite(timestamp) ? timestamp : 0;
@@ -36348,6 +36485,7 @@ var COMMON_CORE_ELA_FRAMEWORK_ALIASES;
36348
36485
  var COMMON_CORE_FRAMEWORK_ALIASES;
36349
36486
  var ASSESSMENT_ATTEMPT_OPEN;
36350
36487
  var ASSESSMENT_ATTEMPT_COMPLETED;
36488
+ var ASSESSMENT_ATTEMPT_SUPERSEDED;
36351
36489
  var ASSESSMENT_RUNTIME_ERROR_STATUS;
36352
36490
  var ROUTING_KEY_MAX_LENGTH = 128;
36353
36491
  var ROUTING_RESULT_KEY_MAX_LENGTH = 256;
@@ -36367,6 +36505,8 @@ var DiagnosticRoutingManifestV1Schema;
36367
36505
  var GRADE_VALUES;
36368
36506
  var POINT_RESPONSE_PATTERN;
36369
36507
  var REVIEW_SELECTION_POLICY_VERSION = "unseen-first-least-recently-delivered-v1";
36508
+ var PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY = "playcademyReviewBank";
36509
+ var PLAYCADEMY_REVIEW_BANK_MANIFEST_VERSION = 1;
36370
36510
  var DEFAULT_REVIEW_SELECTION_POLICY;
36371
36511
  var RuntimeSubjectSchema;
36372
36512
  var RuntimeGradeSchema;
@@ -36571,6 +36711,10 @@ var init_assessment_runtime2 = __esm(() => {
36571
36711
  inProgress: "false",
36572
36712
  scoreStatus: "fully graded"
36573
36713
  };
36714
+ ASSESSMENT_ATTEMPT_SUPERSEDED = {
36715
+ inProgress: "false",
36716
+ scoreStatus: "not submitted"
36717
+ };
36574
36718
  ASSESSMENT_RUNTIME_ERROR_STATUS = {
36575
36719
  NO_ELIGIBLE_TESTS: 404,
36576
36720
  REVIEW_BANK_SHORTAGE: 422,
@@ -115025,73 +115169,6 @@ function stringField2(value) {
115025
115169
  function firstStringField2(...values) {
115026
115170
  return values.map(stringField2).find(Boolean) ?? "";
115027
115171
  }
115028
- function normalizedWhitespace2(value) {
115029
- return value.normalize("NFKC").trim().replace(/\s+/g, " ");
115030
- }
115031
- function normalizedIdentityCase2(value) {
115032
- return value.toLocaleUpperCase("en-US");
115033
- }
115034
- function frameworkAliasKey2(value) {
115035
- return value.normalize("NFKC").toLocaleLowerCase("en-US").replace(/[^a-z0-9]+/g, "");
115036
- }
115037
- function isUuidShaped2(value) {
115038
- return UUID_PATTERN2.test(value.trim());
115039
- }
115040
- function isCommonCoreMathIdentifier2(identifier) {
115041
- const canonical = normalizedIdentityCase2(normalizedWhitespace2(identifier));
115042
- 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);
115043
- }
115044
- function isCommonCoreElaIdentifier2(identifier) {
115045
- const canonical = normalizedIdentityCase2(normalizedWhitespace2(identifier));
115046
- 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);
115047
- }
115048
- function canonicalFramework2(authoredFramework, identifier) {
115049
- const aliasKey = frameworkAliasKey2(authoredFramework);
115050
- if (COMMON_CORE_MATH_FRAMEWORK_ALIASES2.has(aliasKey) || COMMON_CORE_FRAMEWORK_ALIASES2.has(aliasKey) && isCommonCoreMathIdentifier2(identifier)) {
115051
- return "CCSS.Math";
115052
- }
115053
- if (COMMON_CORE_ELA_FRAMEWORK_ALIASES2.has(aliasKey) || COMMON_CORE_FRAMEWORK_ALIASES2.has(aliasKey) && isCommonCoreElaIdentifier2(identifier)) {
115054
- return "CCSS.ELA-Literacy";
115055
- }
115056
- if (COMMON_CORE_FRAMEWORK_ALIASES2.has(aliasKey)) {
115057
- return "CCSS";
115058
- }
115059
- return normalizedIdentityCase2(authoredFramework);
115060
- }
115061
- function canonicalIdentifier2(framework, authoredIdentifier) {
115062
- const identifier = normalizedIdentityCase2(normalizedWhitespace2(authoredIdentifier));
115063
- if (framework === "CCSS.Math") {
115064
- return identifier.replace(/^CCSS\.MATH\.CONTENT\./, "").replace(/^CCSS\.MATH\.PRACTICE\./, "").replace(/^CCSS\.MATH\./, "");
115065
- }
115066
- if (framework === "CCSS.ELA-Literacy") {
115067
- return identifier.replace(/^CCSS\.ELA-LITERACY\./, "");
115068
- }
115069
- return identifier;
115070
- }
115071
- function readableAlignmentIdentifier2(alignment) {
115072
- const identifier = alignment.identifier?.trim() ?? "";
115073
- if (identifier && !isUuidShaped2(identifier)) {
115074
- return identifier;
115075
- }
115076
- const id = alignment.id.trim();
115077
- return id && !isUuidShaped2(id) ? id : "";
115078
- }
115079
- function canonicalAssessmentStandardRef2(standard) {
115080
- const authoredFramework = normalizedWhitespace2(standard.framework);
115081
- const framework = canonicalFramework2(authoredFramework, standard.identifier);
115082
- const identifier = canonicalIdentifier2(framework, standard.identifier);
115083
- return {
115084
- framework,
115085
- identifier
115086
- };
115087
- }
115088
- function assessmentStandardRefKey2(standard) {
115089
- const canonical = canonicalAssessmentStandardRef2(standard);
115090
- return JSON.stringify([
115091
- normalizedIdentityCase2(canonical.framework),
115092
- normalizedIdentityCase2(canonical.identifier)
115093
- ]);
115094
- }
115095
115172
  function dedupeStandards2(standards) {
115096
115173
  const deduped = new Map;
115097
115174
  for (const standard of standards) {
@@ -115182,22 +115259,6 @@ function qtiStandardsFromMetadata(metadata2) {
115182
115259
  standards.push(...qtiAlignmentStandardsFromMetadata2(metadata2));
115183
115260
  return dedupeStandards2(standards);
115184
115261
  }
115185
- function qtiAssessmentStandardRefsFromMetadata2(metadata2) {
115186
- const refs = new Map;
115187
- for (const alignment of qtiAlignmentStandardsFromMetadata2(metadata2)) {
115188
- const identifier = readableAlignmentIdentifier2(alignment);
115189
- if (identifier) {
115190
- const canonical = canonicalAssessmentStandardRef2({
115191
- framework: alignment.source,
115192
- identifier
115193
- });
115194
- if (canonical.framework && canonical.identifier) {
115195
- refs.set(assessmentStandardRefKey2(canonical), canonical);
115196
- }
115197
- }
115198
- }
115199
- return [...refs.values()];
115200
- }
115201
115262
  var EVENT_HANDLER_ATTRIBUTE;
115202
115263
  var SCRIPT_SCHEMES2;
115203
115264
  var DATA_RASTER_IMAGE_PATTERN2;
@@ -115242,7 +115303,6 @@ var BLANK_SENTINEL22 = "
115242
115303
  var PLAYABLE_POINT_VALUE;
115243
115304
  var POINT_INTERACTION_TYPES2;
115244
115305
  var INDEPENDENT_PROCESSING_TAGS2;
115245
- var UUID_PATTERN2;
115246
115306
  var COMMON_CORE_MATH_FRAMEWORK_ALIASES2;
115247
115307
  var COMMON_CORE_ELA_FRAMEWORK_ALIASES2;
115248
115308
  var COMMON_CORE_FRAMEWORK_ALIASES2;
@@ -115495,7 +115555,6 @@ var init_qti = __esm(() => {
115495
115555
  "qti-sum",
115496
115556
  "qti-base-value"
115497
115557
  ]);
115498
- 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;
115499
115558
  COMMON_CORE_MATH_FRAMEWORK_ALIASES2 = new Set([
115500
115559
  "ccssm",
115501
115560
  "ccssmath",
@@ -117895,8 +117954,8 @@ function assertAssessmentHasQuestions(questions) {
117895
117954
  }
117896
117955
  }
117897
117956
  function assertReviewAssessmentHasStandards(standardCounts) {
117898
- if (standardCounts.some((count) => count <= 0)) {
117899
- throw new ValidationError("Every question in a review assessment must have a standards alignment");
117957
+ if (standardCounts.some((count) => count !== 1)) {
117958
+ throw new ValidationError("Every question in a review assessment must have exactly one standards alignment");
117900
117959
  }
117901
117960
  }
117902
117961
  function planAssessmentRemoval(status) {
@@ -117959,6 +118018,41 @@ function orderLiveAssessmentRows(liveRows, purpose, testIdentifiers) {
117959
118018
  var init_timeback_assessment_rules_util = __esm(() => {
117960
118019
  init_errors2();
117961
118020
  });
118021
+ function stageAssessmentAttemptSupersession(attempt) {
118022
+ Object.assign(attempt.result, ASSESSMENT_ATTEMPT_SUPERSEDED);
118023
+ Object.assign(attempt.selection, ASSESSMENT_ATTEMPT_SUPERSEDED);
118024
+ }
118025
+ function shouldConfirmAssessmentVersionUnavailable(details) {
118026
+ return typeof details === "object" && details !== null && "expectedRevision" in details && typeof details.expectedRevision === "string" && !("currentRevision" in details);
118027
+ }
118028
+ function buildAssessmentAttemptSupersessionUpdate(result) {
118029
+ if (result.status !== "active" || !isAssessmentAttemptOpen({
118030
+ inProgress: result.inProgress ?? "",
118031
+ scoreStatus: result.scoreStatus
118032
+ })) {
118033
+ return null;
118034
+ }
118035
+ return {
118036
+ status: "active",
118037
+ assessmentLineItem: result.assessmentLineItem,
118038
+ student: result.student,
118039
+ score: result.score ?? 0,
118040
+ scoreDate: result.scoreDate,
118041
+ ...ASSESSMENT_ATTEMPT_SUPERSEDED,
118042
+ metadata: result.metadata ?? {}
118043
+ };
118044
+ }
118045
+ async function resolveAvailableAssessmentAttempt(initialDecision, select3, resume) {
118046
+ let decision = initialDecision;
118047
+ while (decision?.kind === "resume") {
118048
+ const snapshot = await resume(decision);
118049
+ if (snapshot !== null) {
118050
+ return { kind: "resumed", snapshot };
118051
+ }
118052
+ decision = select3();
118053
+ }
118054
+ return { kind: "continue", decision };
118055
+ }
117962
118056
  function buildReviewItemResultMetadata(input) {
117963
118057
  const administration = itemAdministration(input.metadata.itemSubmissions, input.selection.itemIdentifier);
117964
118058
  if (!administration) {
@@ -118403,8 +118497,8 @@ function parsePlayableItem(question) {
118403
118497
  function assessmentContentRevision(test, questions) {
118404
118498
  return sha256Hex(canonicalJson2({
118405
118499
  identifier: test.identifier,
118500
+ title: test.title,
118406
118501
  qtiVersion: test.qtiVersion,
118407
- rawXml: test.rawXml,
118408
118502
  parts: test["qti-test-part"],
118409
118503
  questions: questions.questions.map(({ reference, question }) => ({
118410
118504
  identifier: reference.identifier,
@@ -118560,11 +118654,30 @@ async function hydrateQtiTestQuestions(client2, references) {
118560
118654
  }));
118561
118655
  return { ...references, questions };
118562
118656
  }
118563
- async function loadHydratedQtiTest(client2, identifier) {
118657
+ async function hydrateQtiTestQuestionSelection(client2, references, itemIdentifiers) {
118658
+ const referenceByIdentifier = new Map(references.questions.map((reference) => [reference.reference.identifier, reference]));
118659
+ const selectedReferences = itemIdentifiers.map((itemIdentifier) => {
118660
+ const reference = referenceByIdentifier.get(itemIdentifier);
118661
+ if (!reference) {
118662
+ throw new Error(`QTI assessment ${references.assessmentTest} does not contain item ${itemIdentifier}`);
118663
+ }
118664
+ return reference;
118665
+ });
118666
+ return hydrateQtiTestQuestions(client2, {
118667
+ ...references,
118668
+ totalQuestions: selectedReferences.length,
118669
+ questions: selectedReferences
118670
+ });
118671
+ }
118672
+ async function loadQtiTestReferences(client2, identifier) {
118564
118673
  const [test, references] = await Promise.all([
118565
118674
  client2.qtiApi.assessmentTests.get(identifier),
118566
118675
  client2.qtiApi.assessmentTests.getQuestions(identifier)
118567
118676
  ]);
118677
+ return { test, references };
118678
+ }
118679
+ async function loadHydratedQtiTest(client2, identifier) {
118680
+ const { test, references } = await loadQtiTestReferences(client2, identifier);
118568
118681
  return {
118569
118682
  test,
118570
118683
  questions: await hydrateQtiTestQuestions(client2, references)
@@ -118578,6 +118691,7 @@ class TimebackAssessmentRuntimeService {
118578
118691
  static ASSESSMENT_CACHE_TTL_MS = 60000;
118579
118692
  static EXPORT_CONCURRENCY = 4;
118580
118693
  static ITEM_SUBMISSION_MAX_PREPARATION_ATTEMPTS = 3;
118694
+ static ASSESSMENT_VERSION_CONFIRMATION_DELAY_MS = 25;
118581
118695
  static PROJECTION_BARRIER_MAX_ATTEMPTS = 5;
118582
118696
  static PROJECTION_BARRIER_RETRY_DELAY_MS = 25;
118583
118697
  static SCORING_CONCURRENCY = 4;
@@ -118590,6 +118704,10 @@ class TimebackAssessmentRuntimeService {
118590
118704
  defaultTTL: TimebackAssessmentRuntimeService.ASSESSMENT_CACHE_TTL_MS,
118591
118705
  maxSize: TimebackAssessmentRuntimeService.ASSESSMENT_CACHE_LIMIT
118592
118706
  });
118707
+ reviewSelectionCache = new TimebackCache({
118708
+ defaultTTL: TimebackAssessmentRuntimeService.ASSESSMENT_CACHE_TTL_MS,
118709
+ maxSize: TimebackAssessmentRuntimeService.ASSESSMENT_CACHE_LIMIT
118710
+ });
118593
118711
  constructor(deps) {
118594
118712
  this.deps = deps;
118595
118713
  }
@@ -118608,6 +118726,7 @@ class TimebackAssessmentRuntimeService {
118608
118726
  }
118609
118727
  const masteryStandard = input.purpose === "mastery" ? normalizeMasteryStandard(input.standard) : undefined;
118610
118728
  const requestFingerprint = this.startRequestFingerprint(input);
118729
+ const pendingSupersessions = new Set;
118611
118730
  const candidates = await this.candidateIntegrations(gameId, studentId, input);
118612
118731
  const lockKeys = candidates.rows.map((row) => assessmentRuntimeStartLockKey({
118613
118732
  studentId,
@@ -118630,20 +118749,21 @@ class TimebackAssessmentRuntimeService {
118630
118749
  this.listAttempts(attemptScope)
118631
118750
  ]);
118632
118751
  const attempts = listing.attempts;
118633
- const compatibleAttempts = this.attemptsMatchingRequest(attempts, requestFingerprint);
118634
- let decision = this.requireDecision(selectRuntimeAssessment(tests, [...compatibleAttempts.values()].map((entry2) => entry2.selection)), input.purpose);
118635
- if (decision.kind === "resume") {
118636
- return this.resumeSnapshot(decision, attempts, context2.integration);
118752
+ const initialResolution = await this.resumeAvailableAttempt(tests, attempts, requestFingerprint, pendingSupersessions, (resumeDecision) => this.resumeSnapshot(resumeDecision, attempts, context2.integration));
118753
+ if (initialResolution.kind === "resumed") {
118754
+ await this.persistPendingSupersessionsBestEffort(pendingSupersessions);
118755
+ return initialResolution.snapshot;
118637
118756
  }
118757
+ let decision = this.requireDecision(initialResolution.decision, input.purpose);
118638
118758
  let assessment = await this.loadAssessment(decision.test.qtiTestIdentifier);
118639
118759
  const refreshedListing = await this.listAttempts(attemptScope);
118640
118760
  const refreshed = refreshedListing.attempts;
118641
- const refreshedCompatible = this.attemptsMatchingRequest(refreshed, requestFingerprint);
118642
- const refreshedDecision = this.requireDecision(selectRuntimeAssessment(tests, [...refreshedCompatible.values()].map((entry2) => entry2.selection)), input.purpose);
118643
- if (refreshedDecision.kind === "resume") {
118644
- return this.resumeSnapshot(refreshedDecision, refreshed, context2.integration);
118761
+ const refreshedResolution = await this.resumeAvailableAttempt(tests, refreshed, requestFingerprint, pendingSupersessions, (resumeDecision) => this.resumeSnapshot(resumeDecision, refreshed, context2.integration));
118762
+ if (refreshedResolution.kind === "resumed") {
118763
+ await this.persistPendingSupersessionsBestEffort(pendingSupersessions);
118764
+ return refreshedResolution.snapshot;
118645
118765
  }
118646
- decision = refreshedDecision;
118766
+ decision = this.requireDecision(refreshedResolution.decision, input.purpose);
118647
118767
  if (assessment.identifier !== decision.test.qtiTestIdentifier) {
118648
118768
  assessment = await this.loadAssessment(decision.test.qtiTestIdentifier);
118649
118769
  }
@@ -118701,6 +118821,7 @@ class TimebackAssessmentRuntimeService {
118701
118821
  grade: context2.integration.grade
118702
118822
  })
118703
118823
  });
118824
+ await this.persistPendingSupersessionsBestEffort(pendingSupersessions);
118704
118825
  return this.snapshot(result, metadata2, assessment, context2.integration);
118705
118826
  });
118706
118827
  }
@@ -118710,6 +118831,7 @@ class TimebackAssessmentRuntimeService {
118710
118831
  input
118711
118832
  }) {
118712
118833
  const requestFingerprint = this.startRequestFingerprint(input);
118834
+ const pendingSupersessions = new Set;
118713
118835
  const candidates = await this.candidateIntegrations(gameId, studentId, input);
118714
118836
  const lockKeys = candidates.rows.map((row) => assessmentRuntimeStartLockKey({
118715
118837
  studentId,
@@ -118731,22 +118853,23 @@ class TimebackAssessmentRuntimeService {
118731
118853
  this.liveTests(context2.integration.id, input.purpose, db2, undefined, input.diagnosticKey),
118732
118854
  this.listAttempts(attemptScope)
118733
118855
  ]);
118734
- const compatible = this.attemptsMatchingRequest(listing.attempts, requestFingerprint);
118735
- let decision = this.requireDecision(selectRuntimeAssessment(tests, [...compatible.values()].map((entry2) => entry2.selection)), input.purpose);
118736
- if (decision.kind === "resume") {
118737
- return this.resumeDiagnosticSnapshot(decision, listing.attempts, context2.integration, db2);
118856
+ const initialResolution = await this.resumeAvailableAttempt(tests, listing.attempts, requestFingerprint, pendingSupersessions, (resumeDecision) => this.resumeDiagnosticSnapshot(resumeDecision, listing.attempts, context2.integration, db2));
118857
+ if (initialResolution.kind === "resumed") {
118858
+ await this.persistPendingSupersessionsBestEffort(pendingSupersessions);
118859
+ return initialResolution.snapshot;
118738
118860
  }
118861
+ let decision = this.requireDecision(initialResolution.decision, input.purpose);
118739
118862
  let definition = await this.requireDiagnosticDefinition(decision.test.id, context2.integration.id, db2, true);
118740
118863
  let assessment = await this.loadAssessment(definition.qtiTestIdentifier);
118741
118864
  let routing = await this.initializeHostedDiagnostic(definition, assessment);
118742
118865
  const refreshedListing = await this.listAttempts(attemptScope);
118743
118866
  const refreshed = refreshedListing.attempts;
118744
- const refreshedCompatible = this.attemptsMatchingRequest(refreshed, requestFingerprint);
118745
- const refreshedDecision = this.requireDecision(selectRuntimeAssessment(tests, [...refreshedCompatible.values()].map((entry2) => entry2.selection)), input.purpose);
118746
- if (refreshedDecision.kind === "resume") {
118747
- return this.resumeDiagnosticSnapshot(refreshedDecision, refreshed, context2.integration, db2);
118867
+ const refreshedResolution = await this.resumeAvailableAttempt(tests, refreshed, requestFingerprint, pendingSupersessions, (resumeDecision) => this.resumeDiagnosticSnapshot(resumeDecision, refreshed, context2.integration, db2));
118868
+ if (refreshedResolution.kind === "resumed") {
118869
+ await this.persistPendingSupersessionsBestEffort(pendingSupersessions);
118870
+ return refreshedResolution.snapshot;
118748
118871
  }
118749
- decision = refreshedDecision;
118872
+ decision = this.requireDecision(refreshedResolution.decision, input.purpose);
118750
118873
  if (definition.id !== decision.test.id) {
118751
118874
  definition = await this.requireDiagnosticDefinition(decision.test.id, context2.integration.id, db2, true);
118752
118875
  assessment = await this.loadAssessment(definition.qtiTestIdentifier);
@@ -118815,6 +118938,7 @@ class TimebackAssessmentRuntimeService {
118815
118938
  grade: context2.integration.grade
118816
118939
  })
118817
118940
  });
118941
+ await this.persistPendingSupersessionsBestEffort(pendingSupersessions);
118818
118942
  return this.diagnosticSnapshot(result, metadata2, assessment, context2.integration, routing.state);
118819
118943
  });
118820
118944
  }
@@ -118823,7 +118947,9 @@ class TimebackAssessmentRuntimeService {
118823
118947
  studentId,
118824
118948
  input
118825
118949
  }) {
118950
+ const preparationStartedAt = Date.now();
118826
118951
  const requestFingerprint = reviewRequestFingerprint(input, DEFAULT_REVIEW_SELECTION_POLICY);
118952
+ const pendingSupersessions = new Set;
118827
118953
  const normalizedRequest = normalizeReviewRequest(input);
118828
118954
  const candidates = await this.candidateIntegrations(gameId, studentId, input);
118829
118955
  const lockKeys = candidates.rows.map((row) => assessmentRuntimeStartLockKey({
@@ -118847,38 +118973,44 @@ class TimebackAssessmentRuntimeService {
118847
118973
  this.listAttempts(attemptScope)
118848
118974
  ]);
118849
118975
  const attempts = listing.attempts;
118850
- const compatible = this.attemptsMatchingRequest(attempts, requestFingerprint);
118851
- let decision = selectRuntimeAssessment(tests, [...compatible.values()].map((entry2) => entry2.selection));
118852
- if (decision?.kind === "resume") {
118853
- return this.resumeReviewSnapshot(decision, compatible, listing.reviewChildren, context2);
118976
+ const initialResolution = await this.resumeAvailableAttempt(tests, attempts, requestFingerprint, pendingSupersessions, (resumeDecision) => this.resumeReviewSnapshot(resumeDecision, attempts, listing.reviewChildren, context2));
118977
+ if (initialResolution.kind === "resumed") {
118978
+ await this.persistPendingSupersessionsBestEffort(pendingSupersessions);
118979
+ return initialResolution.snapshot;
118854
118980
  }
118981
+ let decision = initialResolution.decision;
118855
118982
  if (tests.length === 0 || !decision) {
118856
118983
  throw AssessmentRuntimeError.from(assessmentNoEligibleTests(input.purpose));
118857
118984
  }
118858
118985
  if (tests.length > 1) {
118859
118986
  throw new ValidationError("Standards review requires exactly one live review-bank assessment.");
118860
118987
  }
118861
- let source = await this.loadReviewBank(decision.test.qtiTestIdentifier);
118988
+ const manifestStartedAt = Date.now();
118989
+ let catalog = await this.loadReviewBankCatalog(decision.test.qtiTestIdentifier);
118990
+ this.recordReviewPreparationPhase("manifest_load", manifestStartedAt, {
118991
+ candidateCount: catalog.bank.items.length
118992
+ });
118862
118993
  const refreshedListing = await this.listAttempts(attemptScope);
118863
118994
  const refreshed = refreshedListing.attempts;
118864
- const refreshedCompatible = this.attemptsMatchingRequest(refreshed, requestFingerprint);
118865
- const refreshedDecision = selectRuntimeAssessment(tests, [...refreshedCompatible.values()].map((entry2) => entry2.selection));
118866
- if (refreshedDecision?.kind === "resume") {
118867
- return this.resumeReviewSnapshot(refreshedDecision, refreshedCompatible, refreshedListing.reviewChildren, context2);
118995
+ const refreshedResolution = await this.resumeAvailableAttempt(tests, refreshed, requestFingerprint, pendingSupersessions, (resumeDecision) => this.resumeReviewSnapshot(resumeDecision, refreshed, refreshedListing.reviewChildren, context2));
118996
+ if (refreshedResolution.kind === "resumed") {
118997
+ await this.persistPendingSupersessionsBestEffort(pendingSupersessions);
118998
+ return refreshedResolution.snapshot;
118868
118999
  }
119000
+ const refreshedDecision = refreshedResolution.decision;
118869
119001
  if (!refreshedDecision || refreshedDecision.kind !== "start") {
118870
119002
  throw AssessmentRuntimeError.from(assessmentNoEligibleTests(input.purpose));
118871
119003
  }
118872
119004
  decision = refreshedDecision;
118873
- if (source.assessment.identifier !== decision.test.qtiTestIdentifier) {
118874
- source = await this.loadReviewBank(decision.test.qtiTestIdentifier);
119005
+ if (catalog.test.identifier !== decision.test.qtiTestIdentifier) {
119006
+ catalog = await this.loadReviewBankCatalog(decision.test.qtiTestIdentifier);
118875
119007
  }
118876
119008
  const exposures = this.reviewExposures(refreshedListing.reviewChildren.values(), {
118877
- bankRevision: source.bank.bankRevision
119009
+ bankRevision: catalog.bank.bankRevision
118878
119010
  });
118879
119011
  const selected = selectReviewItems({
118880
119012
  request: input,
118881
- bank: source.bank,
119013
+ bank: catalog.bank,
118882
119014
  exposures
118883
119015
  });
118884
119016
  if (selected.selections.length === 0) {
@@ -118887,7 +119019,13 @@ class TimebackAssessmentRuntimeService {
118887
119019
  }
118888
119020
  throw AssessmentRuntimeError.from(assessmentReviewBankShortage(selected.fulfillment));
118889
119021
  }
119022
+ const hydrationStartedAt = Date.now();
119023
+ const source = await this.hydrateReviewBankSelection(catalog, selected.selections.map((selection) => selection.itemIdentifier));
119024
+ this.recordReviewPreparationPhase("selected_hydration", hydrationStartedAt, {
119025
+ selectedCount: selected.selections.length
119026
+ });
118890
119027
  const assessment = projectReviewAssessment(source.assessment, source.bank, selected.selections);
119028
+ const provisioningStartedAt = Date.now();
118891
119029
  const parentLineItemId = await this.ensureReviewBankLineItem({
118892
119030
  integration: context2.integration,
118893
119031
  activityId: input.activityId,
@@ -118950,6 +119088,12 @@ class TimebackAssessmentRuntimeService {
118950
119088
  existingChildResults: refreshedListing.reviewChildren,
118951
119089
  lookupMissingResults: false
118952
119090
  });
119091
+ await this.persistPendingSupersessionsBestEffort(pendingSupersessions);
119092
+ this.recordReviewPreparationPhase("provisioning", provisioningStartedAt);
119093
+ this.recordReviewPreparationPhase("total", preparationStartedAt, {
119094
+ candidateCount: catalog.bank.items.length,
119095
+ selectedCount: selected.selections.length
119096
+ });
118953
119097
  return this.snapshot(result, metadata2, assessment, context2.integration);
118954
119098
  });
118955
119099
  }
@@ -119303,7 +119447,7 @@ class TimebackAssessmentRuntimeService {
119303
119447
  preparationAttempts += 1;
119304
119448
  preview = await this.scoreDiagnosticItemSubmission(params, input);
119305
119449
  } else {
119306
- await this.projectDiagnosticItemResponse(committed.projection);
119450
+ await this.projectDiagnosticItemResponse(committed.projection, preview?.assessment);
119307
119451
  return committed.response;
119308
119452
  }
119309
119453
  }
@@ -119717,7 +119861,7 @@ class TimebackAssessmentRuntimeService {
119717
119861
  where: and(eq(gameTimebackAssessmentTests.id, test.id), eq(gameTimebackAssessmentTests.integrationId, integration.id))
119718
119862
  }) : undefined;
119719
119863
  const diagnostic = definition?.diagnosticKey ? await this.initializeHostedDiagnostic(definition, loaded.assessment) : null;
119720
- const reviewBank = purpose === "review" ? await buildReviewBankIndex(loaded.assessment, loaded.questions) : null;
119864
+ const reviewBank = purpose === "review" ? await buildReviewBankIndex(loaded.assessment, loaded.questions, { customOnly: true }) : null;
119721
119865
  return {
119722
119866
  ...test,
119723
119867
  live: true,
@@ -119780,7 +119924,13 @@ class TimebackAssessmentRuntimeService {
119780
119924
  const enrollmentByCourse = new Map(enrollments.map((enrollment) => [enrollment.course.id, enrollment]));
119781
119925
  const candidateRows = integrations.filter((integration) => enrollmentByCourse.has(integration.courseId));
119782
119926
  const liveTestRows = candidateRows.length === 0 ? [] : await db2.query.gameTimebackAssessmentTests.findMany({
119783
- where: and(inArray(gameTimebackAssessmentTests.integrationId, candidateRows.map((row) => row.id)), eq(gameTimebackAssessmentTests.purpose, input.purpose), eq(gameTimebackAssessmentTests.status, "live"))
119927
+ where: and(inArray(gameTimebackAssessmentTests.integrationId, candidateRows.map((row) => row.id)), eq(gameTimebackAssessmentTests.purpose, input.purpose), eq(gameTimebackAssessmentTests.status, "live")),
119928
+ columns: {
119929
+ integrationId: true,
119930
+ standardFramework: true,
119931
+ standardIdentifier: true,
119932
+ diagnosticKey: true
119933
+ }
119784
119934
  });
119785
119935
  const matchesMasteryStandard = input.purpose === "mastery" ? masteryStandardMatcher(input.standard) : undefined;
119786
119936
  const integrationIdsWithLiveTests = new Set(liveTestRows.filter((row) => (!matchesMasteryStandard || matchesMasteryStandard(assessmentStandardForRow(row))) && (input.purpose !== "diagnostic" || row.diagnosticKey === input.diagnosticKey)).map((row) => row.integrationId));
@@ -119973,6 +120123,75 @@ class TimebackAssessmentRuntimeService {
119973
120123
  });
119974
120124
  return this.diagnosticSnapshot(resumed.result, resumed.metadata, assessment, integration, routing.state);
119975
120125
  }
120126
+ async resumeAvailableAttempt(tests, attempts, requestFingerprint, pendingSupersessions, resume) {
120127
+ const select3 = () => selectRuntimeAssessment(tests, [...this.attemptsMatchingRequest(attempts, requestFingerprint).values()].map((entry2) => entry2.selection));
120128
+ return resolveAvailableAssessmentAttempt(select3(), select3, (decision) => this.resumeSnapshotOrStageSupersession(decision, attempts, pendingSupersessions, () => resume(decision)));
120129
+ }
120130
+ async resumeSnapshotOrStageSupersession(decision, attempts, pendingSupersessions, resume) {
120131
+ try {
120132
+ return await resume();
120133
+ } catch (error88) {
120134
+ if (!(error88 instanceof AssessmentRuntimeError) || error88.runtimeCode !== "SELECTED_TEST_VERSION_UNAVAILABLE") {
120135
+ throw error88;
120136
+ }
120137
+ if (shouldConfirmAssessmentVersionUnavailable(error88.details)) {
120138
+ await sleep(TimebackAssessmentRuntimeService.ASSESSMENT_VERSION_CONFIRMATION_DELAY_MS);
120139
+ try {
120140
+ return await resume();
120141
+ } catch (confirmationError) {
120142
+ if (!(confirmationError instanceof AssessmentRuntimeError) || confirmationError.runtimeCode !== "SELECTED_TEST_VERSION_UNAVAILABLE") {
120143
+ throw confirmationError;
120144
+ }
120145
+ }
120146
+ }
120147
+ const staged = attempts.get(decision.attempt.attemptId);
120148
+ pendingSupersessions.add(staged.result.sourcedId);
120149
+ stageAssessmentAttemptSupersession(staged);
120150
+ addEvent("assessment.attempt_supersession_staged", {
120151
+ "app.assessment.attempt_id": decision.attempt.attemptId,
120152
+ "app.assessment.reason": "selected_test_version_unavailable"
120153
+ });
120154
+ return null;
120155
+ }
120156
+ }
120157
+ async persistPendingSupersessions(attemptIds) {
120158
+ for (const attemptId of [...attemptIds].toSorted()) {
120159
+ await this.deps.assessmentRuntimeLock(this.deps.db, assessmentRuntimeAttemptLockKey(attemptId), async () => {
120160
+ let result;
120161
+ try {
120162
+ result = await this.requireClient().api.oneroster.assessmentResults.get(attemptId);
120163
+ } catch (error88) {
120164
+ if (isApiError(error88) && error88.statusCode === 404) {
120165
+ return;
120166
+ }
120167
+ throw error88;
120168
+ }
120169
+ const update2 = buildAssessmentAttemptSupersessionUpdate(result);
120170
+ if (!update2) {
120171
+ return;
120172
+ }
120173
+ await this.requireClient().api.oneroster.assessmentResults.upsert(attemptId, update2);
120174
+ addEvent("assessment.attempt_superseded", {
120175
+ "app.assessment.attempt_id": attemptId,
120176
+ "app.assessment.reason": "selected_test_version_unavailable"
120177
+ });
120178
+ });
120179
+ }
120180
+ }
120181
+ async persistPendingSupersessionsBestEffort(attemptIds) {
120182
+ if (attemptIds.size === 0) {
120183
+ return;
120184
+ }
120185
+ try {
120186
+ await this.persistPendingSupersessions(attemptIds);
120187
+ } catch (error88) {
120188
+ addEvent("assessment.attempt_supersession_persist_failed", {
120189
+ "app.assessment.attempt_ids": [...attemptIds].toSorted().join(","),
120190
+ "exception.type": errorType(error88),
120191
+ "app.error.message": errorMessage2(error88)
120192
+ });
120193
+ }
120194
+ }
119976
120195
  async resumeReviewSnapshot(decision, attempts, existingChildResults, context2) {
119977
120196
  if (decision.additionalResumableAttemptIds.length > 0) {
119978
120197
  addEvent("assessment.multiple_resumable_attempts", {
@@ -119984,7 +120203,8 @@ class TimebackAssessmentRuntimeService {
119984
120203
  if (resumed.metadata.purpose !== "review") {
119985
120204
  throw new Error("A standards-review request selected a non-review attempt");
119986
120205
  }
119987
- const currentSource = await this.loadReviewBank(resumed.metadata.selectedTest.identifier, resumed.metadata.selectedTest.contentRevision);
120206
+ const catalog = await this.loadReviewBankCatalog(resumed.metadata.selectedTest.identifier, resumed.metadata.selectedTest.contentRevision);
120207
+ const currentSource = await this.hydrateReviewBankSelection(catalog, resumed.metadata.review.selections.map((selection) => selection.itemIdentifier));
119988
120208
  const source = this.pinnedReviewBankSource(currentSource, resumed.metadata);
119989
120209
  const assessment = projectReviewAssessment(source.assessment, source.bank, resumed.metadata.review.selections);
119990
120210
  await this.ensureReviewChildResults({
@@ -119998,20 +120218,68 @@ class TimebackAssessmentRuntimeService {
119998
120218
  });
119999
120219
  return this.snapshot(resumed.result, resumed.metadata, assessment, context2.integration);
120000
120220
  }
120001
- async loadReviewBank(identifier, expectedSourceRevision) {
120221
+ recordReviewPreparationPhase(phase, startedAt, counts = {}) {
120222
+ addEvent("assessment.review_preparation_phase", {
120223
+ "app.assessment.review_phase": phase,
120224
+ "app.assessment.duration_ms": Date.now() - startedAt,
120225
+ ...counts.candidateCount === undefined ? {} : { "app.assessment.candidate_count": counts.candidateCount },
120226
+ ...counts.selectedCount === undefined ? {} : { "app.assessment.selected_count": counts.selectedCount }
120227
+ });
120228
+ }
120229
+ async loadReviewBankCatalog(identifier, expectedSourceRevision) {
120002
120230
  if (expectedSourceRevision) {
120003
120231
  const cached3 = this.reviewBankCache.get(`${identifier}\x00${expectedSourceRevision}`);
120004
120232
  if (cached3) {
120005
120233
  return cached3;
120006
120234
  }
120007
120235
  }
120008
- const loaded = await this.loadAssessmentSource(identifier, expectedSourceRevision);
120009
- const source = {
120010
- assessment: loaded.assessment,
120011
- bank: await buildReviewBankIndex(loaded.assessment, loaded.questions)
120012
- };
120013
- this.reviewBankCache.set(`${identifier}\x00${source.assessment.contentRevision}`, source);
120014
- return source;
120236
+ let loaded;
120237
+ try {
120238
+ loaded = await loadQtiTestReferences(this.requireClient(), identifier);
120239
+ } catch (error88) {
120240
+ if (expectedSourceRevision && isApiError(error88) && error88.statusCode === 404) {
120241
+ throw new AssessmentRuntimeError("SELECTED_TEST_VERSION_UNAVAILABLE", `The selected assessment version for ${identifier} is no longer available.`, { identifier, expectedRevision: expectedSourceRevision });
120242
+ }
120243
+ throw error88;
120244
+ }
120245
+ let bank;
120246
+ try {
120247
+ bank = await reviewBankIndexFromManifest(loaded.test.metadata, {
120248
+ bankIdentifier: identifier,
120249
+ membershipItemIdentifiers: loaded.references.questions.map((question) => question.reference.identifier)
120250
+ });
120251
+ } catch (error88) {
120252
+ throw new ServiceUnavailableError(`Review bank ${identifier} has stale or invalid routing metadata. Use Update Mapping in assessment authoring.`, { reason: errorMessage2(error88) });
120253
+ }
120254
+ if (!bank) {
120255
+ throw new ServiceUnavailableError(`Review bank ${identifier} needs its authoring mapping updated before it can serve review questions.`);
120256
+ }
120257
+ if (expectedSourceRevision && bank.sourceContentRevision !== expectedSourceRevision) {
120258
+ throw new AssessmentRuntimeError("SELECTED_TEST_VERSION_UNAVAILABLE", `The selected assessment version for ${identifier} is no longer available.`, {
120259
+ identifier,
120260
+ expectedRevision: expectedSourceRevision,
120261
+ currentRevision: bank.sourceContentRevision
120262
+ });
120263
+ }
120264
+ const catalog = { ...loaded, bank };
120265
+ this.reviewBankCache.set(`${identifier}\x00${bank.sourceContentRevision}`, catalog);
120266
+ return catalog;
120267
+ }
120268
+ async hydrateReviewBankSelection(catalog, itemIdentifiers) {
120269
+ const cacheKey2 = [
120270
+ catalog.test.identifier,
120271
+ catalog.bank.sourceContentRevision,
120272
+ ...itemIdentifiers
120273
+ ].join("\x00");
120274
+ const cached3 = this.reviewSelectionCache.get(cacheKey2);
120275
+ if (cached3) {
120276
+ return { assessment: cached3, bank: catalog.bank };
120277
+ }
120278
+ const questions = await hydrateQtiTestQuestionSelection(this.requireClient(), catalog.references, itemIdentifiers);
120279
+ const assessment = await buildPlayableAssessment(catalog.test, questions);
120280
+ assessment.contentRevision = catalog.bank.sourceContentRevision;
120281
+ this.reviewSelectionCache.set(cacheKey2, assessment);
120282
+ return { assessment, bank: catalog.bank };
120015
120283
  }
120016
120284
  pinnedReviewBankSource(source, metadata2) {
120017
120285
  return {
@@ -120042,7 +120310,8 @@ class TimebackAssessmentRuntimeService {
120042
120310
  if (metadata2.purpose !== "review") {
120043
120311
  return this.loadAssessment(metadata2.selectedTest.identifier, metadata2.selectedTest.contentRevision);
120044
120312
  }
120045
- const currentSource = await this.loadReviewBank(metadata2.selectedTest.identifier, metadata2.selectedTest.contentRevision);
120313
+ const catalog = await this.loadReviewBankCatalog(metadata2.selectedTest.identifier, metadata2.selectedTest.contentRevision);
120314
+ const currentSource = await this.hydrateReviewBankSelection(catalog, metadata2.review.selections.map((selection) => selection.itemIdentifier));
120046
120315
  const source = this.pinnedReviewBankSource(currentSource, metadata2);
120047
120316
  return projectReviewAssessment(source.assessment, source.bank, metadata2.review.selections);
120048
120317
  }
@@ -120665,6 +120934,14 @@ class TimebackAssessmentRuntimeService {
120665
120934
  return AssessmentRuntimeError.from(assessmentAttemptUnauthorized(attemptId));
120666
120935
  }
120667
120936
  async snapshotForResult(result, metadata2, integration) {
120937
+ if (isAssessmentAttemptSuperseded({
120938
+ inProgress: result.inProgress ?? "",
120939
+ scoreStatus: result.scoreStatus
120940
+ })) {
120941
+ throw new GoneError("This assessment attempt has been superseded.", {
120942
+ attemptId: result.sourcedId
120943
+ });
120944
+ }
120668
120945
  const assessment = await this.loadAttemptAssessment(metadata2);
120669
120946
  if (this.isPlatformRoutedDiagnosticMetadata(metadata2)) {
120670
120947
  const routing = await this.loadAttemptDiagnosticRouting(metadata2, assessment);
@@ -122010,6 +122287,60 @@ function buildQtiLibraryListPlan(params) {
122010
122287
  }
122011
122288
  var PLAYCADEMY_QTI_SOURCE_PREFIX = "playcademy-test-";
122012
122289
  var PLAYCADEMY_QTI_SOURCE_UPPER_BOUND = "playcademy-test.";
122290
+ function reviewMappingIssueSummary(label, identifiers) {
122291
+ if (identifiers.length === 0) {
122292
+ return null;
122293
+ }
122294
+ const displayed = identifiers.slice(0, 10).join(", ");
122295
+ const remainder = identifiers.length > 10 ? `, and ${identifiers.length - 10} more` : "";
122296
+ return `${label} (${identifiers.length}): ${displayed}${remainder}`;
122297
+ }
122298
+ async function prepareReviewMappingUpdate(test, questions) {
122299
+ const contentRevision = await assessmentContentRevision(test, questions);
122300
+ const assessment = {
122301
+ identifier: test.identifier,
122302
+ contractVersion: 1,
122303
+ contentRevision,
122304
+ title: test.title,
122305
+ items: questions.questions.map(({ question }) => ({
122306
+ identifier: question.identifier,
122307
+ title: question.title,
122308
+ prompt: "",
122309
+ maxScore: 1,
122310
+ interactions: []
122311
+ }))
122312
+ };
122313
+ const bank = await buildReviewBankIndex(assessment, questions, { customOnly: true });
122314
+ const issues = reviewBankMappingIssues(bank);
122315
+ if (issues.missingOrInvalidItemIdentifiers.length > 0 || issues.multipleStandardItemIdentifiers.length > 0) {
122316
+ const details = [
122317
+ reviewMappingIssueSummary("missing or invalid associations", issues.missingOrInvalidItemIdentifiers),
122318
+ reviewMappingIssueSummary("multiple associations", issues.multipleStandardItemIdentifiers)
122319
+ ].filter((detail) => detail !== null);
122320
+ throw new ValidationError(`Every review question must have exactly one valid standard/node association. ${details.join("; ")}.`);
122321
+ }
122322
+ const manifest = await buildReviewBankManifest(bank);
122323
+ return {
122324
+ update: {
122325
+ ...buildQtiTestUpdateInput(test, test.title),
122326
+ metadata: {
122327
+ ...test.metadata,
122328
+ [PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY]: manifest
122329
+ }
122330
+ },
122331
+ summary: {
122332
+ itemCount: bank.items.length,
122333
+ standardCount: Object.keys(manifest.itemsByStandard).length,
122334
+ sourceFingerprint: manifest.sourceFingerprint
122335
+ }
122336
+ };
122337
+ }
122338
+ var init_timeback_review_mapping_util = __esm(() => {
122339
+ init_assessment_runtime2();
122340
+ init_errors2();
122341
+ init_timeback_assessment_runtime_util();
122342
+ init_timeback_qti_authoring_util();
122343
+ });
122013
122344
  function databaseConstraintName(error88) {
122014
122345
  if (typeof error88 !== "object" || error88 === null) {
122015
122346
  return;
@@ -122091,7 +122422,8 @@ class TimebackAssessmentsService {
122091
122422
  ownerGameSlug: ownership.gameSlug,
122092
122423
  integrationId,
122093
122424
  subject: integration.subject,
122094
- grade: String(integration.grade)
122425
+ grade: String(integration.grade),
122426
+ [PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY]: emptyReviewBankManifest(input.qtiTestIdentifier)
122095
122427
  }
122096
122428
  });
122097
122429
  try {
@@ -122160,7 +122492,9 @@ class TimebackAssessmentsService {
122160
122492
  assertAssessmentHasQuestions(loaded.questions.questions);
122161
122493
  assertPlayableAssessmentImportQuestions(manifest.targetStatus, loaded.questions.questions.map(({ question }) => question));
122162
122494
  if (entry2.purpose === "review") {
122163
- assertReviewAssessmentHasStandards(loaded.questions.questions.map(({ question }) => qtiAssessmentStandardRefsFromMetadata2(question.metadata).filter((standard) => canonicalReviewStandardRef(standard) !== null).length));
122495
+ assertReviewAssessmentHasStandards(loaded.questions.questions.map(({ question }) => reviewRoutingStandardRefsFromMetadata(question.metadata, {
122496
+ customOnly: true
122497
+ }).length));
122164
122498
  }
122165
122499
  return { entry: entry2, test: loaded.test };
122166
122500
  } catch (error88) {
@@ -122312,6 +122646,10 @@ class TimebackAssessmentsService {
122312
122646
  if (input.status !== undefined) {
122313
122647
  validateAssessmentStatusTransition(row.status, input.status);
122314
122648
  }
122649
+ if (!publishing && !activatingReview && nextPurpose === "review" && row.purpose !== "review") {
122650
+ const ownership = await this.requireQtiTestOwnershipContext(integrationId, tx);
122651
+ await this.rebuildReviewMapping(this.requireClient(), qtiTestIdentifier, ownership.gameSlug);
122652
+ }
122315
122653
  if (input.title !== undefined) {
122316
122654
  assertDraftAssessment(row);
122317
122655
  assertAllAssessmentAssociationsDraft(associations);
@@ -122330,7 +122668,8 @@ class TimebackAssessmentsService {
122330
122668
  updates.diagnosticKey = nextDiagnostic.diagnosticKey;
122331
122669
  updates.diagnosticRoutingManifest = nextDiagnostic.routingManifest;
122332
122670
  } else {
122333
- await this.validateAssessmentHasQuestions(qtiTestIdentifier, nextPurpose === "review" && nextStatus === "live");
122671
+ const reviewOwnership = nextPurpose === "review" && nextStatus === "live" ? await this.requireQtiTestOwnershipContext(integrationId, tx) : undefined;
122672
+ await this.validateAssessmentHasQuestions(qtiTestIdentifier, reviewOwnership?.gameSlug);
122334
122673
  }
122335
122674
  }
122336
122675
  let updated = row;
@@ -122425,6 +122764,19 @@ class TimebackAssessmentsService {
122425
122764
  });
122426
122765
  return { ...result, questions };
122427
122766
  }
122767
+ async updateReviewMapping(integrationId, qtiTestIdentifier) {
122768
+ return this.withLockedAssessmentAssociations(integrationId, qtiTestIdentifier, async (row, tx, associations) => {
122769
+ if (!associations.some((association) => association.purpose === "review")) {
122770
+ throw new ValidationError("Review mapping is available only for standards-review assessments.");
122771
+ }
122772
+ const client2 = this.requireClient();
122773
+ const ownership = await this.requireQtiTestOwnershipContext(integrationId, tx);
122774
+ const result = await this.rebuildReviewMapping(client2, qtiTestIdentifier, ownership.gameSlug);
122775
+ setAttribute("app.assessment.operation", "update_review_mapping");
122776
+ setAttribute("app.assessment.review_mapping_item_count", result.itemCount);
122777
+ return result;
122778
+ });
122779
+ }
122428
122780
  async listQuestionLibrary(integrationId, params) {
122429
122781
  const client2 = this.requireClient();
122430
122782
  await this.requireIntegration(integrationId);
@@ -122465,7 +122817,8 @@ class TimebackAssessmentsService {
122465
122817
  integrationId,
122466
122818
  subject: integration.subject,
122467
122819
  grade: String(integration.grade),
122468
- copiedFromTestIdentifier: sourceTestIdentifier
122820
+ copiedFromTestIdentifier: sourceTestIdentifier,
122821
+ [PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY]: emptyReviewBankManifest(targetTestIdentifier)
122469
122822
  }, itemPlan);
122470
122823
  const attemptedItemIdentifiers = [];
122471
122824
  let testCreationAttempted = false;
@@ -122853,16 +123206,20 @@ class TimebackAssessmentsService {
122853
123206
  const plan = buildQtiLibraryListPlan(params);
122854
123207
  return list(plan.params);
122855
123208
  }
122856
- async validateAssessmentHasQuestions(qtiTestIdentifier, requireReviewStandards = false) {
123209
+ async validateAssessmentHasQuestions(qtiTestIdentifier, reviewGameSlug) {
122857
123210
  const client2 = this.requireClient();
122858
- const result = await client2.qtiApi.assessmentTests.getQuestions(qtiTestIdentifier);
123211
+ const [test, result] = await Promise.all([
123212
+ client2.qtiApi.assessmentTests.get(qtiTestIdentifier),
123213
+ client2.qtiApi.assessmentTests.getQuestions(qtiTestIdentifier)
123214
+ ]);
122859
123215
  assertAssessmentHasQuestions(result.questions);
122860
123216
  const hydrated = await hydrateQtiTestQuestions(client2, result);
122861
123217
  for (const { question } of hydrated.questions) {
122862
123218
  assertPlayableQtiQuestion(question);
122863
123219
  }
122864
- if (requireReviewStandards) {
122865
- assertReviewAssessmentHasStandards(hydrated.questions.map(({ question }) => qtiAssessmentStandardRefsFromMetadata2(question.metadata).filter((standard) => canonicalReviewStandardRef(standard) !== null).length));
123220
+ if (reviewGameSlug) {
123221
+ assertQtiTestOwnedByGame(test, reviewGameSlug);
123222
+ await this.writeReviewMapping(client2, test, hydrated);
122866
123223
  }
122867
123224
  }
122868
123225
  async prepareDiagnosticAssociationChanges(row, nextPurpose, requested) {
@@ -122927,6 +123284,19 @@ class TimebackAssessmentsService {
122927
123284
  routingManifest: validation.manifest
122928
123285
  };
122929
123286
  }
123287
+ async rebuildReviewMapping(client2, qtiTestIdentifier, gameSlug) {
123288
+ const [test, references] = await Promise.all([
123289
+ client2.qtiApi.assessmentTests.get(qtiTestIdentifier),
123290
+ client2.qtiApi.assessmentTests.getQuestions(qtiTestIdentifier)
123291
+ ]);
123292
+ assertQtiTestOwnedByGame(test, gameSlug);
123293
+ return this.writeReviewMapping(client2, test, await hydrateQtiTestQuestions(client2, references));
123294
+ }
123295
+ async writeReviewMapping(client2, test, questions) {
123296
+ const prepared = await prepareReviewMappingUpdate(test, questions);
123297
+ await client2.qtiApi.assessmentTests.update(test.identifier, prepared.update);
123298
+ return prepared.summary;
123299
+ }
122930
123300
  }
122931
123301
  var init_timeback_assessments_service = __esm(async () => {
122932
123302
  init_drizzle_orm();
@@ -122934,7 +123304,6 @@ var init_timeback_assessments_service = __esm(async () => {
122934
123304
  init_tables_index();
122935
123305
  init_spans();
122936
123306
  init_assessment_runtime2();
122937
- init_qti();
122938
123307
  init_timeback3();
122939
123308
  init_errors2();
122940
123309
  init_timeback_assessment_import_util();
@@ -122942,6 +123311,7 @@ var init_timeback_assessments_service = __esm(async () => {
122942
123311
  init_timeback_assessment_runtime_util();
122943
123312
  init_timeback_qti_authoring_util();
122944
123313
  init_timeback_qti_hydration_util();
123314
+ init_timeback_review_mapping_util();
122945
123315
  await init_errors8();
122946
123316
  });
122947
123317
  function buildTimebackBaseConfigFromExistingConfig(config5) {
@@ -185557,6 +185927,7 @@ var listAssessments;
185557
185927
  var createAssessment;
185558
185928
  var attachExistingAssessments;
185559
185929
  var updateAssessment;
185930
+ var updateReviewMapping;
185560
185931
  var reorderAssessments;
185561
185932
  var reorderQuestions;
185562
185933
  var removeAssessment;
@@ -186241,6 +186612,14 @@ var init_timeback_controller = __esm(() => {
186241
186612
  const body2 = await parseRequestBody(ctx.request, UpdateAssessmentRequestSchema);
186242
186613
  return ctx.services.timebackAssessments.updateAssessment(integrationId, testIdentifier, body2);
186243
186614
  });
186615
+ updateReviewMapping = requireDeveloper(async (ctx) => {
186616
+ const { gameId, courseId, testIdentifier } = ctx.params;
186617
+ if (!gameId || !courseId || !testIdentifier) {
186618
+ throw ApiError.badRequest("Missing gameId, courseId, or testIdentifier parameter");
186619
+ }
186620
+ const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
186621
+ return ctx.services.timebackAssessments.updateReviewMapping(integrationId, testIdentifier);
186622
+ });
186244
186623
  reorderAssessments = requireDeveloper(async (ctx) => {
186245
186624
  const { gameId, courseId } = ctx.params;
186246
186625
  if (!gameId || !courseId) {
@@ -186383,6 +186762,7 @@ var init_timeback_controller = __esm(() => {
186383
186762
  createAssessment,
186384
186763
  attachExistingAssessments,
186385
186764
  updateAssessment,
186765
+ updateReviewMapping,
186386
186766
  reorderAssessments,
186387
186767
  removeAssessment,
186388
186768
  reorderQuestions,
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcademy/vite-plugin",
3
- "version": "1.2.1-beta.24",
3
+ "version": "1.2.1-beta.25",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -19,14 +19,14 @@
19
19
  "dependencies": {
20
20
  "archiver": "^7.0.1",
21
21
  "picocolors": "^1.1.1",
22
- "playcademy": "0.28.1-beta.24"
22
+ "playcademy": "0.28.1-beta.25"
23
23
  },
24
24
  "devDependencies": {
25
25
  "@electric-sql/pglite": "^0.3.16",
26
26
  "@inquirer/prompts": "^7.8.6",
27
27
  "@playcademy/constants": "0.0.1",
28
- "@playcademy/sandbox": "0.7.1-beta.22",
29
- "@playcademy/sdk": "0.16.1-beta.23",
28
+ "@playcademy/sandbox": "0.7.1-beta.23",
29
+ "@playcademy/sdk": "0.16.1-beta.24",
30
30
  "@playcademy/types": "0.0.1",
31
31
  "@playcademy/utils": "0.0.1",
32
32
  "@types/archiver": "^6.0.3",