@playcademy/vite-plugin 1.3.2-beta.1 → 1.3.2-beta.3

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 +1850 -497
  2. package/package.json +4 -4
package/dist/index.js CHANGED
@@ -24317,7 +24317,7 @@ import path2 from "node:path";
24317
24317
  // package.json
24318
24318
  var package_default = {
24319
24319
  name: "@playcademy/vite-plugin",
24320
- version: "1.3.2-beta.1",
24320
+ version: "1.3.2-beta.3",
24321
24321
  type: "module",
24322
24322
  exports: {
24323
24323
  ".": {
@@ -25934,7 +25934,7 @@ var package_default2;
25934
25934
  var init_package = __esm(() => {
25935
25935
  package_default2 = {
25936
25936
  name: "@playcademy/sandbox",
25937
- version: "0.8.1-beta.4",
25937
+ version: "0.8.0",
25938
25938
  description: "Local development server for Playcademy game development",
25939
25939
  type: "module",
25940
25940
  exports: {
@@ -34232,7 +34232,18 @@ function qtiDirectedPairValidationIssue(values, contract) {
34232
34232
  }
34233
34233
  return null;
34234
34234
  }
34235
- function isRecord2(value) {
34235
+ function validNumericMatchValue(value, baseType) {
34236
+ const syntax = baseType === "integer" ? /^[+-]?\d+$/ : /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i;
34237
+ if (!syntax.test(value.trim())) {
34238
+ return false;
34239
+ }
34240
+ const numeric = Number(value);
34241
+ return Number.isFinite(numeric) && (baseType === "float" || Number.isSafeInteger(numeric));
34242
+ }
34243
+ function sameScore(left, right) {
34244
+ return Number.isFinite(left) && Number.isFinite(right) && Math.abs(left - right) <= Number.EPSILON * Math.max(1, Math.abs(left), Math.abs(right));
34245
+ }
34246
+ function isRecord3(value) {
34236
34247
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
34237
34248
  }
34238
34249
  function stringField(value) {
@@ -34327,13 +34338,13 @@ function dedupeStandards(standards) {
34327
34338
  return [...deduped.values()];
34328
34339
  }
34329
34340
  function qtiAlignmentStandardsFromMetadata(metadata2) {
34330
- if (!isRecord2(metadata2)) {
34341
+ if (!isRecord3(metadata2)) {
34331
34342
  return [];
34332
34343
  }
34333
34344
  const standards = [];
34334
34345
  const alignmentGroups = Array.isArray(metadata2.alignment) ? metadata2.alignment : [];
34335
34346
  for (const group of alignmentGroups) {
34336
- if (isRecord2(group)) {
34347
+ if (isRecord3(group)) {
34337
34348
  const source = stringField(group.curriculum) || "QTI";
34338
34349
  const domains2 = Array.isArray(group.domains) ? group.domains : [];
34339
34350
  if (!domains2.length) {
@@ -34351,11 +34362,11 @@ function qtiAlignmentStandardsFromMetadata(metadata2) {
34351
34362
  }
34352
34363
  } else {
34353
34364
  for (const domain of domains2) {
34354
- if (isRecord2(domain)) {
34365
+ if (isRecord3(domain)) {
34355
34366
  const domainName = stringField(domain.name) || undefined;
34356
34367
  const domainStandards = Array.isArray(domain.standards) ? domain.standards : [];
34357
34368
  for (const standard of domainStandards) {
34358
- if (isRecord2(standard)) {
34369
+ if (isRecord3(standard)) {
34359
34370
  const identifier = stringField(standard.identifier).trim();
34360
34371
  const id = stringField(standard.id).trim() || identifier;
34361
34372
  if (id) {
@@ -34406,12 +34417,9 @@ function isAssessmentAttemptSuperseded(attempt) {
34406
34417
  return attempt.inProgress === ASSESSMENT_ATTEMPT_SUPERSEDED.inProgress && attempt.scoreStatus === ASSESSMENT_ATTEMPT_SUPERSEDED.scoreStatus;
34407
34418
  }
34408
34419
  function classifyAssessmentSubmission(attempt, submissionId) {
34409
- if (isAssessmentAttemptAwaitingAward(attempt)) {
34420
+ if (isAssessmentAttemptAwaitingAward(attempt) || isAssessmentAttemptCompleted(attempt)) {
34410
34421
  return attempt.submissionId === submissionId ? "replay" : "reject";
34411
34422
  }
34412
- if (isAssessmentAttemptCompleted(attempt)) {
34413
- return "reject";
34414
- }
34415
34423
  return isAssessmentAttemptOpen(attempt) ? "submit" : "reject";
34416
34424
  }
34417
34425
  function isSameAssessmentAward(recorded, input) {
@@ -34503,10 +34511,13 @@ function canonicalJson(value) {
34503
34511
  const entries = Object.entries(value).filter(([, entryValue]) => entryValue !== undefined).toSorted(([left], [right]) => compareCodeUnits(left, right)).map(([key, entryValue]) => `${JSON.stringify(key)}:${canonicalJson(entryValue)}`);
34504
34512
  return `{${entries.join(",")}}`;
34505
34513
  }
34506
- async function diagnosticRoutingRevision(manifest) {
34507
- const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(canonicalJson(manifest)));
34514
+ async function canonicalJsonSha256(value) {
34515
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(canonicalJson(value)));
34508
34516
  return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
34509
34517
  }
34518
+ function diagnosticRoutingRevision(manifest) {
34519
+ return canonicalJsonSha256(manifest);
34520
+ }
34510
34521
  function issue(severity, code, message, path3, details) {
34511
34522
  return { severity, code, message, ...path3 ? { path: path3 } : {}, ...details ? { details } : {} };
34512
34523
  }
@@ -35563,11 +35574,11 @@ function itemResponsesEqual(left, right) {
35563
35574
  function itemResponseUpdateMatches(current, update) {
35564
35575
  return itemResponsesEqual(current, applyAssessmentItemResponseUpdate(current, update));
35565
35576
  }
35566
- function prepareAssessmentItemSubmission(state, input) {
35577
+ function resolveAssessmentItemReplay(state, input) {
35567
35578
  const flow = assessmentFlowForPurpose(state.purpose);
35568
35579
  if (flow !== "item-submit") {
35569
35580
  return {
35570
- action: "reject",
35581
+ action: "conflict",
35571
35582
  failure: assessmentFlowViolation("Items cannot be submitted individually in this flow.", { flow })
35572
35583
  };
35573
35584
  }
@@ -35576,7 +35587,7 @@ function prepareAssessmentItemSubmission(state, input) {
35576
35587
  const sameRequest = priorSubmission.itemIdentifier === input.itemIdentifier && itemResponseUpdateMatches(state.responses[input.itemIdentifier], input.responses);
35577
35588
  if (!sameRequest) {
35578
35589
  return {
35579
- action: "reject",
35590
+ action: "conflict",
35580
35591
  failure: assessmentFlowViolation("This item submission ID was already used for a different request.", {
35581
35592
  submissionId: input.submissionId,
35582
35593
  itemIdentifier: input.itemIdentifier,
@@ -35592,6 +35603,16 @@ function prepareAssessmentItemSubmission(state, input) {
35592
35603
  submission: priorSubmission
35593
35604
  };
35594
35605
  }
35606
+ return { action: "missing" };
35607
+ }
35608
+ function prepareAssessmentItemSubmission(state, input) {
35609
+ const replay = resolveAssessmentItemReplay(state, input);
35610
+ if (replay.action === "conflict") {
35611
+ return { action: "reject", failure: replay.failure };
35612
+ }
35613
+ if (replay.action === "replay") {
35614
+ return replay;
35615
+ }
35595
35616
  if (state.responseVersion !== input.expectedResponseVersion) {
35596
35617
  return {
35597
35618
  action: "reject",
@@ -35607,7 +35628,7 @@ function prepareAssessmentItemSubmission(state, input) {
35607
35628
  failure: assessmentFlowViolation("Items must be submitted once and in candidate order.", {
35608
35629
  itemIdentifier: input.itemIdentifier,
35609
35630
  previousItemIdentifier: previousItemIdentifier ?? null,
35610
- flow
35631
+ flow: assessmentFlowForPurpose(state.purpose)
35611
35632
  })
35612
35633
  };
35613
35634
  }
@@ -35637,7 +35658,8 @@ function completeAssessmentItemSubmission(state, input, prepared, scoring, submi
35637
35658
  responseVersion: prepared.responseVersion,
35638
35659
  answered: prepared.answered,
35639
35660
  score: scoring.score,
35640
- isCorrect: scoring.isCorrect
35661
+ isCorrect: scoring.isCorrect,
35662
+ ...scoring.grading ? { grading: scoring.grading } : {}
35641
35663
  };
35642
35664
  return {
35643
35665
  responseVersion: prepared.responseVersion,
@@ -35792,6 +35814,145 @@ function assessmentPresentationForAttempt(assessment, attemptId) {
35792
35814
  });
35793
35815
  return { ...assessment, items };
35794
35816
  }
35817
+ function normalizedMatchText(value, baseType) {
35818
+ const normalized = value.replace(ZERO_WIDTH_CHARACTERS, "").replace(CURLY_SINGLE_QUOTES, "'").replace(CURLY_DOUBLE_QUOTES, '"').replace(UNICODE_DASHES, "-");
35819
+ return baseType === "directedPair" ? normalized : normalized.replace(/[\s\u00A0]+/g, " ").trim();
35820
+ }
35821
+ function normalizedArithmeticText(value) {
35822
+ return value.replace(THOUSANDS_GROUP, (number) => number.replaceAll(",", "")).replace(ARITHMETIC_OPERATOR_SPACING, "$1").replaceAll("÷", "/").replace(/[\u00D7\u00B7]/g, "*");
35823
+ }
35824
+ function parsedMatchValue(value, baseType) {
35825
+ if (typeof value !== "string") {
35826
+ return null;
35827
+ }
35828
+ const normalized = normalizedMatchText(value, baseType);
35829
+ if (baseType === "identifier" && !IDENTIFIER_VALUE.test(normalized)) {
35830
+ return null;
35831
+ }
35832
+ if (baseType === "directedPair" && !DIRECTED_PAIR_VALUE.test(normalized)) {
35833
+ return null;
35834
+ }
35835
+ return normalized;
35836
+ }
35837
+ function matchValuesEqual(left, right, baseType) {
35838
+ return left === right || baseType !== "identifier" && baseType !== "directedPair" && normalizedArithmeticText(left) === normalizedArithmeticText(right);
35839
+ }
35840
+ function parsedMatchValues(values, baseType) {
35841
+ const parsed = values.map((value) => parsedMatchValue(value, baseType));
35842
+ return parsed.every((value) => value !== null) ? parsed : null;
35843
+ }
35844
+ function responseValues2(value) {
35845
+ if (typeof value === "string") {
35846
+ return [value];
35847
+ }
35848
+ return Array.isArray(value) ? value : null;
35849
+ }
35850
+ function unorderedMatch(responses, correct, baseType) {
35851
+ if (responses.length !== correct.length) {
35852
+ return false;
35853
+ }
35854
+ const matched = new Set;
35855
+ for (const response of responses) {
35856
+ const match = correct.findIndex((candidate, index) => !matched.has(index) && matchValuesEqual(response, candidate, baseType));
35857
+ if (match === -1) {
35858
+ return false;
35859
+ }
35860
+ matched.add(match);
35861
+ }
35862
+ return true;
35863
+ }
35864
+ function matchComparisonCorrect(rule, values) {
35865
+ if (rule.comparison.kind !== "match" || rule.comparison.correctValues.length === 0) {
35866
+ return false;
35867
+ }
35868
+ const responses = parsedMatchValues(values, rule.baseType);
35869
+ const correct = parsedMatchValues(rule.comparison.correctValues, rule.baseType);
35870
+ if (!responses || !correct) {
35871
+ return false;
35872
+ }
35873
+ if (rule.cardinality === "single") {
35874
+ if (responses.length !== 1) {
35875
+ return false;
35876
+ }
35877
+ if (rule.baseType === "string") {
35878
+ return correct.some((candidate) => matchValuesEqual(responses[0], candidate, rule.baseType));
35879
+ }
35880
+ return correct.length === 1 && matchValuesEqual(responses[0], correct[0], rule.baseType);
35881
+ }
35882
+ if (responses.length !== correct.length) {
35883
+ return false;
35884
+ }
35885
+ if (rule.cardinality === "ordered") {
35886
+ return responses.every((value, index) => matchValuesEqual(value, correct[index], rule.baseType));
35887
+ }
35888
+ return unorderedMatch(responses, correct, rule.baseType);
35889
+ }
35890
+ function roundedNumericValue(value, roundingMode, figures) {
35891
+ if (!Number.isFinite(value) || !Number.isSafeInteger(figures) || (roundingMode === "decimalPlaces" ? figures < 0 : figures <= 0)) {
35892
+ return null;
35893
+ }
35894
+ if (roundingMode === "decimalPlaces") {
35895
+ const multiplier2 = 10 ** figures;
35896
+ const rounded2 = Math.round(value * multiplier2) / multiplier2;
35897
+ return Number.isFinite(rounded2) ? rounded2 : null;
35898
+ }
35899
+ if (value === 0) {
35900
+ return 0;
35901
+ }
35902
+ const magnitude = Math.floor(Math.log10(Math.abs(value)));
35903
+ const multiplier = 10 ** (figures - magnitude - 1);
35904
+ const rounded = Math.round(value * multiplier) / multiplier;
35905
+ return Number.isFinite(rounded) ? rounded : null;
35906
+ }
35907
+ function numericResponseValue(value) {
35908
+ if (typeof value !== "string") {
35909
+ return null;
35910
+ }
35911
+ const numeric = Number(value);
35912
+ return Number.isFinite(numeric) ? numeric : null;
35913
+ }
35914
+ function numericComparisonCorrect(rule, values) {
35915
+ if (values.length !== 1 || rule.cardinality !== "single" || rule.baseType !== "integer" && rule.baseType !== "float" || rule.comparison.kind === "match") {
35916
+ return false;
35917
+ }
35918
+ const response = numericResponseValue(values[0]);
35919
+ const correct = rule.comparison.correctValue;
35920
+ if (response === null || !Number.isFinite(correct) || rule.baseType === "integer" && !Number.isSafeInteger(correct)) {
35921
+ return false;
35922
+ }
35923
+ if (rule.comparison.kind === "numeric-equal") {
35924
+ return response === correct;
35925
+ }
35926
+ const { roundingMode, figures } = rule.comparison;
35927
+ const roundedResponse = roundedNumericValue(response, roundingMode, figures);
35928
+ const roundedCorrect = roundedNumericValue(correct, roundingMode, figures);
35929
+ return roundedResponse !== null && roundedCorrect !== null && roundedResponse === roundedCorrect;
35930
+ }
35931
+ function ruleCorrect(rule, responses) {
35932
+ const response = responses?.[rule.responseIdentifier];
35933
+ if (response === undefined) {
35934
+ return false;
35935
+ }
35936
+ const values = responseValues2(response);
35937
+ if (!values) {
35938
+ return false;
35939
+ }
35940
+ return rule.comparison.kind === "match" ? matchComparisonCorrect(rule, values) : numericComparisonCorrect(rule, values);
35941
+ }
35942
+ function scorePlatformAssessmentItem(item, responses) {
35943
+ const verdicts = item.rules.map((rule) => ruleCorrect(rule, responses));
35944
+ const rawEarned = item.rules.reduce((total, rule, index) => total + (verdicts[index] ? rule.points : 0), 0);
35945
+ const earned = Math.min(item.maxScore, Math.max(0, rawEarned));
35946
+ return {
35947
+ itemIdentifier: item.itemIdentifier,
35948
+ score: {
35949
+ earned,
35950
+ possible: item.maxScore,
35951
+ normalized: item.maxScore <= 0 ? 0 : Math.min(1, Math.max(0, earned / item.maxScore))
35952
+ },
35953
+ isCorrect: verdicts.length > 0 && verdicts.every(Boolean)
35954
+ };
35955
+ }
35795
35956
  function reviewStandardFieldsWithinLimits(input) {
35796
35957
  return input.framework.trim().length <= TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.standardFieldLength && input.identifier.trim().length <= TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.standardFieldLength;
35797
35958
  }
@@ -36278,6 +36439,64 @@ function assessmentItemEarnedScore(interactionPoints, maxScore) {
36278
36439
  const earned = interactionPoints.reduce((sum, points) => sum + Math.max(0, points), 0);
36279
36440
  return Math.min(earned, maxScore);
36280
36441
  }
36442
+ async function assessmentChildResultPayloadHash(payload) {
36443
+ return canonicalJsonSha256({
36444
+ kind: ASSESSMENT_CHILD_RESULT_KIND,
36445
+ version: ASSESSMENT_CHILD_RESULT_VERSION,
36446
+ payload
36447
+ });
36448
+ }
36449
+ async function buildAssessmentChildResultCommand(payload, recordedAt) {
36450
+ const parsedPayload = AssessmentChildResultPayloadV1Schema.parse(payload);
36451
+ const command = {
36452
+ kind: ASSESSMENT_CHILD_RESULT_KIND,
36453
+ version: ASSESSMENT_CHILD_RESULT_VERSION,
36454
+ recordedAt,
36455
+ payloadHash: await assessmentChildResultPayloadHash(parsedPayload),
36456
+ payload: parsedPayload
36457
+ };
36458
+ return AssessmentChildResultCommandV1Schema.parse(command);
36459
+ }
36460
+ function normalizeOmittedResponses(value) {
36461
+ if (!isRecord(value) || !isRecord(value.payload) || value.payload.responses !== undefined) {
36462
+ return value;
36463
+ }
36464
+ return {
36465
+ ...value,
36466
+ payload: {
36467
+ ...value.payload,
36468
+ responses: {}
36469
+ }
36470
+ };
36471
+ }
36472
+ function parseAssessmentChildResultCommand(value) {
36473
+ const direct = AssessmentChildResultCommandV1Schema.safeParse(normalizeOmittedResponses(value));
36474
+ if (direct.success) {
36475
+ return direct.data;
36476
+ }
36477
+ if (!isRecord(value)) {
36478
+ return null;
36479
+ }
36480
+ const nested = AssessmentChildResultCommandV1Schema.safeParse(normalizeOmittedResponses(value[PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY]));
36481
+ return nested.success ? nested.data : null;
36482
+ }
36483
+ async function verifyAssessmentChildResultCommand(metadataOrCommand) {
36484
+ const command = parseAssessmentChildResultCommand(metadataOrCommand);
36485
+ if (!command) {
36486
+ return { ok: false, reason: "malformed" };
36487
+ }
36488
+ if (await assessmentChildResultPayloadHash(command.payload) !== command.payloadHash) {
36489
+ return { ok: false, reason: "payload_hash_mismatch" };
36490
+ }
36491
+ return { ok: true, command };
36492
+ }
36493
+ function assessmentChildResultId(attemptId, submissionId) {
36494
+ return deterministicUUID([
36495
+ ASSESSMENT_CHILD_RESULT_ID_NAMESPACE,
36496
+ IdentitySchema.parse(attemptId),
36497
+ IdentitySchema.parse(submissionId)
36498
+ ].join("\x00"));
36499
+ }
36281
36500
  function normalizeMasteryStandard(input) {
36282
36501
  const standard = canonicalReviewStandardRef(input);
36283
36502
  if (!standard) {
@@ -36318,7 +36537,19 @@ function isReviewItemOutcome(value) {
36318
36537
  return isRecord(value) && typeof value.itemIdentifier === "string" && value.itemIdentifier.length > 0 && isAssessmentStandardRef(value.standard) && typeof value.answered === "boolean" && isAssessmentScore(value.score) && (value.isCorrect === null || typeof value.isCorrect === "boolean");
36319
36538
  }
36320
36539
  function isAssessmentItemSubmission(value) {
36321
- return isRecord(value) && typeof value.submissionId === "string" && value.submissionId.length > 0 && typeof value.itemIdentifier === "string" && value.itemIdentifier.length > 0 && typeof value.submittedAt === "string" && Number.isInteger(value.responseVersion) && typeof value.answered === "boolean" && isAssessmentScore(value.score) && (value.isCorrect === null || typeof value.isCorrect === "boolean");
36540
+ return isRecord(value) && typeof value.submissionId === "string" && value.submissionId.length > 0 && typeof value.itemIdentifier === "string" && value.itemIdentifier.length > 0 && typeof value.submittedAt === "string" && Number.isInteger(value.responseVersion) && typeof value.answered === "boolean" && isAssessmentScore(value.score) && (value.isCorrect === null || typeof value.isCorrect === "boolean") && (value.grading === undefined || isAssessmentItemGrading(value.grading));
36541
+ }
36542
+ function isAssessmentItemGrading(value) {
36543
+ if (!isRecord(value) || !isNonEmptyString(value.graderVersion)) {
36544
+ return false;
36545
+ }
36546
+ if (value.source === "timeback-qti") {
36547
+ return true;
36548
+ }
36549
+ if (value.source === "platform-qti-adapter") {
36550
+ return isNonEmptyString(value.qtiGraderVersion);
36551
+ }
36552
+ return value.source === "platform-artifact" && isNonEmptyString(value.artifactVersion);
36322
36553
  }
36323
36554
  function isDiagnosticTrackResult(value) {
36324
36555
  return isRecord(value) && isNonEmptyString(value.trackKey) && (value.groupKey === undefined || isNonEmptyString(value.groupKey)) && DIAGNOSTIC_TRACK_OUTCOMES.some((outcome) => outcome === value.outcome) && isNonEmptyString(value.resultKey);
@@ -36352,8 +36583,11 @@ function isDiagnosticRoutingState(value) {
36352
36583
  const nextValid = value.next === null || isRecord(value.next) && isNonEmptyString(value.next.stageKey) && isNonEmptyString(value.next.trackKey) && isNonEmptyString(value.next.nodeKey) && isNonEmptyString(value.next.itemIdentifier);
36353
36584
  return stagesValid && tracksValid && nextValid && (value.status === "in-progress" && value.next !== null || value.status === "ready-to-complete" && value.next === null);
36354
36585
  }
36586
+ function isDiagnosticRoutingSnapshot(value, revision) {
36587
+ return isRecord(value) && value.kind === "platform-routed-diagnostic" && value.revision === revision && (value.status === "in-progress" || value.status === "ready-to-complete") && (value.next === null || isRecord(value.next) && isNonEmptyString(value.next.stageKey) && isNonEmptyString(value.next.trackKey) && isNonEmptyString(value.next.nodeKey) && isNonEmptyString(value.next.itemIdentifier)) && Array.isArray(value.tracks) && value.tracks.every((track) => isRecord(track) && isNonEmptyString(track.trackKey) && (track.groupKey === undefined || isNonEmptyString(track.groupKey)) && ["pending", "active", "completed", "skipped"].includes(String(track.status)) && Number.isInteger(track.administeredCount) && Number(track.administeredCount) >= 0);
36588
+ }
36355
36589
  function isRoutedDiagnosticAttemptMetadata(value, itemSubmissions, responseVersion) {
36356
- if (!isRecord(value) || !isNonEmptyString(value.definitionId) || !isNonEmptyString(value.diagnosticKey) || !isNonEmptyString(value.testName) || !isNonEmptyString(value.routingRevision) || !Array.isArray(value.ledger) || !value.ledger.every(isDiagnosticRoutingLedgerEntry) || !isDiagnosticRoutingState(value.state) || value.ledger.length !== itemSubmissions.length || responseVersion !== itemSubmissions.length) {
36590
+ if (!isRecord(value) || !isNonEmptyString(value.definitionId) || !isNonEmptyString(value.diagnosticKey) || !isNonEmptyString(value.testName) || !isNonEmptyString(value.routingRevision) || !Array.isArray(value.ledger) || !value.ledger.every(isDiagnosticRoutingLedgerEntry) || value.routingSnapshots !== undefined && (!Array.isArray(value.routingSnapshots) || value.routingSnapshots.length !== value.ledger.length || !value.routingSnapshots.every((snapshot) => isDiagnosticRoutingSnapshot(snapshot, String(value.routingRevision)))) || !isDiagnosticRoutingState(value.state) || value.ledger.length !== itemSubmissions.length || responseVersion !== itemSubmissions.length) {
36357
36591
  return false;
36358
36592
  }
36359
36593
  const itemIdentifiers = new Set;
@@ -36439,6 +36673,12 @@ function playcademyAssessmentResultMetadata(value) {
36439
36673
  diagnostic: { ...normalized.diagnostic, ledger: [] }
36440
36674
  };
36441
36675
  }
36676
+ if (isRecord(normalized.diagnostic) && Array.isArray(normalized.diagnostic.ledger) && normalized.diagnostic.ledger.length === 0 && normalized.diagnostic.routingSnapshots === undefined) {
36677
+ normalized = {
36678
+ ...normalized,
36679
+ diagnostic: { ...normalized.diagnostic, routingSnapshots: [] }
36680
+ };
36681
+ }
36442
36682
  if (isRecord(normalized.diagnostic) && isRecord(normalized.diagnostic.state)) {
36443
36683
  const state = normalized.diagnostic.state;
36444
36684
  const tracks = Array.isArray(state.tracks) ? state.tracks.map((track) => isRecord(track) ? {
@@ -36446,10 +36686,12 @@ function playcademyAssessmentResultMetadata(value) {
36446
36686
  currentNodeKey: track.currentNodeKey ?? null,
36447
36687
  terminal: track.terminal ?? null
36448
36688
  } : track) : state.tracks;
36689
+ const routingSnapshots = Array.isArray(normalized.diagnostic.routingSnapshots) ? normalized.diagnostic.routingSnapshots.map((snapshot) => isRecord(snapshot) ? { ...snapshot, next: snapshot.next ?? null } : snapshot) : normalized.diagnostic.routingSnapshots;
36449
36690
  normalized = {
36450
36691
  ...normalized,
36451
36692
  diagnostic: {
36452
36693
  ...normalized.diagnostic,
36694
+ ...routingSnapshots === undefined ? {} : { routingSnapshots },
36453
36695
  state: {
36454
36696
  ...state,
36455
36697
  ...tracks === undefined ? {} : { tracks },
@@ -36526,6 +36768,20 @@ var RESPONSE_CARDINALITIES;
36526
36768
  var RESPONSE_BASE_TYPES;
36527
36769
  var POINT_INTERACTION_TYPES;
36528
36770
  var INDEPENDENT_PROCESSING_TAGS;
36771
+ var PRIVATE_PLAYABLE_KEYS;
36772
+ var trimmedNonEmptyString;
36773
+ var finiteNumber2;
36774
+ var positiveFiniteNumber;
36775
+ var IDENTIFIER_VALUE;
36776
+ var DIRECTED_PAIR_VALUE;
36777
+ var scoringMatchComparisonSchema;
36778
+ var scoringNumericEqualComparisonSchema;
36779
+ var scoringNumericEqualRoundedComparisonSchema;
36780
+ var scoringRuleSchema;
36781
+ var scoringRulesSchema;
36782
+ var scoringItemSchema;
36783
+ var assessmentScoringArtifactItemsSchema;
36784
+ var assessmentScoringArtifactSchema;
36529
36785
  var SUPPORTED_BASE_TYPES;
36530
36786
  var SUPPORTED_CARDINALITIES;
36531
36787
  var MATCH_CORRECT_TEMPLATES;
@@ -36555,19 +36811,45 @@ var DiagnosticRoutingTransitionSchema;
36555
36811
  var DiagnosticRoutingManifestV1Schema;
36556
36812
  var GRADE_VALUES;
36557
36813
  var POINT_RESPONSE_PATTERN;
36814
+ var ZERO_WIDTH_CHARACTERS;
36815
+ var CURLY_SINGLE_QUOTES;
36816
+ var CURLY_DOUBLE_QUOTES;
36817
+ var UNICODE_DASHES;
36818
+ var ARITHMETIC_OPERATOR_SPACING;
36819
+ var THOUSANDS_GROUP;
36558
36820
  var REVIEW_SELECTION_POLICY_VERSION = "unseen-first-least-recently-delivered-v1";
36559
36821
  var PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY = "playcademyReviewBank";
36560
36822
  var PLAYCADEMY_REVIEW_BANK_MANIFEST_VERSION = 1;
36561
36823
  var DEFAULT_REVIEW_SELECTION_POLICY;
36562
36824
  var ASSESSMENT_RUNTIME_FIXTURE_BUNDLE_VERSION = 8;
36825
+ var ASSESSMENT_CHILD_RESULT_KIND = "assessment-child-result";
36826
+ var ASSESSMENT_CHILD_RESULT_VERSION = 1;
36827
+ var ASSESSMENT_CHILD_RESULT_ID_NAMESPACE = "playcademy:assessment-child-result:v1";
36828
+ var IdentitySchema;
36829
+ var RecordedAtSchema;
36830
+ var ResponseKeySchema;
36831
+ var AssessmentResponseValueSchema;
36832
+ var AssessmentScoreSchema;
36833
+ var AssessmentStandardRefSchema;
36834
+ var DiagnosticRoutingLedgerEntryV1Schema;
36835
+ var AssessmentChildResultAttemptV1Schema;
36836
+ var AssessmentChildResultAdministrationV1Schema;
36837
+ var AssessmentChildResultGradingV1Schema;
36838
+ var ReviewChildResultContextV1Schema;
36839
+ var DiagnosticChildResultContextV1Schema;
36840
+ var CommonPayloadShape;
36841
+ var ReviewChildResultPayloadV1Schema;
36842
+ var DiagnosticChildResultPayloadV1Schema;
36843
+ var AssessmentChildResultPayloadV1Schema;
36844
+ var AssessmentChildResultCommandV1Schema;
36563
36845
  var RuntimeSubjectSchema;
36564
36846
  var RuntimeGradeSchema;
36565
36847
  var OptionalQueryGradeSchema;
36566
- var AssessmentResponseValueSchema;
36848
+ var AssessmentResponseValueSchema2;
36567
36849
  var AssessmentRuntimeIdentitySchema;
36568
- var ResponseKeySchema;
36850
+ var ResponseKeySchema2;
36569
36851
  var StartAssessmentBaseSchema;
36570
- var AssessmentStandardRefSchema;
36852
+ var AssessmentStandardRefSchema2;
36571
36853
  var LatestAssessmentFilterBaseSchema;
36572
36854
  var LatestAssessmentFiltersSchema;
36573
36855
  var LatestRuntimeAssessmentQuerySchema;
@@ -36592,6 +36874,7 @@ var init_assessment_runtime2 = __esm(() => {
36592
36874
  init_timeback3();
36593
36875
  init_timeback3();
36594
36876
  init_esm();
36877
+ init_esm();
36595
36878
  init_timeback4();
36596
36879
  init_src();
36597
36880
  init_timeback3();
@@ -36599,6 +36882,9 @@ var init_assessment_runtime2 = __esm(() => {
36599
36882
  init_uuid();
36600
36883
  init_src();
36601
36884
  init_uuid();
36885
+ init_esm();
36886
+ init_src();
36887
+ init_uuid();
36602
36888
  init_src();
36603
36889
  init_timeback4();
36604
36890
  init_esm();
@@ -36737,6 +37023,93 @@ var init_assessment_runtime2 = __esm(() => {
36737
37023
  "qti-sum",
36738
37024
  "qti-base-value"
36739
37025
  ]);
37026
+ PRIVATE_PLAYABLE_KEYS = new Set([
37027
+ "answer",
37028
+ "answerkey",
37029
+ "correctanswer",
37030
+ "correctresponse",
37031
+ "correctresponses",
37032
+ "correct",
37033
+ "correctvalue",
37034
+ "correctvalues",
37035
+ "iscorrect",
37036
+ "responseprocessing",
37037
+ "rule",
37038
+ "rules",
37039
+ "scoring",
37040
+ "scoringrules",
37041
+ "solution",
37042
+ "expected",
37043
+ "expectedanswer",
37044
+ "expectedvalue",
37045
+ "expectedvalues"
37046
+ ]);
37047
+ trimmedNonEmptyString = exports_external.string().min(1).refine((value) => value === value.trim());
37048
+ finiteNumber2 = exports_external.number().finite();
37049
+ positiveFiniteNumber = finiteNumber2.positive();
37050
+ IDENTIFIER_VALUE = /^[A-Za-z_][\w.-]*$/;
37051
+ DIRECTED_PAIR_VALUE = /^[A-Za-z_][\w.-]* [A-Za-z_][\w.-]*$/;
37052
+ scoringMatchComparisonSchema = exports_external.object({
37053
+ kind: exports_external.literal("match"),
37054
+ correctValues: exports_external.array(trimmedNonEmptyString).min(1).refine((values) => new Set(values).size === values.length)
37055
+ }).strict();
37056
+ scoringNumericEqualComparisonSchema = exports_external.object({ kind: exports_external.literal("numeric-equal"), correctValue: finiteNumber2 }).strict();
37057
+ scoringNumericEqualRoundedComparisonSchema = exports_external.object({
37058
+ kind: exports_external.literal("numeric-equal-rounded"),
37059
+ correctValue: finiteNumber2,
37060
+ roundingMode: exports_external.enum(["decimalPlaces", "significantFigures"]),
37061
+ figures: exports_external.number().int().safe().nonnegative()
37062
+ }).strict();
37063
+ scoringRuleSchema = exports_external.object({
37064
+ responseIdentifier: trimmedNonEmptyString,
37065
+ cardinality: exports_external.enum(["single", "multiple", "ordered"]),
37066
+ baseType: exports_external.enum(["string", "identifier", "integer", "float", "directedPair"]),
37067
+ points: positiveFiniteNumber,
37068
+ comparison: exports_external.discriminatedUnion("kind", [
37069
+ scoringMatchComparisonSchema,
37070
+ scoringNumericEqualComparisonSchema,
37071
+ scoringNumericEqualRoundedComparisonSchema
37072
+ ])
37073
+ }).strict().refine((rule) => rule.comparison.kind === "match" || rule.cardinality === "single" && (rule.baseType === "integer" || rule.baseType === "float")).refine((rule) => rule.baseType !== "directedPair" || rule.cardinality === "multiple" && rule.comparison.kind === "match").refine((rule) => rule.comparison.kind !== "match" || rule.cardinality !== "single" || rule.baseType === "string" || rule.comparison.correctValues.length === 1).refine((rule) => rule.comparison.kind !== "numeric-equal-rounded" || rule.comparison.roundingMode === "decimalPlaces" || rule.comparison.figures > 0).refine((rule) => {
37074
+ if (rule.comparison.kind !== "match") {
37075
+ return rule.baseType !== "integer" || Number.isSafeInteger(rule.comparison.correctValue);
37076
+ }
37077
+ if (rule.baseType === "integer" || rule.baseType === "float") {
37078
+ return rule.comparison.correctValues.every((value) => validNumericMatchValue(value, rule.baseType));
37079
+ }
37080
+ if (rule.baseType === "identifier") {
37081
+ return rule.comparison.correctValues.every((value) => IDENTIFIER_VALUE.test(value));
37082
+ }
37083
+ return rule.baseType !== "directedPair" || rule.comparison.correctValues.every((value) => DIRECTED_PAIR_VALUE.test(value));
37084
+ });
37085
+ scoringRulesSchema = exports_external.array(scoringRuleSchema).min(1).refine((rules) => new Set(rules.map((rule) => rule.responseIdentifier)).size === rules.length);
37086
+ scoringItemSchema = exports_external.discriminatedUnion("strategy", [
37087
+ exports_external.object({
37088
+ strategy: exports_external.literal("qti"),
37089
+ itemIdentifier: trimmedNonEmptyString,
37090
+ maxScore: positiveFiniteNumber,
37091
+ reason: exports_external.enum([
37092
+ "response-mapping",
37093
+ "area-mapping",
37094
+ "unsupported-base-type",
37095
+ "unsupported-cardinality",
37096
+ "invalid-correct-response",
37097
+ "no-scorable-response",
37098
+ "unsupported-processing-template",
37099
+ "unsupported-response-processing",
37100
+ "cross-response-processing",
37101
+ "inconsistent-max-score"
37102
+ ])
37103
+ }).strict(),
37104
+ exports_external.object({
37105
+ strategy: exports_external.literal("platform"),
37106
+ itemIdentifier: trimmedNonEmptyString,
37107
+ maxScore: positiveFiniteNumber,
37108
+ rules: scoringRulesSchema
37109
+ }).strict()
37110
+ ]).refine((item) => item.strategy === "qti" || sameScore(item.rules.reduce((total, rule) => total + rule.points, 0), item.maxScore));
37111
+ assessmentScoringArtifactItemsSchema = exports_external.array(scoringItemSchema).refine((items) => new Set(items.map((item) => item.itemIdentifier)).size === items.length);
37112
+ assessmentScoringArtifactSchema = exports_external.object({ items: assessmentScoringArtifactItemsSchema }).strict();
36740
37113
  SUPPORTED_BASE_TYPES = new Set([
36741
37114
  "string",
36742
37115
  "identifier",
@@ -36863,9 +37236,140 @@ var init_assessment_runtime2 = __esm(() => {
36863
37236
  }).strict();
36864
37237
  GRADE_VALUES = TIMEBACK_GRADES;
36865
37238
  POINT_RESPONSE_PATTERN = /^-?\d+(?:\.\d+)? -?\d+(?:\.\d+)?$/;
37239
+ ZERO_WIDTH_CHARACTERS = /[\u200B\uFEFF\u200C\u200D]/g;
37240
+ CURLY_SINGLE_QUOTES = /[\u2018\u2019\u201A\u201B]/g;
37241
+ CURLY_DOUBLE_QUOTES = /[\u201C\u201D\u201E\u201F]/g;
37242
+ UNICODE_DASHES = /[\u2010\u2011\u2012\u2013\u2014\u2015\u2212]/g;
37243
+ ARITHMETIC_OPERATOR_SPACING = /\s*([()+\-*/xX\u00D7\u00F7\u00B7])\s*/g;
37244
+ THOUSANDS_GROUP = /\b\d{1,3}(?:,\d{3})+(?:\.\d+)?\b/g;
36866
37245
  DEFAULT_REVIEW_SELECTION_POLICY = {
36867
37246
  version: REVIEW_SELECTION_POLICY_VERSION
36868
37247
  };
37248
+ IdentitySchema = exports_external.string().min(1).refine((value) => value.trim() === value, {
37249
+ message: "Identifiers must not contain surrounding whitespace"
37250
+ });
37251
+ RecordedAtSchema = exports_external.string().datetime().refine((value) => new Date(value).toISOString() === value, {
37252
+ message: "recordedAt must be a canonical UTC timestamp"
37253
+ });
37254
+ ResponseKeySchema = IdentitySchema;
37255
+ AssessmentResponseValueSchema = exports_external.custom(isAssessmentResponseValue, {
37256
+ message: `Response values are non-empty text, at most ${TIMEBACK_ASSESSMENT_RESPONSE_VALUE_MAX_LENGTH} characters each`
37257
+ });
37258
+ AssessmentScoreSchema = exports_external.object({
37259
+ earned: exports_external.number().finite().nonnegative(),
37260
+ possible: exports_external.number().finite().nonnegative(),
37261
+ normalized: exports_external.number().finite().min(0).max(1)
37262
+ }).strict().superRefine((score, context) => {
37263
+ const expectedNormalized = score.possible === 0 ? 0 : score.earned / score.possible;
37264
+ if (score.earned > score.possible) {
37265
+ context.addIssue({
37266
+ code: exports_external.ZodIssueCode.custom,
37267
+ message: "Earned score must not exceed possible score",
37268
+ path: ["earned"]
37269
+ });
37270
+ }
37271
+ if (score.normalized !== expectedNormalized) {
37272
+ context.addIssue({
37273
+ code: exports_external.ZodIssueCode.custom,
37274
+ message: "Normalized score must equal earned divided by possible",
37275
+ path: ["normalized"]
37276
+ });
37277
+ }
37278
+ });
37279
+ AssessmentStandardRefSchema = exports_external.object({
37280
+ framework: IdentitySchema,
37281
+ identifier: IdentitySchema
37282
+ }).strict();
37283
+ DiagnosticRoutingLedgerEntryV1Schema = exports_external.object({
37284
+ stageKey: IdentitySchema,
37285
+ trackKey: IdentitySchema,
37286
+ routingNodeKey: IdentitySchema,
37287
+ itemIdentifier: IdentitySchema,
37288
+ isCorrect: exports_external.boolean()
37289
+ }).strict();
37290
+ AssessmentChildResultAttemptV1Schema = exports_external.object({
37291
+ attemptId: IdentitySchema,
37292
+ gameId: IdentitySchema,
37293
+ studentId: IdentitySchema,
37294
+ activityId: IdentitySchema,
37295
+ courseId: IdentitySchema,
37296
+ integrationId: IdentitySchema,
37297
+ enrollmentId: IdentitySchema,
37298
+ selectedTest: exports_external.object({
37299
+ assessmentKey: IdentitySchema,
37300
+ identifier: IdentitySchema,
37301
+ contentRevision: IdentitySchema
37302
+ }).strict()
37303
+ }).strict();
37304
+ AssessmentChildResultAdministrationV1Schema = exports_external.object({
37305
+ submissionId: IdentitySchema,
37306
+ responseVersion: exports_external.number().int().positive(),
37307
+ itemIdentifier: IdentitySchema
37308
+ }).strict();
37309
+ AssessmentChildResultGradingV1Schema = exports_external.discriminatedUnion("source", [
37310
+ exports_external.object({
37311
+ source: exports_external.literal("timeback-qti"),
37312
+ graderVersion: IdentitySchema
37313
+ }).strict(),
37314
+ exports_external.object({
37315
+ source: exports_external.literal("platform-qti-adapter"),
37316
+ graderVersion: IdentitySchema,
37317
+ qtiGraderVersion: IdentitySchema
37318
+ }).strict(),
37319
+ exports_external.object({
37320
+ source: exports_external.literal("platform-artifact"),
37321
+ artifactVersion: IdentitySchema,
37322
+ graderVersion: IdentitySchema
37323
+ }).strict()
37324
+ ]);
37325
+ ReviewChildResultContextV1Schema = exports_external.object({
37326
+ kind: exports_external.literal("review"),
37327
+ bankRevision: IdentitySchema,
37328
+ standard: AssessmentStandardRefSchema
37329
+ }).strict();
37330
+ DiagnosticChildResultContextV1Schema = exports_external.object({
37331
+ kind: exports_external.literal("platform-routed-diagnostic"),
37332
+ definitionId: IdentitySchema,
37333
+ diagnosticKey: IdentitySchema,
37334
+ routingRevision: IdentitySchema,
37335
+ transition: DiagnosticRoutingLedgerEntryV1Schema
37336
+ }).strict();
37337
+ CommonPayloadShape = {
37338
+ attempt: AssessmentChildResultAttemptV1Schema,
37339
+ administration: AssessmentChildResultAdministrationV1Schema,
37340
+ responses: exports_external.record(ResponseKeySchema, AssessmentResponseValueSchema),
37341
+ score: AssessmentScoreSchema,
37342
+ grading: AssessmentChildResultGradingV1Schema
37343
+ };
37344
+ ReviewChildResultPayloadV1Schema = exports_external.object({
37345
+ ...CommonPayloadShape,
37346
+ isCorrect: exports_external.boolean().optional(),
37347
+ context: ReviewChildResultContextV1Schema
37348
+ }).strict();
37349
+ DiagnosticChildResultPayloadV1Schema = exports_external.object({
37350
+ ...CommonPayloadShape,
37351
+ isCorrect: exports_external.boolean(),
37352
+ context: DiagnosticChildResultContextV1Schema
37353
+ }).strict().superRefine((payload, context) => {
37354
+ if (payload.context.transition.itemIdentifier !== payload.administration.itemIdentifier || payload.context.transition.isCorrect !== payload.isCorrect) {
37355
+ context.addIssue({
37356
+ code: exports_external.ZodIssueCode.custom,
37357
+ message: "Diagnostic transition must match the administered item and correctness",
37358
+ path: ["context", "transition"]
37359
+ });
37360
+ }
37361
+ });
37362
+ AssessmentChildResultPayloadV1Schema = exports_external.union([
37363
+ ReviewChildResultPayloadV1Schema,
37364
+ DiagnosticChildResultPayloadV1Schema
37365
+ ]);
37366
+ AssessmentChildResultCommandV1Schema = exports_external.object({
37367
+ kind: exports_external.literal(ASSESSMENT_CHILD_RESULT_KIND),
37368
+ version: exports_external.literal(ASSESSMENT_CHILD_RESULT_VERSION),
37369
+ recordedAt: RecordedAtSchema,
37370
+ payloadHash: exports_external.string().regex(/^[a-f0-9]{64}$/),
37371
+ payload: AssessmentChildResultPayloadV1Schema
37372
+ }).strict();
36869
37373
  RuntimeSubjectSchema = exports_external.enum(TIMEBACK_SUBJECTS);
36870
37374
  RuntimeGradeSchema = exports_external.number().refine(isTimebackGrade, {
36871
37375
  message: `Grade must be one of: ${TIMEBACK_GRADES.join(", ")}`
@@ -36876,14 +37380,14 @@ var init_assessment_runtime2 = __esm(() => {
36876
37380
  }
36877
37381
  return typeof value === "string" && value.trim() !== "" ? Number(value) : Number.NaN;
36878
37382
  }, RuntimeGradeSchema.optional());
36879
- AssessmentResponseValueSchema = exports_external.custom(isAssessmentResponseValue, {
37383
+ AssessmentResponseValueSchema2 = exports_external.custom(isAssessmentResponseValue, {
36880
37384
  message: `Response values are non-empty text, at most ${TIMEBACK_ASSESSMENT_RESPONSE_VALUE_MAX_LENGTH} characters each`
36881
37385
  });
36882
37386
  AssessmentRuntimeIdentitySchema = exports_external.object({
36883
37387
  gameId: exports_external.string().uuid(),
36884
37388
  studentId: exports_external.string().trim().min(1)
36885
37389
  });
36886
- ResponseKeySchema = exports_external.string().refine((key) => key.length > 0 && key.trim() === key, {
37390
+ ResponseKeySchema2 = exports_external.string().refine((key) => key.length > 0 && key.trim() === key, {
36887
37391
  message: "Response keys must be non-empty without surrounding whitespace"
36888
37392
  });
36889
37393
  StartAssessmentBaseSchema = exports_external.object({
@@ -36891,7 +37395,7 @@ var init_assessment_runtime2 = __esm(() => {
36891
37395
  subject: RuntimeSubjectSchema.optional(),
36892
37396
  grade: RuntimeGradeSchema.optional()
36893
37397
  });
36894
- AssessmentStandardRefSchema = exports_external.object({
37398
+ AssessmentStandardRefSchema2 = exports_external.object({
36895
37399
  framework: exports_external.string().trim().min(1).max(TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.standardFieldLength),
36896
37400
  identifier: exports_external.string().trim().min(1).max(TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.standardFieldLength)
36897
37401
  }).refine((standard) => {
@@ -36908,7 +37412,7 @@ var init_assessment_runtime2 = __esm(() => {
36908
37412
  }),
36909
37413
  LatestAssessmentFilterBaseSchema.extend({
36910
37414
  purpose: exports_external.literal("mastery"),
36911
- standard: AssessmentStandardRefSchema
37415
+ standard: AssessmentStandardRefSchema2
36912
37416
  })
36913
37417
  ]);
36914
37418
  LatestRuntimeAssessmentQuerySchema = exports_external.intersection(AssessmentRuntimeIdentitySchema, LatestAssessmentFiltersSchema);
@@ -36922,26 +37426,26 @@ var init_assessment_runtime2 = __esm(() => {
36922
37426
  }),
36923
37427
  StartAssessmentBaseSchema.extend({
36924
37428
  purpose: exports_external.literal("review"),
36925
- standards: exports_external.array(AssessmentStandardRefSchema).min(1).max(TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.standards),
37429
+ standards: exports_external.array(AssessmentStandardRefSchema2).min(1).max(TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.standards),
36926
37430
  candidateItemsPerStandard: exports_external.number().int().positive().max(TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.candidateItemsPerStandard).optional()
36927
37431
  }),
36928
37432
  StartAssessmentBaseSchema.extend({
36929
37433
  purpose: exports_external.literal("mastery"),
36930
- standard: AssessmentStandardRefSchema
37434
+ standard: AssessmentStandardRefSchema2
36931
37435
  })
36932
37436
  ]);
36933
37437
  AssessmentPreparationReceiptSchema = exports_external.string().max(32000);
36934
37438
  StartAssessmentRequestBodySchema = exports_external.intersection(StartAssessmentBodySchema, exports_external.object({ preparationReceipt: AssessmentPreparationReceiptSchema.optional() }));
36935
37439
  SaveAssessmentBodySchema = exports_external.object({
36936
37440
  expectedResponseVersion: exports_external.number().int().nonnegative(),
36937
- responses: exports_external.record(ResponseKeySchema, exports_external.record(ResponseKeySchema, AssessmentResponseValueSchema.nullable()))
37441
+ responses: exports_external.record(ResponseKeySchema2, exports_external.record(ResponseKeySchema2, AssessmentResponseValueSchema2.nullable()))
36938
37442
  });
36939
37443
  SubmitAssessmentItemBodySchema = exports_external.object({
36940
37444
  expectedResponseVersion: exports_external.number().int().nonnegative(),
36941
37445
  submissionId: exports_external.string().trim().min(1),
36942
- itemIdentifier: ResponseKeySchema,
36943
- routingNodeKey: ResponseKeySchema.optional(),
36944
- responses: exports_external.record(ResponseKeySchema, AssessmentResponseValueSchema.nullable())
37446
+ itemIdentifier: ResponseKeySchema2,
37447
+ routingNodeKey: ResponseKeySchema2.optional(),
37448
+ responses: exports_external.record(ResponseKeySchema2, AssessmentResponseValueSchema2.nullable())
36945
37449
  });
36946
37450
  SubmitAssessmentBodySchema = exports_external.object({
36947
37451
  expectedResponseVersion: exports_external.number().int().nonnegative(),
@@ -61433,7 +61937,7 @@ var init_types2 = __esm(() => {
61433
61937
  function kebabToTitleCase(kebabStr) {
61434
61938
  return kebabStr.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
61435
61939
  }
61436
- function isRecord5(value) {
61940
+ function isRecord4(value) {
61437
61941
  return typeof value === "object" && value !== null;
61438
61942
  }
61439
61943
  function resolveIntegrationSubject(patchSubject, liveSubject, storedSubject) {
@@ -61510,9 +62014,9 @@ function getGeneratedMetricValue(event, type) {
61510
62014
  return Number.isFinite(value) ? value : undefined;
61511
62015
  }
61512
62016
  function getMergedCaliperExtensions(event) {
61513
- const objectActivityExtensions = isRecord5(event.object.activity?.extensions) ? event.object.activity.extensions : undefined;
61514
- const generatedExtensions = isRecord5(event.generated?.extensions) ? event.generated.extensions : undefined;
61515
- const eventExtensions = isRecord5(event.extensions) ? event.extensions : undefined;
62017
+ const objectActivityExtensions = isRecord4(event.object.activity?.extensions) ? event.object.activity.extensions : undefined;
62018
+ const generatedExtensions = isRecord4(event.generated?.extensions) ? event.generated.extensions : undefined;
62019
+ const eventExtensions = isRecord4(event.extensions) ? event.extensions : undefined;
61516
62020
  return {
61517
62021
  ...objectActivityExtensions,
61518
62022
  ...generatedExtensions,
@@ -61521,7 +62025,7 @@ function getMergedCaliperExtensions(event) {
61521
62025
  }
61522
62026
  function getPlaycademyMetadata(event) {
61523
62027
  const extensions = getMergedCaliperExtensions(event);
61524
- return isRecord5(extensions.playcademy) ? extensions.playcademy : undefined;
62028
+ return isRecord4(extensions.playcademy) ? extensions.playcademy : undefined;
61525
62029
  }
61526
62030
  function getActivityId(event, playcademy) {
61527
62031
  const metadataActivityId = getStringValue(playcademy?.activityId);
@@ -61573,7 +62077,7 @@ function buildResourceMetadata({
61573
62077
  }
61574
62078
  function getDurationSecondsFromExtensions(event) {
61575
62079
  const extensions = getMergedCaliperExtensions(event);
61576
- const playcademy = isRecord5(extensions.playcademy) ? extensions.playcademy : undefined;
62080
+ const playcademy = isRecord4(extensions.playcademy) ? extensions.playcademy : undefined;
61577
62081
  const rawValue = extensions.durationSeconds ?? playcademy?.durationSeconds;
61578
62082
  const value = typeof rawValue === "number" ? rawValue : Number(rawValue);
61579
62083
  return Number.isFinite(value) ? value : undefined;
@@ -72031,7 +72535,7 @@ var AssessmentKeySchema;
72031
72535
  var DiagnosticKeySchema;
72032
72536
  var DiagnosticRoutingManifestSchema;
72033
72537
  var DiagnosticAssessmentDefinitionInputSchema;
72034
- var AssessmentStandardRefSchema2;
72538
+ var AssessmentStandardRefSchema3;
72035
72539
  var CreateAssessmentRequestSchema;
72036
72540
  var UpdateAssessmentRequestSchema;
72037
72541
  var CopyAssessmentRequestSchema;
@@ -72289,7 +72793,7 @@ var init_schemas4 = __esm(() => {
72289
72793
  diagnosticKey: DiagnosticKeySchema,
72290
72794
  routingManifest: DiagnosticRoutingManifestSchema
72291
72795
  });
72292
- AssessmentStandardRefSchema2 = exports_external.object({
72796
+ AssessmentStandardRefSchema3 = exports_external.object({
72293
72797
  framework: exports_external.string().trim().min(1).max(TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.standardFieldLength),
72294
72798
  identifier: exports_external.string().trim().min(1).max(TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.standardFieldLength)
72295
72799
  });
@@ -72297,7 +72801,7 @@ var init_schemas4 = __esm(() => {
72297
72801
  assessmentKey: AssessmentKeySchema,
72298
72802
  title: exports_external.string().min(1, "Assessment title is required"),
72299
72803
  purpose: AssessmentPurposeSchema,
72300
- standard: AssessmentStandardRefSchema2.optional()
72804
+ standard: AssessmentStandardRefSchema3.optional()
72301
72805
  }).superRefine((input, context2) => {
72302
72806
  requireMasteryStandard(input, context2);
72303
72807
  rejectSystemManagedReview(input, context2);
@@ -72305,7 +72809,7 @@ var init_schemas4 = __esm(() => {
72305
72809
  UpdateAssessmentRequestSchema = exports_external.object({
72306
72810
  title: exports_external.string().trim().min(1, "Assessment title is required").optional(),
72307
72811
  purpose: AssessmentPurposeSchema.optional(),
72308
- standard: AssessmentStandardRefSchema2.optional(),
72812
+ standard: AssessmentStandardRefSchema3.optional(),
72309
72813
  diagnostic: DiagnosticAssessmentDefinitionInputSchema.nullable().optional(),
72310
72814
  status: AssessmentStatusSchema.optional()
72311
72815
  }).refine((input) => input.title !== undefined || input.purpose !== undefined || input.standard !== undefined || input.diagnostic !== undefined || input.status !== undefined, {
@@ -72315,7 +72819,7 @@ var init_schemas4 = __esm(() => {
72315
72819
  assessmentKey: AssessmentKeySchema,
72316
72820
  testIdentifier: exports_external.string().trim().min(1, "Assessment identifier is required"),
72317
72821
  purpose: AssessmentPurposeSchema,
72318
- standard: AssessmentStandardRefSchema2.optional()
72822
+ standard: AssessmentStandardRefSchema3.optional()
72319
72823
  }).superRefine((input, context2) => {
72320
72824
  requireMasteryStandard(input, context2);
72321
72825
  rejectSystemManagedReview(input, context2);
@@ -72324,7 +72828,7 @@ var init_schemas4 = __esm(() => {
72324
72828
  assessmentKey: AssessmentKeySchema,
72325
72829
  qtiTestIdentifier: exports_external.string().trim().min(1, "Assessment identifier is required"),
72326
72830
  purpose: AssessmentPurposeSchema,
72327
- standard: AssessmentStandardRefSchema2.optional()
72831
+ standard: AssessmentStandardRefSchema3.optional()
72328
72832
  }).superRefine((input, context2) => {
72329
72833
  requireMasteryStandard(input, context2);
72330
72834
  rejectSystemManagedReview(input, context2);
@@ -120539,16 +121043,16 @@ function qtiResponseMappingMaximum(input) {
120539
121043
  return null;
120540
121044
  }
120541
121045
  const defaultValue = mapping.defaultValue ?? 0;
120542
- const responseValues2 = input.responseValues ? [...new Set(input.responseValues)].map((value) => mapping.entries[value] ?? defaultValue) : entryValues;
121046
+ const responseValues3 = input.responseValues ? [...new Set(input.responseValues)].map((value) => mapping.entries[value] ?? defaultValue) : entryValues;
120543
121047
  let maximum;
120544
121048
  if (input.cardinality === "single") {
120545
- maximum = Math.max(0, defaultValue, ...responseValues2);
121049
+ maximum = Math.max(0, defaultValue, ...responseValues3);
120546
121050
  } else {
120547
121051
  if (!input.responseValues && defaultValue > 0) {
120548
121052
  return null;
120549
121053
  }
120550
- const limit = Math.min(input.maximumResponses ?? responseValues2.length, responseValues2.length);
120551
- maximum = responseValues2.toSorted((left, right) => right - left).slice(0, limit).reduce((sum, value) => sum + Math.max(0, value), 0);
121054
+ const limit = Math.min(input.maximumResponses ?? responseValues3.length, responseValues3.length);
121055
+ maximum = responseValues3.toSorted((left, right) => right - left).slice(0, limit).reduce((sum, value) => sum + Math.max(0, value), 0);
120552
121056
  }
120553
121057
  if (mapping.lowerBound !== undefined) {
120554
121058
  maximum = Math.max(maximum, mapping.lowerBound);
@@ -121110,16 +121614,16 @@ function authoredMappingContext(interaction, declaration) {
121110
121614
  return {};
121111
121615
  }
121112
121616
  const type = qtiAuthoringInteractionType(interaction);
121113
- let responseValues2;
121617
+ let responseValues3;
121114
121618
  if (type === "choice" || type === "inline-choice" || type === "order" || type === "hotspot") {
121115
- responseValues2 = interaction.choices?.map((choice) => String(choice.identifier)) ?? [];
121619
+ responseValues3 = interaction.choices?.map((choice) => String(choice.identifier)) ?? [];
121116
121620
  } else if (type === "hottext") {
121117
- responseValues2 = (interaction.textSegments ?? []).flatMap((segment) => segment.selectable && segment.identifier ? [segment.identifier] : []);
121621
+ responseValues3 = (interaction.textSegments ?? []).flatMap((segment) => segment.selectable && segment.identifier ? [segment.identifier] : []);
121118
121622
  } else if (type === "match" || type === "graphic-gap-match") {
121119
121623
  const [left = [], right = []] = interaction.choiceSets ?? [];
121120
- responseValues2 = authoredResponsePairs(left.map((choice) => String(choice.identifier)), right.map((choice) => String(choice.identifier)));
121624
+ responseValues3 = authoredResponsePairs(left.map((choice) => String(choice.identifier)), right.map((choice) => String(choice.identifier)));
121121
121625
  }
121122
- const capacity = responseValues2?.length ?? Object.keys(declaration.mapping?.entries ?? {}).length;
121626
+ const capacity = responseValues3?.length ?? Object.keys(declaration.mapping?.entries ?? {}).length;
121123
121627
  const limitAttribute = type === "match" || type === "gap-match" || type === "graphic-associate" || type === "graphic-gap-match" ? "max-associations" : "max-choices";
121124
121628
  const rawLimit = interaction.attributes?.[limitAttribute];
121125
121629
  const declaredLimit = rawLimit === undefined ? undefined : Number(rawLimit);
@@ -121138,7 +121642,7 @@ function authoredMappingContext(interaction, declaration) {
121138
121642
  maximumResponses = capacity;
121139
121643
  }
121140
121644
  return {
121141
- ...responseValues2 ? { responseValues: responseValues2 } : {},
121645
+ ...responseValues3 ? { responseValues: responseValues3 } : {},
121142
121646
  ...maximumResponses > 0 ? { maximumResponses } : {}
121143
121647
  };
121144
121648
  }
@@ -121322,6 +121826,13 @@ function qtiGapContentMatchesGaps(nodes, gaps) {
121322
121826
  const identifiers = gapContentIdentifiers(nodes);
121323
121827
  return identifiers.length === gaps.length && identifiers.every((identifier, index2) => identifier === gaps[index2]);
121324
121828
  }
121829
+ function qtiFloatValue(value) {
121830
+ if (value === undefined || !QTI_FLOAT_VALUE.test(value.trim())) {
121831
+ return;
121832
+ }
121833
+ const number7 = Number(value);
121834
+ return Number.isFinite(number7) ? number7 : undefined;
121835
+ }
121325
121836
  function normalizedInteractionType(value) {
121326
121837
  if (typeof value !== "string") {
121327
121838
  return;
@@ -121540,24 +122051,24 @@ function interactionResponseSpace(item, declaration) {
121540
122051
  }
121541
122052
  case "match": {
121542
122053
  const [source = [], target = []] = interaction.choiceSets;
121543
- const responseValues2 = responsePairs(source.map((choice) => choice.identifier), target.map((choice) => choice.identifier));
122054
+ const responseValues3 = responsePairs(source.map((choice) => choice.identifier), target.map((choice) => choice.identifier));
121544
122055
  return {
121545
- responseValues: responseValues2,
121546
- maximumResponses: qtiAttributeMaximum(attributes2, "max-associations", 1, responseValues2.length)
122056
+ responseValues: responseValues3,
122057
+ maximumResponses: qtiAttributeMaximum(attributes2, "max-associations", 1, responseValues3.length)
121547
122058
  };
121548
122059
  }
121549
122060
  case "hottext": {
121550
- const responseValues2 = interaction.textSegments.flatMap((segment) => segment.selectable && segment.identifier ? [segment.identifier] : []);
122061
+ const responseValues3 = interaction.textSegments.flatMap((segment) => segment.selectable && segment.identifier ? [segment.identifier] : []);
121551
122062
  return {
121552
- responseValues: responseValues2,
121553
- maximumResponses: qtiAttributeMaximum(attributes2, "max-choices", 1, responseValues2.length)
122063
+ responseValues: responseValues3,
122064
+ maximumResponses: qtiAttributeMaximum(attributes2, "max-choices", 1, responseValues3.length)
121554
122065
  };
121555
122066
  }
121556
122067
  case "gap-match": {
121557
- const responseValues2 = responsePairs(interaction.gapTexts.map((choice) => choice.identifier), interaction.gaps);
122068
+ const responseValues3 = responsePairs(interaction.gapTexts.map((choice) => choice.identifier), interaction.gaps);
121558
122069
  return {
121559
- responseValues: responseValues2,
121560
- maximumResponses: qtiAttributeMaximum(attributes2, "max-associations", 0, responseValues2.length)
122070
+ responseValues: responseValues3,
122071
+ maximumResponses: qtiAttributeMaximum(attributes2, "max-associations", 0, responseValues3.length)
121561
122072
  };
121562
122073
  }
121563
122074
  case "hotspot": {
@@ -121567,10 +122078,10 @@ function interactionResponseSpace(item, declaration) {
121567
122078
  };
121568
122079
  }
121569
122080
  case "graphic-gap-match": {
121570
- const responseValues2 = responsePairs(interaction.gapImages.map((choice) => choice.identifier), interaction.hotspots.map((hotspot) => hotspot.identifier));
122081
+ const responseValues3 = responsePairs(interaction.gapImages.map((choice) => choice.identifier), interaction.hotspots.map((hotspot) => hotspot.identifier));
121571
122082
  return {
121572
- responseValues: responseValues2,
121573
- maximumResponses: qtiAttributeMaximum(attributes2, "max-associations", 0, responseValues2.length)
122083
+ responseValues: responseValues3,
122084
+ maximumResponses: qtiAttributeMaximum(attributes2, "max-associations", 0, responseValues3.length)
121574
122085
  };
121575
122086
  }
121576
122087
  case "graphic-associate": {
@@ -121615,8 +122126,8 @@ function responseMappingMaximum(item, declaration, space) {
121615
122126
  function qtiItemScoringValidationMessage(item) {
121616
122127
  for (const declaration of item.responseDeclarations) {
121617
122128
  const space = interactionResponseSpace(item, declaration);
121618
- const { responseValues: responseValues2, maximumResponses } = space;
121619
- if (responseValues2 && declaration.correctValues.some((value) => !responseValues2.includes(value))) {
122129
+ const { responseValues: responseValues3, maximumResponses } = space;
122130
+ if (responseValues3 && declaration.correctValues.some((value) => !responseValues3.includes(value))) {
121620
122131
  return `Response ${declaration.identifier} declares a correct value its interaction cannot produce.`;
121621
122132
  }
121622
122133
  const interaction = item.interactions.find((candidate) => candidate.responseIdentifier === declaration.identifier);
@@ -121656,13 +122167,13 @@ function qtiItemScoringValidationMessage(item) {
121656
122167
  }
121657
122168
  function qtiItemMaxScore(item) {
121658
122169
  const maxScore = item.outcomeDeclarations.find((declaration) => declaration.identifier.toUpperCase() === "MAXSCORE");
121659
- const declaredMaxScore = Number(maxScore?.defaultValues[0]);
121660
- if (Number.isFinite(declaredMaxScore) && declaredMaxScore > 0) {
122170
+ const declaredMaxScore = qtiFloatValue(maxScore?.defaultValues[0]);
122171
+ if (declaredMaxScore !== undefined && declaredMaxScore > 0) {
121661
122172
  return declaredMaxScore;
121662
122173
  }
121663
122174
  const score = item.outcomeDeclarations.find((declaration) => declaration.identifier.toUpperCase() === "SCORE");
121664
- const normalMaximum = Number(score?.attributes["normal-maximum"]);
121665
- if (Number.isFinite(normalMaximum) && normalMaximum > 0) {
122175
+ const normalMaximum = qtiFloatValue(score?.attributes["normal-maximum"]);
122176
+ if (normalMaximum !== undefined && normalMaximum > 0) {
121666
122177
  return normalMaximum;
121667
122178
  }
121668
122179
  const inferredMaximum = item.responseDeclarations.reduce((total, declaration) => {
@@ -121690,7 +122201,7 @@ function scoresResponsesIndependently(item, scoredIdentifiers) {
121690
122201
  if (!processing) {
121691
122202
  return true;
121692
122203
  }
121693
- if (processing.attributes.template || processing.conditions.length !== scoredIdentifiers.size || !processing.elements.every((element) => INDEPENDENT_PROCESSING_TAGS2.has(element.tagName)) || !processing.baseValues.every((base) => Number.isFinite(Number(base.value)))) {
122204
+ if (processing.attributes.template || processing.conditions.length !== scoredIdentifiers.size || !processing.elements.every((element) => INDEPENDENT_PROCESSING_TAGS2.has(element.tagName)) || !processing.baseValues.every((base) => qtiFloatValue(base.value) !== undefined)) {
121694
122205
  return false;
121695
122206
  }
121696
122207
  const declaredIdentifiers = new Set(item.responseDeclarations.map((declaration) => declaration.identifier));
@@ -121778,6 +122289,40 @@ function supportedQtiQuestionInteractionType(question) {
121778
122289
  return null;
121779
122290
  }
121780
122291
  }
122292
+ function validNumericMatchValue2(value, baseType) {
122293
+ const syntax = baseType === "integer" ? /^[+-]?\d+$/ : /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i;
122294
+ if (!syntax.test(value.trim())) {
122295
+ return false;
122296
+ }
122297
+ const numeric3 = Number(value);
122298
+ return Number.isFinite(numeric3) && (baseType === "float" || Number.isSafeInteger(numeric3));
122299
+ }
122300
+ function sameScore2(left, right) {
122301
+ return Number.isFinite(left) && Number.isFinite(right) && Math.abs(left - right) <= Number.EPSILON * Math.max(1, Math.abs(left), Math.abs(right));
122302
+ }
122303
+ function scoringItemMatchesPlayableItem(scoringItem, item) {
122304
+ if (!sameScore2(scoringItem.maxScore, item.maxScore)) {
122305
+ return false;
122306
+ }
122307
+ if (scoringItem.strategy === "qti") {
122308
+ return true;
122309
+ }
122310
+ const playableResponses = new Set(item.interactions.map((interaction) => interaction.responseIdentifier));
122311
+ const scoringResponses = new Set(scoringItem.rules.map((rule) => rule.responseIdentifier));
122312
+ return playableResponses.size === item.interactions.length && scoringResponses.size === scoringItem.rules.length && playableResponses.size === scoringResponses.size && [...playableResponses].every((identifier) => scoringResponses.has(identifier));
122313
+ }
122314
+ function normalizedKey(key) {
122315
+ return key.replaceAll(/[^a-z0-9]/gi, "").toLowerCase();
122316
+ }
122317
+ function containsPrivatePlayableAssessmentData(value) {
122318
+ if (Array.isArray(value)) {
122319
+ return value.some(containsPrivatePlayableAssessmentData);
122320
+ }
122321
+ if (!isRecord(value)) {
122322
+ return false;
122323
+ }
122324
+ return Object.entries(value).some(([key, entry2]) => PRIVATE_PLAYABLE_KEYS2.has(normalizedKey(key)) || containsPrivatePlayableAssessmentData(entry2));
122325
+ }
121781
122326
  function childElements(node, localName2) {
121782
122327
  return node.children.filter((child) => child.kind === "element" && (!localName2 || child.localName === localName2));
121783
122328
  }
@@ -121819,6 +122364,22 @@ function numericCorrectValue(declaration) {
121819
122364
  }
121820
122365
  return parsedNumericValue(declaration.correctValues[0], declaration.baseType);
121821
122366
  }
122367
+ function rawCorrectValues(declaration) {
122368
+ const correctResponses = childElements(declaration, "qti-correct-response");
122369
+ const correctResponse = correctResponses.length === 1 ? correctResponses[0] : undefined;
122370
+ if (!correctResponse || !onlyWhitespaceText(correctResponse.children)) {
122371
+ return null;
122372
+ }
122373
+ const values = childElements(correctResponse);
122374
+ if (values.length === 0 || values.some((value) => value.localName !== "qti-value" || !onlyAttributes(value, []))) {
122375
+ return null;
122376
+ }
122377
+ const rawValues = values.map((value) => {
122378
+ const text3 = directText(value);
122379
+ return text3 === null ? null : decodeXmlEntities(text3).trim();
122380
+ });
122381
+ return rawValues.every((value) => value !== null) ? rawValues : null;
122382
+ }
121822
122383
  function declarationResult(raw, normalized) {
121823
122384
  const identifier = raw.attributes.identifier?.trim();
121824
122385
  if (!normalized || !identifier || normalized.identifier !== identifier) {
@@ -121835,16 +122396,23 @@ function declarationResult(raw, normalized) {
121835
122396
  if (baseType === "directedPair" && cardinality !== "multiple") {
121836
122397
  return { reason: "unsupported-cardinality" };
121837
122398
  }
122399
+ const correctValues = rawCorrectValues(raw);
122400
+ if (!correctValues) {
122401
+ return { reason: "invalid-correct-response" };
122402
+ }
121838
122403
  const declaration = {
121839
122404
  identifier,
121840
122405
  cardinality,
121841
122406
  baseType,
121842
- correctValues: [...normalized.correctValues]
122407
+ correctValues
121843
122408
  };
121844
- if (declaration.correctValues.length === 0 || declaration.correctValues.some((value) => value.trim() === "") || declaration.cardinality === "single" && declaration.correctValues.length !== 1 && declaration.baseType !== "string") {
122409
+ if (declaration.correctValues.length === 0 || declaration.correctValues.some((value) => value.trim() === "") || new Set(declaration.correctValues).size !== declaration.correctValues.length || declaration.cardinality === "single" && declaration.correctValues.length !== 1 && declaration.baseType !== "string") {
122410
+ return { reason: "invalid-correct-response" };
122411
+ }
122412
+ if (declaration.baseType === "identifier" && declaration.correctValues.some((value) => !IDENTIFIER_VALUE2.test(value))) {
121845
122413
  return { reason: "invalid-correct-response" };
121846
122414
  }
121847
- if (declaration.baseType === "directedPair" && (declaration.correctValues.some((value) => !DIRECTED_PAIR_VALUE.test(value)) || new Set(declaration.correctValues).size !== declaration.correctValues.length)) {
122415
+ if (declaration.baseType === "directedPair" && declaration.correctValues.some((value) => !DIRECTED_PAIR_VALUE2.test(value))) {
121848
122416
  return { reason: "invalid-correct-response" };
121849
122417
  }
121850
122418
  if ((declaration.baseType === "integer" || declaration.baseType === "float") && declaration.correctValues.some((value) => parsedNumericValue(value, declaration.baseType) === null)) {
@@ -121860,16 +122428,14 @@ function scoringDeclarations(rawDeclarations, normalizedDeclarations) {
121860
122428
  const declarations = new Map;
121861
122429
  for (const [index2, rawDeclaration] of rawDeclarations.entries()) {
121862
122430
  const normalized = normalizedDeclarations[index2];
121863
- if (normalized?.correctValues.length) {
121864
- const result = declarationResult(rawDeclaration, normalized);
121865
- if ("reason" in result) {
121866
- return result;
121867
- }
121868
- if (declarations.has(result.declaration.identifier)) {
121869
- return { reason: "invalid-correct-response" };
121870
- }
121871
- declarations.set(result.declaration.identifier, result.declaration);
122431
+ const result = declarationResult(rawDeclaration, normalized);
122432
+ if ("reason" in result) {
122433
+ return result;
122434
+ }
122435
+ if (declarations.has(result.declaration.identifier)) {
122436
+ return { reason: "invalid-correct-response" };
121872
122437
  }
122438
+ declarations.set(result.declaration.identifier, result.declaration);
121873
122439
  }
121874
122440
  return declarations.size > 0 ? { declarations } : { reason: "no-scorable-response" };
121875
122441
  }
@@ -121879,15 +122445,12 @@ function declarationConstructReason(declarations) {
121879
122445
  }
121880
122446
  return declarations.some((declaration) => childElements(declaration, "qti-area-mapping").length > 0) ? "area-mapping" : null;
121881
122447
  }
121882
- function sameScore(left, right) {
121883
- return Math.abs(left - right) <= Number.EPSILON * Math.max(1, Math.abs(left), Math.abs(right));
121884
- }
121885
122448
  function platformResult(itemIdentifier, parsedMaxScore, rules) {
121886
122449
  const ruleMaximum = rules.reduce((sum, rule) => sum + rule.points, 0);
121887
122450
  if (!Number.isFinite(ruleMaximum) || ruleMaximum <= 0) {
121888
122451
  return fallback(itemIdentifier, parsedMaxScore, "no-scorable-response");
121889
122452
  }
121890
- if (!sameScore(parsedMaxScore, ruleMaximum)) {
122453
+ if (!sameScore2(parsedMaxScore, ruleMaximum)) {
121891
122454
  return fallback(itemIdentifier, parsedMaxScore, "inconsistent-max-score");
121892
122455
  }
121893
122456
  return {
@@ -121948,9 +122511,8 @@ function comparisonResult(comparison, declaration) {
121948
122511
  return null;
121949
122512
  }
121950
122513
  const roundingMode = comparison.attributes["rounding-mode"];
121951
- const figures = Number(comparison.attributes.figures);
121952
- const validFigures = Number.isInteger(figures) && (roundingMode === "decimalPlaces" ? figures >= 0 : figures > 0);
121953
- if (roundingMode !== "decimalPlaces" && roundingMode !== "significantFigures" || !validFigures) {
122514
+ const figures = parsedNumericValue(comparison.attributes.figures ?? "", "integer");
122515
+ if (roundingMode !== "decimalPlaces" && roundingMode !== "significantFigures" || figures === null || (roundingMode === "decimalPlaces" ? figures < 0 : figures <= 0)) {
121954
122516
  return null;
121955
122517
  }
121956
122518
  return {
@@ -121982,8 +122544,47 @@ function additivePoints(setOutcome) {
121982
122544
  return null;
121983
122545
  }
121984
122546
  const rawPoints = directText(pointValue);
121985
- const points = rawPoints === null ? Number.NaN : Number(rawPoints);
121986
- return Number.isFinite(points) && points > 0 ? points : null;
122547
+ const points = rawPoints === null ? null : parsedNumericValue(rawPoints, "float");
122548
+ return points !== null && points > 0 ? points : null;
122549
+ }
122550
+ function constantOutcomeValue(setOutcome, identifier, baseType) {
122551
+ if (!setOutcome || setOutcome.localName !== "qti-set-outcome-value" || !onlyAttributes(setOutcome, ["identifier"]) || setOutcome.attributes.identifier !== identifier || !onlyWhitespaceText(setOutcome.children)) {
122552
+ return null;
122553
+ }
122554
+ const values = childElements(setOutcome);
122555
+ const value = values.length === 1 ? values[0] : undefined;
122556
+ return value && value.localName === "qti-base-value" && onlyAttributes(value, ["base-type"]) && value.attributes["base-type"] === baseType ? directText(value) : null;
122557
+ }
122558
+ function feedbackOutcomeIsDeclared(item) {
122559
+ const feedbackOutcomes = childElements(item, "qti-outcome-declaration").filter((outcome) => outcome.attributes.identifier === "FEEDBACK");
122560
+ const feedback = feedbackOutcomes[0];
122561
+ return feedbackOutcomes.length === 1 && feedback !== undefined && onlyAttributes(feedback, ["identifier", "cardinality", "base-type"]) && feedback.attributes.cardinality === "single" && feedback.attributes["base-type"] === "identifier" && childElements(feedback).length === 0 && onlyWhitespaceText(feedback.children);
122562
+ }
122563
+ function binaryBranchRule(condition, declaration) {
122564
+ if (!onlyAttributes(condition, []) || !onlyWhitespaceText(condition.children)) {
122565
+ return null;
122566
+ }
122567
+ const branches = childElements(condition);
122568
+ const responseIf = branches[0];
122569
+ const responseElse = branches[1];
122570
+ if (branches.length !== 2 || responseIf?.localName !== "qti-response-if" || responseElse?.localName !== "qti-response-else" || !onlyAttributes(responseIf, []) || !onlyAttributes(responseElse, []) || !onlyWhitespaceText(responseIf.children) || !onlyWhitespaceText(responseElse.children)) {
122571
+ return null;
122572
+ }
122573
+ const ifChildren = childElements(responseIf);
122574
+ const elseChildren = childElements(responseElse);
122575
+ const [comparison, ifFeedback, ifScore] = ifChildren;
122576
+ const [elseFeedback, elseScore] = elseChildren;
122577
+ const comparisonPayload = comparison ? comparisonResult(comparison, declaration) : null;
122578
+ if (ifChildren.length !== 3 || elseChildren.length !== 2 || !comparisonPayload || constantOutcomeValue(ifFeedback, "FEEDBACK", "identifier") !== "CORRECT" || constantOutcomeValue(ifScore, "SCORE", "float") !== "1" || constantOutcomeValue(elseFeedback, "FEEDBACK", "identifier") !== "INCORRECT" || constantOutcomeValue(elseScore, "SCORE", "float") !== "0") {
122579
+ return null;
122580
+ }
122581
+ return {
122582
+ responseIdentifier: declaration.identifier,
122583
+ cardinality: declaration.cardinality,
122584
+ baseType: declaration.baseType,
122585
+ points: 1,
122586
+ comparison: comparisonPayload
122587
+ };
121987
122588
  }
121988
122589
  function additiveRule(condition, declarations) {
121989
122590
  const responseIdentifiers = new Set(declarations.keys());
@@ -122061,7 +122662,7 @@ function scoreDefaultIsZero(item) {
122061
122662
  }
122062
122663
  const values = childElements(defaults[0], "qti-value");
122063
122664
  const defaultText = values.length === 1 ? directText(values[0]) : null;
122064
- return defaultText !== null && defaultText !== "" && Number(defaultText) === 0;
122665
+ return defaultText !== null && parsedNumericValue(defaultText, "float") === 0;
122065
122666
  }
122066
122667
  function compileResponseProcessing(item, itemIdentifier, maxScore, declarations) {
122067
122668
  const processingElements = childElements(item, "qti-response-processing");
@@ -122084,7 +122685,17 @@ function compileResponseProcessing(item, itemIdentifier, maxScore, declarations)
122084
122685
  const response = declarations.get("RESPONSE");
122085
122686
  return response && declarations.size === 1 ? platformResult(itemIdentifier, maxScore, [matchRule(response, 1)]) : fallback(itemIdentifier, maxScore, "unsupported-response-processing");
122086
122687
  }
122087
- if (!onlyAttributes(processing, []) || !onlyWhitespaceText(processing.children) || processingRules.length === 0 || processingRules.some((rule) => rule.localName !== "qti-response-condition") || !scoreDefaultIsZero(item)) {
122688
+ if (!onlyAttributes(processing, []) || !onlyWhitespaceText(processing.children) || processingRules.length === 0 || processingRules.some((rule) => rule.localName !== "qti-response-condition")) {
122689
+ return fallback(itemIdentifier, maxScore, "unsupported-response-processing");
122690
+ }
122691
+ if (processingRules.length === 1 && declarations.size === 1 && scoreDefaultIsZero(item) && feedbackOutcomeIsDeclared(item)) {
122692
+ const declaration = [...declarations.values()][0];
122693
+ const rule = binaryBranchRule(processingRules[0], declaration);
122694
+ if (rule) {
122695
+ return platformResult(itemIdentifier, maxScore, [rule]);
122696
+ }
122697
+ }
122698
+ if (!scoreDefaultIsZero(item)) {
122088
122699
  return fallback(itemIdentifier, maxScore, "unsupported-response-processing");
122089
122700
  }
122090
122701
  const compiled = additiveRules(processingRules, declarations);
@@ -122137,7 +122748,7 @@ function compileQtiScoringArtifact(questions) {
122137
122748
  });
122138
122749
  return { items };
122139
122750
  }
122140
- function isRecord22(value) {
122751
+ function isRecord32(value) {
122141
122752
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
122142
122753
  }
122143
122754
  function stringField2(value) {
@@ -122221,13 +122832,13 @@ function dedupeStandards2(standards) {
122221
122832
  return [...deduped.values()];
122222
122833
  }
122223
122834
  function qtiAlignmentStandardsFromMetadata2(metadata2) {
122224
- if (!isRecord22(metadata2)) {
122835
+ if (!isRecord32(metadata2)) {
122225
122836
  return [];
122226
122837
  }
122227
122838
  const standards = [];
122228
122839
  const alignmentGroups = Array.isArray(metadata2.alignment) ? metadata2.alignment : [];
122229
122840
  for (const group of alignmentGroups) {
122230
- if (isRecord22(group)) {
122841
+ if (isRecord32(group)) {
122231
122842
  const source = stringField2(group.curriculum) || "QTI";
122232
122843
  const domains2 = Array.isArray(group.domains) ? group.domains : [];
122233
122844
  if (!domains2.length) {
@@ -122245,11 +122856,11 @@ function qtiAlignmentStandardsFromMetadata2(metadata2) {
122245
122856
  }
122246
122857
  } else {
122247
122858
  for (const domain3 of domains2) {
122248
- if (isRecord22(domain3)) {
122859
+ if (isRecord32(domain3)) {
122249
122860
  const domainName = stringField2(domain3.name) || undefined;
122250
122861
  const domainStandards = Array.isArray(domain3.standards) ? domain3.standards : [];
122251
122862
  for (const standard of domainStandards) {
122252
- if (isRecord22(standard)) {
122863
+ if (isRecord32(standard)) {
122253
122864
  const identifier = stringField2(standard.identifier).trim();
122254
122865
  const id = stringField2(standard.id).trim() || identifier;
122255
122866
  if (id) {
@@ -122272,13 +122883,13 @@ function qtiAlignmentStandardsFromMetadata2(metadata2) {
122272
122883
  return dedupeStandards2(standards);
122273
122884
  }
122274
122885
  function qtiStandardsFromMetadata(metadata2) {
122275
- if (!isRecord22(metadata2)) {
122886
+ if (!isRecord32(metadata2)) {
122276
122887
  return [];
122277
122888
  }
122278
122889
  const standards = [];
122279
122890
  const objectiveSets = Array.isArray(metadata2.learningObjectiveSet) ? metadata2.learningObjectiveSet : [];
122280
122891
  for (const set3 of objectiveSets) {
122281
- if (isRecord22(set3)) {
122892
+ if (isRecord32(set3)) {
122282
122893
  const source = stringField2(set3.source) || "QTI";
122283
122894
  const ids = Array.isArray(set3.learningObjectiveIds) ? set3.learningObjectiveIds : [];
122284
122895
  for (const id of ids) {
@@ -122333,13 +122944,27 @@ var NUMERIC_COMPARISON_ATTRIBUTES2;
122333
122944
  var RESPONSE_CARDINALITIES2;
122334
122945
  var RESPONSE_BASE_TYPES2;
122335
122946
  var BLANK_SENTINEL22 = "￿";
122947
+ var QTI_FLOAT_VALUE;
122336
122948
  var PLAYABLE_POINT_VALUE;
122337
122949
  var POINT_INTERACTION_TYPES2;
122338
122950
  var INDEPENDENT_PROCESSING_TAGS2;
122951
+ var PRIVATE_PLAYABLE_KEYS2;
122952
+ var trimmedNonEmptyString2;
122953
+ var finiteNumber22;
122954
+ var positiveFiniteNumber2;
122955
+ var IDENTIFIER_VALUE2;
122956
+ var DIRECTED_PAIR_VALUE2;
122957
+ var scoringMatchComparisonSchema2;
122958
+ var scoringNumericEqualComparisonSchema2;
122959
+ var scoringNumericEqualRoundedComparisonSchema2;
122960
+ var scoringRuleSchema2;
122961
+ var scoringRulesSchema2;
122962
+ var scoringItemSchema2;
122963
+ var assessmentScoringArtifactItemsSchema2;
122964
+ var assessmentScoringArtifactSchema2;
122339
122965
  var SUPPORTED_BASE_TYPES2;
122340
122966
  var SUPPORTED_CARDINALITIES2;
122341
122967
  var MATCH_CORRECT_TEMPLATES2;
122342
- var DIRECTED_PAIR_VALUE;
122343
122968
  var QtiScoringArtifactValidationError;
122344
122969
  var COMMON_CORE_MATH_FRAMEWORK_ALIASES2;
122345
122970
  var COMMON_CORE_ELA_FRAMEWORK_ALIASES2;
@@ -122351,6 +122976,7 @@ var init_qti = __esm(() => {
122351
122976
  init_timeback3();
122352
122977
  init_timeback3();
122353
122978
  init_timeback3();
122979
+ init_esm();
122354
122980
  EVENT_HANDLER_ATTRIBUTE = /^on/i;
122355
122981
  SCRIPT_SCHEMES2 = {
122356
122982
  javascript: ["java", "script:"].join(""),
@@ -122575,6 +123201,7 @@ var init_qti = __esm(() => {
122575
123201
  "string",
122576
123202
  "uri"
122577
123203
  ]);
123204
+ QTI_FLOAT_VALUE = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i;
122578
123205
  PLAYABLE_POINT_VALUE = /^-?\d+(?:\.\d+)? -?\d+(?:\.\d+)?$/;
122579
123206
  POINT_INTERACTION_TYPES2 = new Set([
122580
123207
  "select-point",
@@ -122593,6 +123220,93 @@ var init_qti = __esm(() => {
122593
123220
  "qti-sum",
122594
123221
  "qti-base-value"
122595
123222
  ]);
123223
+ PRIVATE_PLAYABLE_KEYS2 = new Set([
123224
+ "answer",
123225
+ "answerkey",
123226
+ "correctanswer",
123227
+ "correctresponse",
123228
+ "correctresponses",
123229
+ "correct",
123230
+ "correctvalue",
123231
+ "correctvalues",
123232
+ "iscorrect",
123233
+ "responseprocessing",
123234
+ "rule",
123235
+ "rules",
123236
+ "scoring",
123237
+ "scoringrules",
123238
+ "solution",
123239
+ "expected",
123240
+ "expectedanswer",
123241
+ "expectedvalue",
123242
+ "expectedvalues"
123243
+ ]);
123244
+ trimmedNonEmptyString2 = exports_external.string().min(1).refine((value) => value === value.trim());
123245
+ finiteNumber22 = exports_external.number().finite();
123246
+ positiveFiniteNumber2 = finiteNumber22.positive();
123247
+ IDENTIFIER_VALUE2 = /^[A-Za-z_][\w.-]*$/;
123248
+ DIRECTED_PAIR_VALUE2 = /^[A-Za-z_][\w.-]* [A-Za-z_][\w.-]*$/;
123249
+ scoringMatchComparisonSchema2 = exports_external.object({
123250
+ kind: exports_external.literal("match"),
123251
+ correctValues: exports_external.array(trimmedNonEmptyString2).min(1).refine((values) => new Set(values).size === values.length)
123252
+ }).strict();
123253
+ scoringNumericEqualComparisonSchema2 = exports_external.object({ kind: exports_external.literal("numeric-equal"), correctValue: finiteNumber22 }).strict();
123254
+ scoringNumericEqualRoundedComparisonSchema2 = exports_external.object({
123255
+ kind: exports_external.literal("numeric-equal-rounded"),
123256
+ correctValue: finiteNumber22,
123257
+ roundingMode: exports_external.enum(["decimalPlaces", "significantFigures"]),
123258
+ figures: exports_external.number().int().safe().nonnegative()
123259
+ }).strict();
123260
+ scoringRuleSchema2 = exports_external.object({
123261
+ responseIdentifier: trimmedNonEmptyString2,
123262
+ cardinality: exports_external.enum(["single", "multiple", "ordered"]),
123263
+ baseType: exports_external.enum(["string", "identifier", "integer", "float", "directedPair"]),
123264
+ points: positiveFiniteNumber2,
123265
+ comparison: exports_external.discriminatedUnion("kind", [
123266
+ scoringMatchComparisonSchema2,
123267
+ scoringNumericEqualComparisonSchema2,
123268
+ scoringNumericEqualRoundedComparisonSchema2
123269
+ ])
123270
+ }).strict().refine((rule) => rule.comparison.kind === "match" || rule.cardinality === "single" && (rule.baseType === "integer" || rule.baseType === "float")).refine((rule) => rule.baseType !== "directedPair" || rule.cardinality === "multiple" && rule.comparison.kind === "match").refine((rule) => rule.comparison.kind !== "match" || rule.cardinality !== "single" || rule.baseType === "string" || rule.comparison.correctValues.length === 1).refine((rule) => rule.comparison.kind !== "numeric-equal-rounded" || rule.comparison.roundingMode === "decimalPlaces" || rule.comparison.figures > 0).refine((rule) => {
123271
+ if (rule.comparison.kind !== "match") {
123272
+ return rule.baseType !== "integer" || Number.isSafeInteger(rule.comparison.correctValue);
123273
+ }
123274
+ if (rule.baseType === "integer" || rule.baseType === "float") {
123275
+ return rule.comparison.correctValues.every((value) => validNumericMatchValue2(value, rule.baseType));
123276
+ }
123277
+ if (rule.baseType === "identifier") {
123278
+ return rule.comparison.correctValues.every((value) => IDENTIFIER_VALUE2.test(value));
123279
+ }
123280
+ return rule.baseType !== "directedPair" || rule.comparison.correctValues.every((value) => DIRECTED_PAIR_VALUE2.test(value));
123281
+ });
123282
+ scoringRulesSchema2 = exports_external.array(scoringRuleSchema2).min(1).refine((rules) => new Set(rules.map((rule) => rule.responseIdentifier)).size === rules.length);
123283
+ scoringItemSchema2 = exports_external.discriminatedUnion("strategy", [
123284
+ exports_external.object({
123285
+ strategy: exports_external.literal("qti"),
123286
+ itemIdentifier: trimmedNonEmptyString2,
123287
+ maxScore: positiveFiniteNumber2,
123288
+ reason: exports_external.enum([
123289
+ "response-mapping",
123290
+ "area-mapping",
123291
+ "unsupported-base-type",
123292
+ "unsupported-cardinality",
123293
+ "invalid-correct-response",
123294
+ "no-scorable-response",
123295
+ "unsupported-processing-template",
123296
+ "unsupported-response-processing",
123297
+ "cross-response-processing",
123298
+ "inconsistent-max-score"
123299
+ ])
123300
+ }).strict(),
123301
+ exports_external.object({
123302
+ strategy: exports_external.literal("platform"),
123303
+ itemIdentifier: trimmedNonEmptyString2,
123304
+ maxScore: positiveFiniteNumber2,
123305
+ rules: scoringRulesSchema2
123306
+ }).strict()
123307
+ ]).refine((item) => item.strategy === "qti" || sameScore2(item.rules.reduce((total, rule) => total + rule.points, 0), item.maxScore));
123308
+ assessmentScoringArtifactItemsSchema2 = exports_external.array(scoringItemSchema2).refine((items) => new Set(items.map((item) => item.itemIdentifier)).size === items.length);
123309
+ assessmentScoringArtifactSchema2 = exports_external.object({ items: assessmentScoringArtifactItemsSchema2 }).strict();
122596
123310
  SUPPORTED_BASE_TYPES2 = new Set([
122597
123311
  "string",
122598
123312
  "identifier",
@@ -122610,7 +123324,6 @@ var init_qti = __esm(() => {
122610
123324
  "https://www.imsglobal.org/question/qti_v3p0/rptemplates/match_correct",
122611
123325
  "https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/match_correct"
122612
123326
  ].flatMap((template) => [template, `${template}.xml`]));
122613
- DIRECTED_PAIR_VALUE = /^[A-Za-z_][\w.-]* [A-Za-z_][\w.-]*$/;
122614
123327
  QtiScoringArtifactValidationError = class QtiScoringArtifactValidationError2 extends Error {
122615
123328
  constructor(message) {
122616
123329
  super(message);
@@ -124972,50 +125685,38 @@ var init_timeback_admin_service = __esm(async () => {
124972
125685
  init_timeback_mastery_completion_util()
124973
125686
  ]);
124974
125687
  });
124975
- function normalizedKey(key) {
124976
- return key.replaceAll(/[^a-z0-9]/gi, "").toLowerCase();
124977
- }
124978
- function containsPrivatePlayableKey(value) {
124979
- if (Array.isArray(value)) {
124980
- return value.some(containsPrivatePlayableKey);
124981
- }
124982
- if (!isRecord(value)) {
124983
- return false;
124984
- }
124985
- return Object.entries(value).some(([key, entry2]) => PRIVATE_PLAYABLE_KEYS.has(normalizedKey(key)) || containsPrivatePlayableKey(entry2));
124986
- }
124987
125688
  function playableSchema(identity) {
124988
125689
  return exports_external.object({
124989
125690
  contractVersion: exports_external.literal(PLAYCADEMY_ASSESSMENT_CONTRACT_VERSION),
124990
125691
  identifier: exports_external.literal(identity.qtiTestIdentifier),
124991
125692
  contentRevision: exports_external.literal(identity.kind === "assessment" ? identity.contentRevision : identity.sourceContentRevision),
124992
- title: trimmedNonEmptyString,
125693
+ title: trimmedNonEmptyString3,
124993
125694
  items: exports_external.array(playableItemSchema)
124994
125695
  }).strict();
124995
125696
  }
124996
125697
  function scoringSchema(identity) {
124997
125698
  if (identity.kind === "assessment") {
124998
125699
  return exports_external.object({
124999
- items: uniqueScoringItemsSchema,
125700
+ items: assessmentScoringArtifactItemsSchema2,
125000
125701
  reviewBank: exports_external.undefined(),
125001
125702
  diagnostic: diagnosticContextSchema.optional()
125002
- });
125703
+ }).strict();
125003
125704
  }
125004
125705
  return exports_external.object({
125005
- items: uniqueScoringItemsSchema,
125706
+ items: assessmentScoringArtifactItemsSchema2,
125006
125707
  reviewBank: exports_external.object({
125007
125708
  sourceContentRevision: exports_external.literal(identity.sourceContentRevision),
125008
125709
  bankRevision: exports_external.literal(identity.bankRevision),
125009
125710
  items: exports_external.array(exports_external.object({
125010
- itemIdentifier: trimmedNonEmptyString,
125711
+ itemIdentifier: trimmedNonEmptyString3,
125011
125712
  standards: exports_external.array(exports_external.unknown())
125012
- }))
125013
- }),
125713
+ }).strict())
125714
+ }).strict(),
125014
125715
  diagnostic: exports_external.undefined()
125015
- }).refine((payload) => payload.reviewBank.items.length === payload.items.length && payload.reviewBank.items.every((item, index2) => item.itemIdentifier === payload.items[index2]?.itemIdentifier));
125716
+ }).strict().refine((payload) => payload.reviewBank.items.length === payload.items.length && payload.reviewBank.items.every((item, index2) => item.itemIdentifier === payload.items[index2]?.itemIdentifier));
125016
125717
  }
125017
125718
  function assertPlayableAssessmentArtifact(payload, identity) {
125018
- if (!playableSchema(identity).safeParse(payload).success || containsPrivatePlayableKey(payload)) {
125719
+ if (!playableSchema(identity).safeParse(payload).success || containsPrivatePlayableAssessmentData(payload)) {
125019
125720
  throw new ValidationError("Playable assessment artifact payload is invalid or private");
125020
125721
  }
125021
125722
  }
@@ -125025,8 +125726,11 @@ function assertScoringAssessmentArtifact(payload, identity) {
125025
125726
  }
125026
125727
  }
125027
125728
  function assertScoringArtifactMatchesPlayable(assessment, scoring, identity) {
125028
- const scoringByIdentifier = new Map(scoring.items.map((item) => [item.itemIdentifier, item.maxScore]));
125029
- const playableMatches = assessment.items.every((item) => scoringByIdentifier.get(item.identifier) === item.maxScore);
125729
+ const scoringByIdentifier = new Map(scoring.items.map((item) => [item.itemIdentifier, item]));
125730
+ const playableMatches = assessment.items.every((item) => {
125731
+ const scoringItem = scoringByIdentifier.get(item.identifier);
125732
+ return scoringItem !== undefined && scoringItemMatchesPlayableItem(scoringItem, item);
125733
+ });
125030
125734
  const hasExactManagedMembership = identity.kind === "review-bank" || scoring.items.length === assessment.items.length;
125031
125735
  if (!playableMatches || !hasExactManagedMembership) {
125032
125736
  throw new ValidationError("Scoring artifact does not match the playable assessment");
@@ -125064,9 +125768,9 @@ function pinnedReviewBankFromAttempt(metadata2, bankIdentifier) {
125064
125768
  }))
125065
125769
  };
125066
125770
  }
125067
- var PRIVATE_PLAYABLE_KEYS;
125068
- var trimmedNonEmptyString;
125069
- var finiteNumber2;
125771
+ var trimmedNonEmptyString3;
125772
+ var finiteNumber3;
125773
+ var positiveFiniteNumber3;
125070
125774
  var nonnegativeInteger;
125071
125775
  var playableContentNodesSchema;
125072
125776
  var playableChoiceSchema;
@@ -125078,76 +125782,54 @@ var playableGapImageSchema;
125078
125782
  var playableInteractionBase;
125079
125783
  var playableInteractionSchema;
125080
125784
  var playableItemSchema;
125081
- var scoringRuleSchema;
125082
- var scoringItemSchema;
125083
- var uniqueScoringItemsSchema;
125084
125785
  var diagnosticContextSchema;
125085
125786
  var init_assessment_artifact_runtime_util = __esm(() => {
125086
125787
  init_esm();
125087
125788
  init_src();
125789
+ init_qti();
125088
125790
  init_errors2();
125089
- PRIVATE_PLAYABLE_KEYS = new Set([
125090
- "answer",
125091
- "answerkey",
125092
- "correctanswer",
125093
- "correctresponse",
125094
- "correctresponses",
125095
- "correct",
125096
- "correctvalue",
125097
- "correctvalues",
125098
- "iscorrect",
125099
- "responseprocessing",
125100
- "rule",
125101
- "rules",
125102
- "scoring",
125103
- "scoringrules",
125104
- "solution",
125105
- "expected",
125106
- "expectedanswer",
125107
- "expectedvalue",
125108
- "expectedvalues"
125109
- ]);
125110
- trimmedNonEmptyString = exports_external.string().min(1).refine((value) => value === value.trim());
125111
- finiteNumber2 = exports_external.number().finite();
125791
+ trimmedNonEmptyString3 = exports_external.string().min(1).refine((value) => value === value.trim());
125792
+ finiteNumber3 = exports_external.number().finite();
125793
+ positiveFiniteNumber3 = finiteNumber3.positive();
125112
125794
  nonnegativeInteger = exports_external.number().int().nonnegative();
125113
125795
  playableContentNodesSchema = exports_external.array(exports_external.unknown());
125114
125796
  playableChoiceSchema = exports_external.object({
125115
- identifier: trimmedNonEmptyString,
125797
+ identifier: trimmedNonEmptyString3,
125116
125798
  content: exports_external.string(),
125117
125799
  contentNodes: playableContentNodesSchema.optional()
125118
125800
  }).strict();
125119
125801
  playableMatchChoiceSchema = exports_external.object({
125120
- identifier: trimmedNonEmptyString,
125802
+ identifier: trimmedNonEmptyString3,
125121
125803
  content: exports_external.string(),
125122
125804
  contentNodes: playableContentNodesSchema.optional(),
125123
125805
  matchMax: nonnegativeInteger
125124
125806
  }).strict();
125125
125807
  playableGraphicSchema = exports_external.object({
125126
- src: trimmedNonEmptyString,
125127
- width: finiteNumber2.optional(),
125128
- height: finiteNumber2.optional(),
125808
+ src: trimmedNonEmptyString3,
125809
+ width: finiteNumber3.optional(),
125810
+ height: finiteNumber3.optional(),
125129
125811
  description: exports_external.string().optional()
125130
125812
  }).strict();
125131
125813
  playableHotspotSchema = exports_external.object({
125132
- identifier: trimmedNonEmptyString,
125814
+ identifier: trimmedNonEmptyString3,
125133
125815
  shape: exports_external.enum(["circle", "ellipse", "rect", "poly", "default"]),
125134
- coords: exports_external.array(finiteNumber2),
125816
+ coords: exports_external.array(finiteNumber3),
125135
125817
  matchMax: nonnegativeInteger.optional(),
125136
125818
  description: exports_external.string().optional()
125137
125819
  }).strict();
125138
125820
  playableGapTextSchema = exports_external.object({
125139
- identifier: trimmedNonEmptyString,
125821
+ identifier: trimmedNonEmptyString3,
125140
125822
  content: exports_external.string(),
125141
125823
  matchMax: nonnegativeInteger
125142
125824
  }).strict();
125143
125825
  playableGapImageSchema = exports_external.object({
125144
- identifier: trimmedNonEmptyString,
125826
+ identifier: trimmedNonEmptyString3,
125145
125827
  image: playableGraphicSchema.optional(),
125146
125828
  matchMax: nonnegativeInteger,
125147
125829
  description: exports_external.string().optional()
125148
125830
  }).strict();
125149
125831
  playableInteractionBase = {
125150
- responseIdentifier: trimmedNonEmptyString,
125832
+ responseIdentifier: trimmedNonEmptyString3,
125151
125833
  prompt: exports_external.string().optional()
125152
125834
  };
125153
125835
  playableInteractionSchema = exports_external.discriminatedUnion("type", [
@@ -125187,7 +125869,7 @@ var init_assessment_artifact_runtime_util = __esm(() => {
125187
125869
  ...playableInteractionBase,
125188
125870
  type: exports_external.literal("hottext"),
125189
125871
  segments: exports_external.array(exports_external.object({
125190
- identifier: trimmedNonEmptyString.optional(),
125872
+ identifier: trimmedNonEmptyString3.optional(),
125191
125873
  content: exports_external.string(),
125192
125874
  selectable: exports_external.boolean()
125193
125875
  }).strict()),
@@ -125207,7 +125889,7 @@ var init_assessment_artifact_runtime_util = __esm(() => {
125207
125889
  content: exports_external.string(),
125208
125890
  contentNodes: playableContentNodesSchema,
125209
125891
  gapTexts: exports_external.array(playableGapTextSchema),
125210
- gaps: exports_external.array(trimmedNonEmptyString),
125892
+ gaps: exports_external.array(trimmedNonEmptyString3),
125211
125893
  maxAssociations: nonnegativeInteger
125212
125894
  }).strict(),
125213
125895
  exports_external.object({
@@ -125241,59 +125923,17 @@ var init_assessment_artifact_runtime_util = __esm(() => {
125241
125923
  }).strict()
125242
125924
  ]);
125243
125925
  playableItemSchema = exports_external.object({
125244
- identifier: trimmedNonEmptyString,
125926
+ identifier: trimmedNonEmptyString3,
125245
125927
  title: exports_external.string(),
125246
125928
  prompt: exports_external.string(),
125247
125929
  promptContent: playableContentNodesSchema.optional(),
125248
- maxScore: finiteNumber2,
125930
+ maxScore: positiveFiniteNumber3,
125249
125931
  interactions: exports_external.array(playableInteractionSchema)
125250
125932
  }).strict();
125251
- scoringRuleSchema = exports_external.object({
125252
- responseIdentifier: trimmedNonEmptyString,
125253
- cardinality: exports_external.enum(["single", "multiple", "ordered"]),
125254
- baseType: exports_external.enum(["string", "identifier", "integer", "float", "directedPair"]),
125255
- points: finiteNumber2,
125256
- comparison: exports_external.discriminatedUnion("kind", [
125257
- exports_external.object({ kind: exports_external.literal("match"), correctValues: exports_external.array(exports_external.string()) }),
125258
- exports_external.object({ kind: exports_external.literal("numeric-equal"), correctValue: finiteNumber2 }),
125259
- exports_external.object({
125260
- kind: exports_external.literal("numeric-equal-rounded"),
125261
- correctValue: finiteNumber2,
125262
- roundingMode: exports_external.enum(["decimalPlaces", "significantFigures"]),
125263
- figures: exports_external.number().int().nonnegative()
125264
- })
125265
- ])
125266
- });
125267
- scoringItemSchema = exports_external.discriminatedUnion("strategy", [
125268
- exports_external.object({
125269
- strategy: exports_external.literal("qti"),
125270
- itemIdentifier: trimmedNonEmptyString,
125271
- maxScore: finiteNumber2,
125272
- reason: exports_external.enum([
125273
- "response-mapping",
125274
- "area-mapping",
125275
- "unsupported-base-type",
125276
- "unsupported-cardinality",
125277
- "invalid-correct-response",
125278
- "no-scorable-response",
125279
- "unsupported-processing-template",
125280
- "unsupported-response-processing",
125281
- "cross-response-processing",
125282
- "inconsistent-max-score"
125283
- ])
125284
- }),
125285
- exports_external.object({
125286
- strategy: exports_external.literal("platform"),
125287
- itemIdentifier: trimmedNonEmptyString,
125288
- maxScore: finiteNumber2,
125289
- rules: exports_external.array(scoringRuleSchema)
125290
- })
125291
- ]);
125292
- uniqueScoringItemsSchema = exports_external.array(scoringItemSchema).refine((items) => new Set(items.map((item) => item.itemIdentifier)).size === items.length);
125293
125933
  diagnosticContextSchema = exports_external.object({
125294
- routingRevision: trimmedNonEmptyString,
125934
+ routingRevision: trimmedNonEmptyString3,
125295
125935
  routingManifest: exports_external.record(exports_external.unknown())
125296
- });
125936
+ }).strict();
125297
125937
  });
125298
125938
 
125299
125939
  class AssessmentArtifactRuntimeLoader {
@@ -126211,16 +126851,8 @@ function prepareDiagnosticAssessmentResponses(assessment, current, input) {
126211
126851
  const update2 = { [input.itemIdentifier]: input.responses };
126212
126852
  validateAssessmentResponseUpdate(assessment, update2);
126213
126853
  const responses = applyAssessmentResponseUpdate(current, update2);
126214
- const item = assessment.items.find((candidate) => candidate.identifier === input.itemIdentifier);
126215
126854
  const itemResponses = responses[input.itemIdentifier];
126216
- const missingResponse = item?.interactions.find((interaction) => itemResponses?.[interaction.responseIdentifier] === undefined);
126217
- if (!item || !itemResponses || missingResponse) {
126218
- throw new AssessmentRuntimeError("INVALID_RESPONSE", `Diagnostic item ${input.itemIdentifier} requires a response for every interaction.`, {
126219
- itemIdentifier: input.itemIdentifier,
126220
- ...missingResponse ? { responseIdentifier: missingResponse.responseIdentifier } : {}
126221
- });
126222
- }
126223
- validateAssessmentResponses(assessment, { [input.itemIdentifier]: itemResponses });
126855
+ validateAssessmentDraftResponses(assessment, itemResponses ? { [input.itemIdentifier]: itemResponses } : {});
126224
126856
  return responses;
126225
126857
  }
126226
126858
  function validateAssessmentResponseUpdate(assessment, update2) {
@@ -127684,6 +128316,162 @@ var init_timeback_assessment_artifacts_util = __esm(() => {
127684
128316
  init_timeback_assessment_runtime_util();
127685
128317
  init_timeback_review_mapping_util();
127686
128318
  });
128319
+ async function buildSubmissionChildResultCommand(input) {
128320
+ const { submission, context: context2 } = input;
128321
+ const { isCorrect } = submission;
128322
+ const common2 = {
128323
+ attempt: input.attempt,
128324
+ administration: {
128325
+ submissionId: submission.submissionId,
128326
+ responseVersion: submission.responseVersion,
128327
+ itemIdentifier: submission.itemIdentifier
128328
+ },
128329
+ responses: input.responses ?? {},
128330
+ score: submission.score,
128331
+ grading: input.grading
128332
+ };
128333
+ if (context2.kind === "review") {
128334
+ return buildAssessmentChildResultCommand({
128335
+ ...common2,
128336
+ ...typeof isCorrect === "boolean" ? { isCorrect } : {},
128337
+ context: context2
128338
+ }, submission.submittedAt);
128339
+ }
128340
+ if (typeof isCorrect !== "boolean") {
128341
+ throw new Error("Diagnostic child results require determinate correctness");
128342
+ }
128343
+ return buildAssessmentChildResultCommand({ ...common2, isCorrect, context: context2 }, submission.submittedAt);
128344
+ }
128345
+ function reviewDefinitionWithoutStandards(definition) {
128346
+ return Object.fromEntries(Object.entries(definition).filter(([field]) => field !== "standards"));
128347
+ }
128348
+ function assessmentChildLineItemMetadataMatches(key, stored, expected) {
128349
+ if (key !== "playcademyReviewQuestionDefinition" || typeof stored !== "object" || stored === null || typeof expected !== "object" || expected === null) {
128350
+ return canonicalJson2(stored) === canonicalJson2(expected);
128351
+ }
128352
+ const storedDefinition = stored;
128353
+ const expectedDefinition = expected;
128354
+ const storedStandards = storedDefinition.standards;
128355
+ const expectedStandards = expectedDefinition.standards;
128356
+ if (!Array.isArray(storedStandards) || !Array.isArray(expectedStandards)) {
128357
+ return false;
128358
+ }
128359
+ const sameDefinition = canonicalJson2(reviewDefinitionWithoutStandards(storedDefinition)) === canonicalJson2(reviewDefinitionWithoutStandards(expectedDefinition));
128360
+ function contains(standards, candidate) {
128361
+ return standards.some((standard) => canonicalJson2(standard) === canonicalJson2(candidate));
128362
+ }
128363
+ return sameDefinition && (expectedStandards.every((standard) => contains(storedStandards, standard)) || storedStandards.every((standard) => contains(expectedStandards, standard)));
128364
+ }
128365
+ function assessmentChildLineItemMatches(stored, expected) {
128366
+ if (stored.status !== expected.status || stored.parentAssessmentLineItem?.sourcedId !== expected.parentAssessmentLineItem.sourcedId || stored.course?.sourcedId !== expected.course.sourcedId || stored.resultValueMin !== expected.resultValueMin || stored.resultValueMax !== expected.resultValueMax || typeof stored.metadata !== "object" || stored.metadata === null) {
128367
+ return false;
128368
+ }
128369
+ return Object.entries(expected.metadata).every(([key, value]) => assessmentChildLineItemMetadataMatches(key, stored.metadata[key], value));
128370
+ }
128371
+ function buildAssessmentChildResultUpdate(command, childLineItemId) {
128372
+ return {
128373
+ status: "active",
128374
+ assessmentLineItem: { sourcedId: childLineItemId },
128375
+ student: { sourcedId: command.payload.attempt.studentId },
128376
+ score: command.payload.score.earned,
128377
+ scoreDate: command.recordedAt,
128378
+ ...ASSESSMENT_ATTEMPT_COMPLETED,
128379
+ metadata: {
128380
+ [PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY]: command
128381
+ }
128382
+ };
128383
+ }
128384
+ async function classifyAssessmentChildResult(input) {
128385
+ const { result } = input;
128386
+ if (typeof result.metadata !== "object" || result.metadata === null || !(PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY in result.metadata)) {
128387
+ return { outcome: "conflict", reason: "malformed-command" };
128388
+ }
128389
+ const verification2 = await verifyAssessmentChildResultCommand(result.metadata);
128390
+ if (!verification2.ok) {
128391
+ return {
128392
+ outcome: "conflict",
128393
+ reason: verification2.reason === "payload_hash_mismatch" ? "payload-hash-mismatch" : "malformed-command"
128394
+ };
128395
+ }
128396
+ if (verification2.command.payloadHash !== input.command.payloadHash) {
128397
+ return { outcome: "conflict", reason: "payload-conflict" };
128398
+ }
128399
+ if (result.status === "tobedeleted") {
128400
+ return { outcome: "conflict", reason: "tombstoned" };
128401
+ }
128402
+ const expected = buildAssessmentChildResultUpdate(verification2.command, input.childLineItemId);
128403
+ const projectionChecks = [
128404
+ ["status-mismatch", result.status, expected.status],
128405
+ ["line-item-mismatch", result.assessmentLineItem?.sourcedId, input.childLineItemId],
128406
+ ["student-mismatch", result.student?.sourcedId, expected.student.sourcedId],
128407
+ ["score-mismatch", result.score, expected.score],
128408
+ ["score-date-mismatch", result.scoreDate, expected.scoreDate],
128409
+ ["score-status-mismatch", result.scoreStatus, expected.scoreStatus],
128410
+ ["in-progress-mismatch", result.inProgress, expected.inProgress]
128411
+ ];
128412
+ const mismatch = projectionChecks.find(([, stored, wanted]) => stored !== wanted);
128413
+ if (mismatch) {
128414
+ return { outcome: "conflict", reason: mismatch[0] };
128415
+ }
128416
+ const auxiliaryFields = [
128417
+ result.textScore,
128418
+ result.scoreScale,
128419
+ result.scorePercentile,
128420
+ result.comment,
128421
+ result.learningObjectiveSet,
128422
+ result.incomplete,
128423
+ result.late,
128424
+ result.missing
128425
+ ];
128426
+ if (auxiliaryFields.some((value) => value !== undefined && value !== null)) {
128427
+ return { outcome: "conflict", reason: "auxiliary-field-mismatch" };
128428
+ }
128429
+ return { outcome: "replay" };
128430
+ }
128431
+ function hasApiStatus(error88, statusCode) {
128432
+ return isApiError(error88) && error88.statusCode === statusCode;
128433
+ }
128434
+ async function persistAssessmentChildResult(input) {
128435
+ const { assessmentResults, childLineItemId } = input;
128436
+ const verification2 = await verifyAssessmentChildResultCommand(input.command);
128437
+ if (!verification2.ok) {
128438
+ throw new Error(`Invalid assessment child result command: ${verification2.reason}`);
128439
+ }
128440
+ const { command } = verification2;
128441
+ const resultId = await assessmentChildResultId(command.payload.attempt.attemptId, command.payload.administration.submissionId);
128442
+ async function classifyOccupant() {
128443
+ const resolution = await classifyAssessmentChildResult({
128444
+ result: await assessmentResults.get(resultId),
128445
+ command,
128446
+ childLineItemId
128447
+ });
128448
+ return resolution.outcome === "replay" ? { outcome: "replay", resultId } : { outcome: "conflict", resultId, reason: resolution.reason };
128449
+ }
128450
+ try {
128451
+ return await classifyOccupant();
128452
+ } catch (error88) {
128453
+ if (!hasApiStatus(error88, 404)) {
128454
+ throw error88;
128455
+ }
128456
+ }
128457
+ try {
128458
+ await assessmentResults.create({
128459
+ sourcedId: resultId,
128460
+ ...buildAssessmentChildResultUpdate(command, childLineItemId)
128461
+ });
128462
+ return { outcome: "created", resultId };
128463
+ } catch (error88) {
128464
+ if (!hasApiStatus(error88, 409)) {
128465
+ throw error88;
128466
+ }
128467
+ }
128468
+ return classifyOccupant();
128469
+ }
128470
+ var init_timeback_assessment_child_result_util = __esm(async () => {
128471
+ init_src();
128472
+ init_assessment_runtime2();
128473
+ await init_errors8();
128474
+ });
127687
128475
 
127688
128476
  class AssessmentPreparationFlights {
127689
128477
  limit;
@@ -127816,6 +128604,7 @@ function buildPreparedAssessmentMetadata(plan, timestamp6, testName) {
127816
128604
  testName,
127817
128605
  routingRevision: plan.diagnostic.routingRevision,
127818
128606
  ledger: [],
128607
+ routingSnapshots: [],
127819
128608
  state: plan.diagnostic.initialState
127820
128609
  }
127821
128610
  };
@@ -127867,7 +128656,7 @@ function declarationNeutralQtiXml(source) {
127867
128656
  }
127868
128657
  function managedMetadataRecord(metadata2, key) {
127869
128658
  const managed = metadata2?.[key];
127870
- return isRecord5(managed) ? managed : null;
128659
+ return isRecord4(managed) ? managed : null;
127871
128660
  }
127872
128661
  function sourceMetadata(metadata2) {
127873
128662
  return Object.fromEntries(Object.entries(metadata2 ?? {}).filter(([key]) => !GENERATED_METADATA_KEYS.has(key)));
@@ -128068,7 +128857,7 @@ function assertManagedAssessmentPublication(test, expected) {
128068
128857
  function managedAssessmentDiagnosticRoutingManifest(test, assessmentKey) {
128069
128858
  const managed = managedMetadataRecord(test.metadata, PLAYCADEMY_MANAGED_ASSESSMENT_METADATA_KEY);
128070
128859
  const manifest = managed?.diagnosticRoutingManifest;
128071
- return managed?.assessmentKey === assessmentKey && isRecord5(manifest) ? manifest : null;
128860
+ return managed?.assessmentKey === assessmentKey && isRecord4(manifest) ? manifest : null;
128072
128861
  }
128073
128862
  async function assertManagedAssessmentItem(item, expected) {
128074
128863
  const managed = managedMetadataRecord(item.metadata, PLAYCADEMY_MANAGED_ITEM_METADATA_KEY);
@@ -128111,6 +128900,192 @@ var init_timeback_assessment_publication_util = __esm(() => {
128111
128900
  ]);
128112
128901
  MANAGED_ASSET_URL = new RegExp(`https?://[^\\s"')]+/(${ASSESSMENT_ASSET_KEY_PREFIX}v1/sha256/[a-f0-9]{64}\\.[a-z0-9]+)`, "gi");
128113
128902
  });
128903
+ function assessmentChildProjectionIsTerminal(result) {
128904
+ const state = { inProgress: result.inProgress ?? "", scoreStatus: result.scoreStatus };
128905
+ return isAssessmentAttemptAwaitingAward(state) || isAssessmentAttemptCompleted(state);
128906
+ }
128907
+ async function reconcileReviewChildResults({
128908
+ initial,
128909
+ writeOpen,
128910
+ crossBarrier,
128911
+ readLatest,
128912
+ restoreTerminal
128913
+ }) {
128914
+ if (initial.metadata.purpose !== "review") {
128915
+ throw new Error("A standards-review request selected a non-review attempt");
128916
+ }
128917
+ if (assessmentChildProjectionIsTerminal(initial.result)) {
128918
+ const settled = { result: initial.result, metadata: initial.metadata };
128919
+ await restoreTerminal(settled);
128920
+ return settled;
128921
+ }
128922
+ await writeOpen();
128923
+ await crossBarrier();
128924
+ const current = await readLatest();
128925
+ if (current.metadata.purpose !== "review") {
128926
+ throw new Error("A standards-review request selected a non-review attempt");
128927
+ }
128928
+ const reviewed = { result: current.result, metadata: current.metadata };
128929
+ if (assessmentChildProjectionIsTerminal(current.result)) {
128930
+ await restoreTerminal(reviewed);
128931
+ }
128932
+ return reviewed;
128933
+ }
128934
+ var init_timeback_assessment_reconciliation_util = __esm(() => {
128935
+ init_assessment_runtime2();
128936
+ });
128937
+ function resolveDiagnosticItemReplay(metadata2, input) {
128938
+ const priorIndex = metadata2.itemSubmissions.findIndex((submission) => submission.submissionId === input.submissionId);
128939
+ if (priorIndex === -1) {
128940
+ return { action: "missing" };
128941
+ }
128942
+ const prior = metadata2.itemSubmissions[priorIndex];
128943
+ const ledgerEntry = metadata2.diagnostic.ledger[priorIndex];
128944
+ if (!prior || !ledgerEntry || prior.itemIdentifier !== input.itemIdentifier || ledgerEntry.routingNodeKey !== input.routingNodeKey || !itemResponseUpdateMatches(metadata2.responses[input.itemIdentifier], input.responses)) {
128945
+ return {
128946
+ action: "conflict",
128947
+ failure: assessmentFlowViolation("This diagnostic submission ID was already used for a different request.", {
128948
+ submissionId: input.submissionId,
128949
+ routingNodeKey: input.routingNodeKey,
128950
+ itemIdentifier: input.itemIdentifier
128951
+ })
128952
+ };
128953
+ }
128954
+ return {
128955
+ action: "replay",
128956
+ submission: prior,
128957
+ ledgerEntry,
128958
+ priorIndex,
128959
+ routing: metadata2.diagnostic.routingSnapshots?.[priorIndex]
128960
+ };
128961
+ }
128962
+ function buildAssessmentItemReplayResult(attemptId, metadata2, submission) {
128963
+ return {
128964
+ attemptId,
128965
+ responseVersion: metadata2.responseVersion,
128966
+ status: "in_progress",
128967
+ responses: metadata2.responses,
128968
+ itemSubmissions: metadata2.itemSubmissions,
128969
+ submission
128970
+ };
128971
+ }
128972
+ function diagnosticReceipt(submission, ledgerEntry) {
128973
+ return {
128974
+ submissionId: submission.submissionId,
128975
+ routingNodeKey: ledgerEntry.routingNodeKey,
128976
+ itemIdentifier: submission.itemIdentifier,
128977
+ submittedAt: submission.submittedAt,
128978
+ responseVersion: submission.responseVersion,
128979
+ answered: submission.answered,
128980
+ score: submission.score,
128981
+ isCorrect: ledgerEntry.isCorrect
128982
+ };
128983
+ }
128984
+ function buildDiagnosticItemReplayResult(attemptId, submission, ledgerEntry, routing) {
128985
+ return {
128986
+ attemptId,
128987
+ responseVersion: submission.responseVersion,
128988
+ status: "in_progress",
128989
+ routing,
128990
+ submission: diagnosticReceipt(submission, ledgerEntry)
128991
+ };
128992
+ }
128993
+ function requireAssessmentMutationAccess(attemptId, integrationStatus, enrollment) {
128994
+ if (!enrollment || integrationStatus === "deactivated") {
128995
+ throw AssessmentRuntimeError.from(assessmentAttemptUnauthorized(attemptId));
128996
+ }
128997
+ return enrollment;
128998
+ }
128999
+ function committedReviewItemResults(metadata2, assessment) {
129000
+ const available = new Set(assessment.items.map((item) => item.identifier));
129001
+ const seen = new Set;
129002
+ return metadata2.itemSubmissions.map((submission) => {
129003
+ if (!available.has(submission.itemIdentifier) || seen.has(submission.itemIdentifier)) {
129004
+ throw AssessmentRuntimeError.from(assessmentFlowViolation("The persisted review item-submission ledger is corrupt.", {
129005
+ itemIdentifier: submission.itemIdentifier
129006
+ }));
129007
+ }
129008
+ seen.add(submission.itemIdentifier);
129009
+ return {
129010
+ itemIdentifier: submission.itemIdentifier,
129011
+ score: submission.score,
129012
+ isCorrect: submission.isCorrect
129013
+ };
129014
+ });
129015
+ }
129016
+ var init_timeback_assessment_replay_util = __esm(() => {
129017
+ init_assessment_runtime2();
129018
+ init_errors2();
129019
+ });
129020
+ function diagnosticRoutingFallbackReason(artifact, routingRevision) {
129021
+ if (!artifact) {
129022
+ return "artifact-unavailable";
129023
+ }
129024
+ if (!artifact.diagnostic) {
129025
+ return "diagnostic-context-missing";
129026
+ }
129027
+ return artifact.diagnostic.routingRevision === routingRevision ? "routing-manifest-invalid" : "routing-revision-mismatch";
129028
+ }
129029
+ function assertDiagnosticItemsAvailable(definitionId, manifest, assessment) {
129030
+ const itemIdentifiers = new Set(assessment.items.map((item) => item.identifier));
129031
+ const unknownItem = manifest.nodes.find((node) => !itemIdentifiers.has(node.itemIdentifier));
129032
+ if (unknownItem) {
129033
+ throw new AssessmentRuntimeError("SELECTED_TEST_VERSION_UNAVAILABLE", `Diagnostic routing references unavailable item ${unknownItem.itemIdentifier}.`, {
129034
+ definitionId,
129035
+ nodeKey: unknownItem.key,
129036
+ itemIdentifier: unknownItem.itemIdentifier
129037
+ });
129038
+ }
129039
+ }
129040
+ function replayAttemptDiagnosticRouting(metadata2, manifest) {
129041
+ const replayed = replayDiagnosticRouting(manifest, metadata2.diagnostic.ledger);
129042
+ if (!replayed.ok || canonicalJson2(replayed.state) !== canonicalJson2(metadata2.diagnostic.state)) {
129043
+ throw AssessmentRuntimeError.from(assessmentFlowViolation("The persisted diagnostic routing state is corrupt.", {
129044
+ definitionId: metadata2.diagnostic.definitionId,
129045
+ ...replayed.ok ? {} : { failure: replayed.failure }
129046
+ }));
129047
+ }
129048
+ return replayed.state;
129049
+ }
129050
+ async function resolveCompiledDiagnosticRouting(metadata2, assessment, artifact) {
129051
+ const diagnostic = artifact?.diagnostic;
129052
+ if (diagnostic?.routingRevision !== metadata2.diagnostic.routingRevision) {
129053
+ return null;
129054
+ }
129055
+ const initialized = initializeDiagnosticRouting(diagnostic.routingManifest);
129056
+ if (!initialized.ok) {
129057
+ return null;
129058
+ }
129059
+ const routingRevision = await diagnosticRoutingRevision(initialized.manifest);
129060
+ if (routingRevision !== metadata2.diagnostic.routingRevision) {
129061
+ return null;
129062
+ }
129063
+ assertDiagnosticItemsAvailable(metadata2.diagnostic.definitionId, initialized.manifest, assessment);
129064
+ return {
129065
+ manifest: initialized.manifest,
129066
+ routingRevision,
129067
+ state: replayAttemptDiagnosticRouting(metadata2, initialized.manifest)
129068
+ };
129069
+ }
129070
+ function summarizeDiagnosticSubmission(metadata2, manifest) {
129071
+ const state = replayAttemptDiagnosticRouting(metadata2, manifest);
129072
+ const tracks = diagnosticTrackResults(state);
129073
+ if (!tracks) {
129074
+ throw AssessmentRuntimeError.from(assessmentFlowViolation("Diagnostic routing must be ready to complete before final submit.", { routingStatus: state.status }));
129075
+ }
129076
+ if (metadata2.itemSubmissions.length === 0) {
129077
+ throw AssessmentRuntimeError.from(assessmentFlowViolation("A diagnostic cannot be finalized without an administered item."));
129078
+ }
129079
+ const possible = metadata2.itemSubmissions.reduce((total, submission) => total + submission.score.possible, 0);
129080
+ const score = aggregateAssessmentScore(metadata2.itemSubmissions.map((submission) => submission.score.earned), possible);
129081
+ const correctQuestions = countCorrectQuestions(metadata2.itemSubmissions.map((submission) => submission.isCorrect));
129082
+ return { tracks, score, correctQuestions };
129083
+ }
129084
+ var init_timeback_assessment_routing_util = __esm(() => {
129085
+ init_assessment_runtime2();
129086
+ init_errors2();
129087
+ init_timeback_assessment_runtime_util();
129088
+ });
128114
129089
  function validateAssessmentStatusTransition(current, next) {
128115
129090
  if (current === next) {
128116
129091
  return;
@@ -128187,6 +129162,157 @@ function validateUniqueAssessmentIdentifiers(testIdentifiers) {
128187
129162
  var init_timeback_assessment_rules_util = __esm(() => {
128188
129163
  init_errors2();
128189
129164
  });
129165
+ async function scoreAssessmentItems({
129166
+ assessment,
129167
+ responses,
129168
+ artifactIdentity,
129169
+ purpose,
129170
+ itemIdentifiers,
129171
+ artifact,
129172
+ processResponse,
129173
+ onEvent
129174
+ }) {
129175
+ const scoringItems = assessmentScoringRequests(assessment, responses).filter((item) => !itemIdentifiers || itemIdentifiers.has(item.itemIdentifier));
129176
+ const artifactItems = new Map(artifact?.items.map((item) => [item.itemIdentifier, item]));
129177
+ const scored = new Map;
129178
+ const qti2 = [];
129179
+ for (const item of scoringItems) {
129180
+ const artifactItem = artifactItems.get(item.itemIdentifier);
129181
+ let fallback2 = null;
129182
+ if (!artifact) {
129183
+ fallback2 = { reason: "artifact-unavailable" };
129184
+ } else if (!artifactItem) {
129185
+ fallback2 = { reason: "artifact-item-missing" };
129186
+ } else if (artifactItem.strategy === "qti") {
129187
+ fallback2 = { reason: "artifact-qti", artifactReason: artifactItem.reason };
129188
+ } else {
129189
+ const startedAt = Date.now();
129190
+ try {
129191
+ scored.set(item.itemIdentifier, {
129192
+ ...scorePlatformAssessmentItem(artifactItem, responses[item.itemIdentifier]),
129193
+ grading: PLATFORM_CHILD_GRADING
129194
+ });
129195
+ onEvent("assessment.item_scoring", {
129196
+ "app.assessment.qti_item_identifier": item.itemIdentifier,
129197
+ "app.assessment.purpose": purpose,
129198
+ "app.assessment.artifact_identity_kind": artifactIdentity.kind,
129199
+ "app.assessment.grading_source": "platform-artifact",
129200
+ "app.assessment.grading_duration_ms": Date.now() - startedAt
129201
+ });
129202
+ } catch (error88) {
129203
+ fallback2 = { reason: "platform-grader-error", error: error88 };
129204
+ }
129205
+ }
129206
+ if (fallback2) {
129207
+ qti2.push({ item, ...fallback2 });
129208
+ }
129209
+ }
129210
+ if (qti2.length > 0) {
129211
+ const fallbackTasks = qti2.flatMap(({ item, reason, artifactReason, error: graderError }) => {
129212
+ const interactionResults = [];
129213
+ let startedAt;
129214
+ let completedInteractions = 0;
129215
+ let failed = false;
129216
+ const omittedResponseCount = item.requests.filter((request) => responses[item.itemIdentifier]?.[request.identifier] === undefined).length;
129217
+ let qtiRemoteInteractionCount = 0;
129218
+ let syntheticIncorrectInteractionCount = 0;
129219
+ function fallbackEvent(outcome) {
129220
+ onEvent("assessment.item_scoring_fallback", {
129221
+ "app.assessment.qti_item_identifier": item.itemIdentifier,
129222
+ "app.assessment.purpose": purpose,
129223
+ "app.assessment.artifact_identity_kind": artifactIdentity.kind,
129224
+ "app.assessment.grading_source": omittedResponseCount > 0 ? "platform-qti-adapter" : "timeback-qti",
129225
+ "app.assessment.fallback_reason": reason,
129226
+ "app.assessment.fallback_duration_ms": startedAt === undefined ? 0 : Date.now() - startedAt,
129227
+ "app.assessment.qti_remote_interaction_count": qtiRemoteInteractionCount,
129228
+ "app.assessment.synthetic_incorrect_interaction_count": syntheticIncorrectInteractionCount,
129229
+ ...artifactReason ? { "app.assessment.artifact_fallback_reason": artifactReason } : {},
129230
+ ...graderError ? {
129231
+ "app.assessment.platform_grader_exception_type": errorType(graderError)
129232
+ } : {},
129233
+ ...outcome
129234
+ });
129235
+ }
129236
+ function completeItem() {
129237
+ const earned = assessmentItemEarnedScore(interactionResults.map((result) => Number.isFinite(result.score) ? result.score : 0), item.maxScore);
129238
+ const possible = item.maxScore;
129239
+ fallbackEvent({ "app.assessment.fallback_outcome": "succeeded" });
129240
+ scored.set(item.itemIdentifier, {
129241
+ itemIdentifier: item.itemIdentifier,
129242
+ score: {
129243
+ earned,
129244
+ possible,
129245
+ normalized: possible === 0 ? 0 : earned / possible
129246
+ },
129247
+ isCorrect: assessmentItemCorrectness(interactionResults.map((result) => result.isCorrect)),
129248
+ grading: omittedResponseCount > 0 ? PLATFORM_QTI_ADAPTER_CHILD_GRADING : QTI_CHILD_GRADING
129249
+ });
129250
+ }
129251
+ if (item.requests.length === 0) {
129252
+ completeItem();
129253
+ return [];
129254
+ }
129255
+ return item.requests.map((request, index2) => async () => {
129256
+ startedAt ??= Date.now();
129257
+ try {
129258
+ const response = responses[item.itemIdentifier]?.[request.identifier];
129259
+ if (response === undefined) {
129260
+ syntheticIncorrectInteractionCount += 1;
129261
+ interactionResults[index2] = { score: 0, isCorrect: false };
129262
+ } else {
129263
+ qtiRemoteInteractionCount += 1;
129264
+ interactionResults[index2] = await processResponse(item.itemIdentifier, request);
129265
+ }
129266
+ completedInteractions += 1;
129267
+ if (completedInteractions === item.requests.length && !failed) {
129268
+ completeItem();
129269
+ }
129270
+ } catch (error88) {
129271
+ if (!failed) {
129272
+ failed = true;
129273
+ fallbackEvent({
129274
+ "app.assessment.fallback_outcome": "failed",
129275
+ "exception.type": errorType(error88),
129276
+ "app.error.message": errorMessage2(error88)
129277
+ });
129278
+ }
129279
+ throw error88;
129280
+ }
129281
+ });
129282
+ });
129283
+ await runWithConcurrency(fallbackTasks, SCORING_CONCURRENCY, (task) => task());
129284
+ }
129285
+ return scoringItems.map((item) => {
129286
+ const result = scored.get(item.itemIdentifier);
129287
+ if (!result) {
129288
+ throw new Error(`Assessment item ${item.itemIdentifier} was not scored`);
129289
+ }
129290
+ return result;
129291
+ });
129292
+ }
129293
+ var QTI_CHILD_GRADING;
129294
+ var PLATFORM_QTI_ADAPTER_CHILD_GRADING;
129295
+ var PLATFORM_CHILD_GRADING;
129296
+ var SCORING_CONCURRENCY = 4;
129297
+ var init_timeback_assessment_scoring_util = __esm(() => {
129298
+ init_assessment_runtime2();
129299
+ init_assessment_artifact_envelope_util();
129300
+ init_timeback_assessment_runtime_util();
129301
+ QTI_CHILD_GRADING = {
129302
+ source: "timeback-qti",
129303
+ graderVersion: "timeback-qti-process-response-v1"
129304
+ };
129305
+ PLATFORM_QTI_ADAPTER_CHILD_GRADING = {
129306
+ source: "platform-qti-adapter",
129307
+ graderVersion: "platform-qti-adapter-v1",
129308
+ qtiGraderVersion: "timeback-qti-process-response-v1"
129309
+ };
129310
+ PLATFORM_CHILD_GRADING = {
129311
+ source: "platform-artifact",
129312
+ artifactVersion: `assessment-artifact-v${ASSESSMENT_ARTIFACT_SCHEMA_VERSION}`,
129313
+ graderVersion: `platform-grader-v${ASSESSMENT_GRADER_PROTOCOL_VERSION}`
129314
+ };
129315
+ });
128190
129316
  function qtiItemHref(client2, itemIdentifier) {
128191
129317
  return `${client2.getQtiBaseUrl()}/assessment-items/${itemIdentifier}`;
128192
129318
  }
@@ -128262,6 +129388,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
128262
129388
  init_spans();
128263
129389
  init_assessment_runtime2();
128264
129390
  init_constants3();
129391
+ init_qti();
128265
129392
  init_types2();
128266
129393
  init_uuid();
128267
129394
  init_errors2();
@@ -128270,13 +129397,18 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
128270
129397
  init_timeback_assessment_artifacts_util();
128271
129398
  init_timeback_assessment_preparation_util();
128272
129399
  init_timeback_assessment_publication_util();
129400
+ init_timeback_assessment_reconciliation_util();
129401
+ init_timeback_assessment_replay_util();
129402
+ init_timeback_assessment_routing_util();
128273
129403
  init_timeback_assessment_rules_util();
128274
129404
  init_timeback_assessment_runtime_util();
129405
+ init_timeback_assessment_scoring_util();
128275
129406
  init_timeback_qti_hydration_util();
128276
129407
  await __promiseAll([
128277
129408
  init_dist5(),
128278
129409
  init_errors8(),
128279
- init_assessment_artifact_loader_util()
129410
+ init_assessment_artifact_loader_util(),
129411
+ init_timeback_assessment_child_result_util()
128280
129412
  ]);
128281
129413
  AssessmentPreparationPrerequisiteError = class AssessmentPreparationPrerequisiteError2 extends Error {
128282
129414
  original;
@@ -128657,12 +129789,19 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
128657
129789
  activityId: params.input.activityId,
128658
129790
  source
128659
129791
  }));
128660
- await this.provisionPreparationPrerequisites(() => runWithConcurrency(assessment.items, TimebackAssessmentRuntimeService2.SCORING_CONCURRENCY, (item) => this.ensureReviewQuestionLineItem({
128661
- parentLineItemId,
128662
- integration: context2.integration,
128663
- bank: catalog.bank,
128664
- item
128665
- })));
129792
+ await this.provisionPreparationPrerequisites(() => runWithConcurrency(assessment.items, TimebackAssessmentRuntimeService2.SCORING_CONCURRENCY, (item) => {
129793
+ const indexed = catalog.bank.items.find((candidate) => candidate.itemIdentifier === item.identifier);
129794
+ if (!indexed) {
129795
+ throw new Error(`Review-bank index is missing item ${item.identifier}`);
129796
+ }
129797
+ return this.ensureReviewQuestionLineItem({
129798
+ parentLineItemId,
129799
+ integration: context2.integration,
129800
+ bankRevision: catalog.bank.bankRevision,
129801
+ standards: [...indexed.standards],
129802
+ item
129803
+ });
129804
+ }));
128666
129805
  const attempt = await this.provisionPreparationPrerequisites(() => this.claimAttemptId({
128667
129806
  gameId: params.gameId,
128668
129807
  studentId: params.studentId,
@@ -128851,6 +129990,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
128851
129990
  testName: assessment.title,
128852
129991
  routingRevision: routing.routingRevision,
128853
129992
  ledger: [],
129993
+ routingSnapshots: [],
128854
129994
  state: routing.state
128855
129995
  }
128856
129996
  };
@@ -129010,15 +130150,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
129010
130150
  grade: context2.integration.grade
129011
130151
  })
129012
130152
  });
129013
- await this.ensureReviewChildResults({
129014
- result,
129015
- metadata: metadata2,
129016
- integration: context2.integration,
129017
- source,
129018
- assessment,
129019
- existingChildResults: refreshedListing.reviewChildren,
129020
- lookupMissingResults: false
129021
- });
129022
130153
  await this.persistPendingSupersessionsBestEffort(db2, pendingSupersessions);
129023
130154
  this.recordReviewPreparationPhase("provisioning", provisioningStartedAt);
129024
130155
  this.recordReviewPreparationPhase("total", preparationStartedAt, {
@@ -129153,13 +130284,39 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
129153
130284
  let preparationAttempts = 1;
129154
130285
  while (true) {
129155
130286
  const committed = await this.deps.assessmentRuntimeLock(this.deps.db, [assessmentRuntimeAttemptLockKey(params.attemptId)], async (db2) => {
129156
- const attempt = await this.authorizeAttempt(params, {
130287
+ const { attempt, enrollment } = await this.resolveAttemptAuthorization(params, {
129157
130288
  db: db2,
129158
130289
  developerAccessValidated: true
129159
130290
  });
129160
130291
  if (this.isPlatformRoutedDiagnosticMetadata(attempt.metadata)) {
130292
+ this.requireActiveAttempt({ attempt, enrollment }, params.attemptId);
129161
130293
  throw AssessmentRuntimeError.from(assessmentFlowViolation("Platform-routed diagnostic submissions require routingNodeKey.", { flow: "platform-routed-item-submit" }));
129162
130294
  }
130295
+ const replay = resolveAssessmentItemReplay(attempt.metadata, input);
130296
+ if (replay.action === "conflict") {
130297
+ this.requireActiveAttempt({ attempt, enrollment }, params.attemptId);
130298
+ throw AssessmentRuntimeError.from(replay.failure);
130299
+ }
130300
+ if (replay.action === "replay") {
130301
+ const prior = replay.submission;
130302
+ const child2 = attempt.metadata.purpose === "review" ? await this.prepareReviewAssessmentChildResult({
130303
+ attempt,
130304
+ metadata: attempt.metadata,
130305
+ submission: prior
130306
+ }) : null;
130307
+ return {
130308
+ action: "complete",
130309
+ response: buildAssessmentItemReplayResult(attempt.result.sourcedId, attempt.metadata, prior),
130310
+ checkpoint: child2,
130311
+ projection: attempt.metadata.purpose === "review" ? {
130312
+ result: attempt.result,
130313
+ metadata: attempt.metadata,
130314
+ itemIdentifier: input.itemIdentifier,
130315
+ integration: attempt.integration
130316
+ } : null
130317
+ };
130318
+ }
130319
+ this.requireActiveAttempt({ attempt, enrollment }, params.attemptId);
129163
130320
  this.assertInProgress(attempt.result);
129164
130321
  if (!preview || preview.responseVersion !== attempt.metadata.responseVersion || preview.submissionId !== input.submissionId || preview.itemIdentifier !== input.itemIdentifier) {
129165
130322
  if (preparationAttempts >= TimebackAssessmentRuntimeService2.ITEM_SUBMISSION_MAX_PREPARATION_ATTEMPTS) {
@@ -129178,6 +130335,11 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
129178
130335
  throw AssessmentRuntimeError.from(transition.failure);
129179
130336
  }
129180
130337
  if (transition.action === "replay") {
130338
+ const child2 = attempt.metadata.purpose === "review" ? await this.prepareReviewAssessmentChildResult({
130339
+ attempt,
130340
+ metadata: attempt.metadata,
130341
+ submission: transition.submission
130342
+ }) : null;
129181
130343
  return {
129182
130344
  action: "complete",
129183
130345
  response: {
@@ -129188,10 +130350,12 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
129188
130350
  itemSubmissions: attempt.metadata.itemSubmissions,
129189
130351
  submission: transition.submission
129190
130352
  },
130353
+ checkpoint: child2,
129191
130354
  projection: attempt.metadata.purpose === "review" ? {
129192
130355
  result: attempt.result,
129193
130356
  metadata: attempt.metadata,
129194
- itemIdentifier: input.itemIdentifier
130357
+ itemIdentifier: input.itemIdentifier,
130358
+ integration: attempt.integration
129195
130359
  } : null
129196
130360
  };
129197
130361
  }
@@ -129207,6 +130371,11 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
129207
130371
  itemSubmissions: completed.itemSubmissions,
129208
130372
  updatedAt: submittedAt
129209
130373
  };
130374
+ const child = metadata2.purpose === "review" ? await this.prepareReviewAssessmentChildResult({
130375
+ attempt,
130376
+ metadata: metadata2,
130377
+ submission: completed.submission
130378
+ }) : null;
129210
130379
  await this.putResultMetadata(attempt.result, metadata2);
129211
130380
  return {
129212
130381
  action: "complete",
@@ -129218,10 +130387,12 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
129218
130387
  itemSubmissions: completed.itemSubmissions,
129219
130388
  submission: completed.submission
129220
130389
  },
130390
+ checkpoint: child,
129221
130391
  projection: metadata2.purpose === "review" ? {
129222
130392
  result: attempt.result,
129223
130393
  metadata: metadata2,
129224
- itemIdentifier: input.itemIdentifier
130394
+ itemIdentifier: input.itemIdentifier,
130395
+ integration: attempt.integration
129225
130396
  } : null
129226
130397
  };
129227
130398
  });
@@ -129230,7 +130401,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
129230
130401
  preview = await this.scoreItemSubmission(params, input);
129231
130402
  } else {
129232
130403
  if (committed.projection) {
129233
- await this.projectReviewItemResponse(params, committed.projection);
130404
+ this.projectReviewItemResponse(params, committed.projection, committed.checkpoint);
129234
130405
  }
129235
130406
  return committed.response;
129236
130407
  }
@@ -129241,40 +130412,39 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
129241
130412
  let preparationAttempts = 1;
129242
130413
  while (true) {
129243
130414
  const committed = await this.deps.assessmentRuntimeLock(this.deps.db, [assessmentRuntimeAttemptLockKey(params.attemptId)], async (db2) => {
129244
- const attempt = await this.authorizeAttempt(params, {
129245
- db: db2,
129246
- developerAccessValidated: true
129247
- });
130415
+ const { attempt, enrollment } = await this.resolveAttemptAuthorization(params, { db: db2, developerAccessValidated: true });
129248
130416
  if (!this.isPlatformRoutedDiagnosticMetadata(attempt.metadata)) {
130417
+ this.requireActiveAttempt({ attempt, enrollment }, params.attemptId);
129249
130418
  throw AssessmentRuntimeError.from(assessmentFlowViolation("routingNodeKey is accepted only for a platform-routed diagnostic.", { routingNodeKey: input.routingNodeKey }));
129250
130419
  }
129251
130420
  const metadata2 = attempt.metadata;
129252
- const priorIndex = metadata2.itemSubmissions.findIndex((submission2) => submission2.submissionId === input.submissionId);
129253
- if (priorIndex !== -1) {
129254
- const prior = metadata2.itemSubmissions[priorIndex];
129255
- const ledgerEntry = metadata2.diagnostic.ledger[priorIndex];
129256
- const sameRequest = prior !== undefined && ledgerEntry !== undefined && prior.itemIdentifier === input.itemIdentifier && ledgerEntry.routingNodeKey === input.routingNodeKey && itemResponseUpdateMatches(metadata2.responses[input.itemIdentifier], input.responses);
129257
- if (!sameRequest || !prior || !ledgerEntry) {
129258
- throw AssessmentRuntimeError.from(assessmentFlowViolation("This diagnostic submission ID was already used for a different request.", {
129259
- submissionId: input.submissionId,
129260
- routingNodeKey: input.routingNodeKey,
129261
- itemIdentifier: input.itemIdentifier
129262
- }));
129263
- }
129264
- const loaded2 = await this.loadAttemptDiagnosticManifest(metadata2, db2);
129265
- const replayed = replayDiagnosticRouting(loaded2.manifest, metadata2.diagnostic.ledger.slice(0, priorIndex + 1));
129266
- if (!replayed.ok) {
129267
- throw AssessmentRuntimeError.from(assessmentFlowViolation("The committed diagnostic routing state cannot be replayed.", { failure: replayed.failure }));
130421
+ const replay = resolveDiagnosticItemReplay(metadata2, input);
130422
+ if (replay.action === "conflict") {
130423
+ this.requireActiveAttempt({ attempt, enrollment }, params.attemptId);
130424
+ throw AssessmentRuntimeError.from(replay.failure);
130425
+ }
130426
+ if (replay.action === "replay") {
130427
+ const { submission: prior, ledgerEntry, priorIndex } = replay;
130428
+ let routing = replay.routing;
130429
+ if (!routing) {
130430
+ const assessment = preview?.assessment ?? await this.loadAttemptAssessment(metadata2);
130431
+ const loaded2 = await this.loadAttemptDiagnosticRouting(metadata2, assessment, db2);
130432
+ const replayed = replayDiagnosticRouting(loaded2.manifest, metadata2.diagnostic.ledger.slice(0, priorIndex + 1));
130433
+ if (!replayed.ok) {
130434
+ throw AssessmentRuntimeError.from(assessmentFlowViolation("The committed diagnostic routing state cannot be replayed.", { failure: replayed.failure }));
130435
+ }
130436
+ routing = this.diagnosticRoutingSnapshot(metadata2, replayed.state);
129268
130437
  }
130438
+ const child2 = await this.prepareDiagnosticAssessmentChildResult({
130439
+ attempt,
130440
+ metadata: metadata2,
130441
+ submission: prior,
130442
+ transition: ledgerEntry
130443
+ });
129269
130444
  return {
129270
130445
  action: "complete",
129271
- response: {
129272
- attemptId: attempt.result.sourcedId,
129273
- responseVersion: prior.responseVersion,
129274
- status: "in_progress",
129275
- routing: this.diagnosticRoutingSnapshot(metadata2, replayed.state),
129276
- submission: this.diagnosticReceipt(prior, ledgerEntry)
129277
- },
130446
+ response: buildDiagnosticItemReplayResult(attempt.result.sourcedId, prior, ledgerEntry, routing),
130447
+ checkpoint: child2,
129278
130448
  projection: {
129279
130449
  result: attempt.result,
129280
130450
  metadata: metadata2,
@@ -129283,6 +130453,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
129283
130453
  }
129284
130454
  };
129285
130455
  }
130456
+ this.requireActiveAttempt({ attempt, enrollment }, params.attemptId);
129286
130457
  this.assertInProgress(attempt.result);
129287
130458
  if (!preview || preview.responseVersion !== metadata2.responseVersion || preview.submissionId !== input.submissionId || preview.routingNodeKey !== input.routingNodeKey || preview.itemIdentifier !== input.itemIdentifier) {
129288
130459
  if (preparationAttempts >= TimebackAssessmentRuntimeService2.ITEM_SUBMISSION_MAX_PREPARATION_ATTEMPTS) {
@@ -129316,14 +130487,17 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
129316
130487
  }
129317
130488
  const submittedAt = new Date().toISOString();
129318
130489
  const responseVersion = metadata2.responseVersion + 1;
130490
+ const routingSnapshot = this.diagnosticRoutingSnapshot(metadata2, advanced.state);
130491
+ const routingSnapshots = metadata2.diagnostic.routingSnapshots ? [...metadata2.diagnostic.routingSnapshots, routingSnapshot] : undefined;
129319
130492
  const submission = {
129320
130493
  submissionId: input.submissionId,
129321
130494
  itemIdentifier: input.itemIdentifier,
129322
130495
  submittedAt,
129323
130496
  responseVersion,
129324
- answered: true,
130497
+ answered: Object.keys(responses[input.itemIdentifier] ?? {}).length > 0,
129325
130498
  score: scoring.score,
129326
- isCorrect: scoring.isCorrect
130499
+ isCorrect: scoring.isCorrect,
130500
+ grading: scoring.grading
129327
130501
  };
129328
130502
  const nextMetadata = {
129329
130503
  ...metadata2,
@@ -129334,9 +130508,16 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
129334
130508
  diagnostic: {
129335
130509
  ...metadata2.diagnostic,
129336
130510
  ledger: [...metadata2.diagnostic.ledger, advanced.ledgerEntry],
130511
+ ...routingSnapshots ? { routingSnapshots } : {},
129337
130512
  state: advanced.state
129338
130513
  }
129339
130514
  };
130515
+ const child = await this.prepareDiagnosticAssessmentChildResult({
130516
+ attempt,
130517
+ metadata: nextMetadata,
130518
+ submission,
130519
+ transition: advanced.ledgerEntry
130520
+ });
129340
130521
  await this.putResultMetadata(attempt.result, nextMetadata);
129341
130522
  return {
129342
130523
  action: "complete",
@@ -129344,9 +130525,10 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
129344
130525
  attemptId: attempt.result.sourcedId,
129345
130526
  responseVersion,
129346
130527
  status: "in_progress",
129347
- routing: this.diagnosticRoutingSnapshot(nextMetadata, advanced.state),
129348
- submission: this.diagnosticReceipt(submission, advanced.ledgerEntry)
130528
+ routing: routingSnapshot,
130529
+ submission: diagnosticReceipt(submission, advanced.ledgerEntry)
129349
130530
  },
130531
+ checkpoint: child,
129350
130532
  projection: {
129351
130533
  result: attempt.result,
129352
130534
  metadata: nextMetadata,
@@ -129359,21 +130541,11 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
129359
130541
  preparationAttempts += 1;
129360
130542
  preview = await this.scoreDiagnosticItemSubmission(params, input);
129361
130543
  } else {
129362
- await this.projectDiagnosticItemResponse(committed.projection, preview?.assessment);
130544
+ this.projectDiagnosticItemResponse(committed.projection, preview?.assessment, committed.checkpoint);
129363
130545
  return committed.response;
129364
130546
  }
129365
130547
  }
129366
130548
  }
129367
- diagnosticReceipt(submission, ledgerEntry) {
129368
- return {
129369
- submissionId: submission.submissionId,
129370
- routingNodeKey: ledgerEntry.routingNodeKey,
129371
- itemIdentifier: submission.itemIdentifier,
129372
- submittedAt: submission.submittedAt,
129373
- responseVersion: submission.responseVersion,
129374
- answered: submission.answered
129375
- };
129376
- }
129377
130549
  async prepareDiagnosticItemSubmission(params, input) {
129378
130550
  try {
129379
130551
  return await this.scoreDiagnosticItemSubmission(params, input);
@@ -129393,22 +130565,24 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
129393
130565
  }
129394
130566
  const prior = attempt.metadata.itemSubmissions.find((submission) => submission.submissionId === input.submissionId);
129395
130567
  if (prior || !this.isOpen(attempt.result)) {
129396
- return {
129397
- responseVersion: attempt.metadata.responseVersion,
129398
- submissionId: input.submissionId,
129399
- routingNodeKey: input.routingNodeKey,
129400
- itemIdentifier: input.itemIdentifier,
129401
- assessment: await this.loadAttemptAssessment(attempt.metadata),
129402
- scoring: null
129403
- };
130568
+ return null;
129404
130569
  }
129405
130570
  const assessment = await this.loadAttemptAssessment(attempt.metadata);
129406
- const loaded = await this.loadAttemptDiagnosticRouting(attempt.metadata, assessment);
130571
+ const artifactIdentity = assessmentArtifactIdentityFromAttempt(attempt.metadata);
130572
+ const scoringArtifact = await this.artifactLoader.loadMatchedScoring(assessment, artifactIdentity);
130573
+ const loaded = await this.loadAttemptDiagnosticRouting(attempt.metadata, assessment, this.deps.db, scoringArtifact);
129407
130574
  const expected = loaded.state.next;
129408
130575
  let scoring = null;
129409
130576
  if (loaded.state.status === "in-progress" && expected?.nodeKey === input.routingNodeKey && expected.itemIdentifier === input.itemIdentifier && attempt.metadata.responseVersion === input.expectedResponseVersion) {
129410
130577
  const responses = prepareDiagnosticAssessmentResponses(assessment, attempt.metadata.responses, input);
129411
- scoring = await this.scoreItem(assessment, responses, input.itemIdentifier, assessmentArtifactIdentityFromAttempt(attempt.metadata));
130578
+ scoring = await this.scoreItem({
130579
+ assessment,
130580
+ responses,
130581
+ itemIdentifier: input.itemIdentifier,
130582
+ artifactIdentity,
130583
+ purpose: attempt.metadata.purpose,
130584
+ preparedArtifact: scoringArtifact
130585
+ });
129412
130586
  }
129413
130587
  return {
129414
130588
  responseVersion: attempt.metadata.responseVersion,
@@ -129419,39 +130593,27 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
129419
130593
  scoring
129420
130594
  };
129421
130595
  }
129422
- async scoreItems(assessment, responses, artifactIdentity, itemIdentifiers) {
129423
- await this.artifactLoader.loadMatchedScoring(assessment, artifactIdentity);
129424
- const client2 = this.requireClient();
129425
- const scoringItems = assessmentScoringRequests(assessment, responses).filter((item) => !itemIdentifiers || itemIdentifiers.has(item.itemIdentifier));
129426
- const scoringTasks = scoringItems.flatMap((item, itemIndex) => item.requests.map((request) => ({
129427
- itemIndex,
129428
- itemIdentifier: item.itemIdentifier,
129429
- request
129430
- })));
129431
- const interactionResults = await runWithConcurrency(scoringTasks, TimebackAssessmentRuntimeService2.SCORING_CONCURRENCY, async ({ itemIdentifier, request }) => client2.qtiApi.assessmentItems.processResponse(itemIdentifier, request));
129432
- const scoresByItem = scoringItems.map(() => []);
129433
- const verdictsByItem = scoringItems.map(() => []);
129434
- scoringTasks.forEach((task, taskIndex) => {
129435
- const processed = interactionResults[taskIndex];
129436
- scoresByItem[task.itemIndex].push(Number.isFinite(processed.score) ? processed.score : 0);
129437
- verdictsByItem[task.itemIndex].push(processed.isCorrect);
129438
- });
129439
- return scoringItems.map((item, itemIndex) => {
129440
- const earned = assessmentItemEarnedScore(scoresByItem[itemIndex], item.maxScore);
129441
- const possible = item.maxScore;
129442
- return {
129443
- itemIdentifier: item.itemIdentifier,
129444
- score: {
129445
- earned,
129446
- possible,
129447
- normalized: possible === 0 ? 0 : earned / possible
129448
- },
129449
- isCorrect: assessmentItemCorrectness(verdictsByItem[itemIndex])
129450
- };
130596
+ async scoreItems({
130597
+ preparedArtifact,
130598
+ ...input
130599
+ }) {
130600
+ const artifact = preparedArtifact === undefined ? await this.artifactLoader.loadMatchedScoring(input.assessment, input.artifactIdentity) : preparedArtifact;
130601
+ return scoreAssessmentItems({
130602
+ ...input,
130603
+ artifact,
130604
+ processResponse: (identifier, request) => this.requireClient().qtiApi.assessmentItems.processResponse(identifier, request),
130605
+ onEvent: addEvent
129451
130606
  });
129452
130607
  }
129453
- async scoreItem(assessment, responses, itemIdentifier, artifactIdentity) {
129454
- const [scoring] = await this.scoreItems(assessment, responses, artifactIdentity, new Set([itemIdentifier]));
130608
+ async scoreItem(input) {
130609
+ const [scoring] = await this.scoreItems({
130610
+ assessment: input.assessment,
130611
+ responses: input.responses,
130612
+ artifactIdentity: input.artifactIdentity,
130613
+ purpose: input.purpose,
130614
+ itemIdentifiers: new Set([input.itemIdentifier]),
130615
+ preparedArtifact: input.preparedArtifact
130616
+ });
129455
130617
  return scoring;
129456
130618
  }
129457
130619
  async prepareItemSubmission(params, input) {
@@ -129468,7 +130630,8 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
129468
130630
  }
129469
130631
  async scoreItemSubmission(params, input) {
129470
130632
  const attempt = await this.peekAttempt(params);
129471
- if (!this.isOpen(attempt.result)) {
130633
+ const prior = attempt.metadata.itemSubmissions.find((submission) => submission.submissionId === input.submissionId);
130634
+ if (prior || !this.isOpen(attempt.result)) {
129472
130635
  return null;
129473
130636
  }
129474
130637
  const assessment = await this.loadAttemptAssessment(attempt.metadata);
@@ -129479,7 +130642,13 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
129479
130642
  itemSubmissions: attempt.metadata.itemSubmissions,
129480
130643
  assessment
129481
130644
  }, input);
129482
- const scoring = transition.action === "commit" ? await this.scoreItem(assessment, transition.responses, input.itemIdentifier, assessmentArtifactIdentityFromAttempt(attempt.metadata)) : null;
130645
+ const scoring = transition.action === "commit" ? await this.scoreItem({
130646
+ assessment,
130647
+ responses: transition.responses,
130648
+ itemIdentifier: input.itemIdentifier,
130649
+ artifactIdentity: assessmentArtifactIdentityFromAttempt(attempt.metadata),
130650
+ purpose: attempt.metadata.purpose
130651
+ }) : null;
129483
130652
  return {
129484
130653
  responseVersion: attempt.metadata.responseVersion,
129485
130654
  submissionId: input.submissionId,
@@ -129491,8 +130660,12 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
129491
130660
  async scoreSubmission(attempt) {
129492
130661
  const assessment = await this.loadAttemptAssessment(attempt.metadata);
129493
130662
  validateAssessmentResponses(assessment, attempt.metadata.responses);
129494
- const administeredItems = attempt.metadata.purpose === "review" ? administeredItemIdentifiers(attempt.metadata.itemSubmissions) : undefined;
129495
- const itemResults = await this.scoreItems(assessment, attempt.metadata.responses, assessmentArtifactIdentityFromAttempt(attempt.metadata), administeredItems);
130663
+ const itemResults = attempt.metadata.purpose === "review" ? committedReviewItemResults(attempt.metadata, assessment) : await this.scoreItems({
130664
+ assessment,
130665
+ responses: attempt.metadata.responses,
130666
+ artifactIdentity: assessmentArtifactIdentityFromAttempt(attempt.metadata),
130667
+ purpose: attempt.metadata.purpose
130668
+ });
129496
130669
  return {
129497
130670
  responseVersion: attempt.metadata.responseVersion,
129498
130671
  assessment,
@@ -129501,7 +130674,10 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
129501
130674
  correctQuestions: countCorrectQuestions(itemResults.map((result) => result.isCorrect))
129502
130675
  };
129503
130676
  }
129504
- async peekAttempt({ studentId, attemptId }) {
130677
+ async peekAttempt({
130678
+ studentId,
130679
+ attemptId
130680
+ }) {
129505
130681
  const result = await this.requireClient().api.oneroster.assessmentResults.get(attemptId).catch((error88) => {
129506
130682
  if (isApiError(error88) && error88.statusCode === 404) {
129507
130683
  throw this.unauthorizedAttempt(attemptId);
@@ -129546,21 +130722,19 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
129546
130722
  await this.deps.validateDeveloperAccess(params.user, params.gameId);
129547
130723
  const preview = await this.prepareSubmission(params, input);
129548
130724
  const submission = await this.deps.assessmentRuntimeLock(this.deps.db, [assessmentRuntimeAttemptLockKey(params.attemptId)], async (db2) => {
129549
- const attempt = await this.authorizeAttempt(params, {
129550
- db: db2,
129551
- developerAccessValidated: true
129552
- });
130725
+ const { attempt: identity, enrollment } = await this.resolveAttemptAuthorization(params, { db: db2, developerAccessValidated: true });
129553
130726
  const submissionDisposition = classifyAssessmentSubmission({
129554
- inProgress: attempt.result.inProgress ?? "",
129555
- scoreStatus: attempt.result.scoreStatus,
129556
- submissionId: attempt.metadata.submissionId
130727
+ inProgress: identity.result.inProgress ?? "",
130728
+ scoreStatus: identity.result.scoreStatus,
130729
+ submissionId: identity.metadata.submissionId
129557
130730
  }, input.submissionId);
129558
130731
  if (submissionDisposition === "replay") {
129559
130732
  return {
129560
- response: this.submittedResult(attempt.result, attempt.metadata),
129561
- emission: this.replayCompletion(attempt, input.submissionId, params, game2)
130733
+ response: this.submittedResult(identity.result, identity.metadata),
130734
+ emission: this.replayCompletion(identity, input.submissionId, params, game2)
129562
130735
  };
129563
130736
  }
130737
+ const attempt = this.requireActiveAttempt({ attempt: identity, enrollment }, params.attemptId);
129564
130738
  if (submissionDisposition === "reject") {
129565
130739
  throw AssessmentRuntimeError.from(assessmentAlreadySubmitted(attempt.result.sourcedId));
129566
130740
  }
@@ -129588,12 +130762,9 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
129588
130762
  const { assessment, itemResults, score, correctQuestions } = scored;
129589
130763
  const client2 = this.requireClient();
129590
130764
  const timestamp6 = new Date().toISOString();
129591
- const reviewOutcomes = attempt.metadata.purpose === "review" ? await this.finalizeReviewChildResults({
129592
- result: attempt.result,
130765
+ const reviewOutcomes = attempt.metadata.purpose === "review" ? this.reviewItemOutcomes({
129593
130766
  metadata: attempt.metadata,
129594
- itemResults,
129595
- submissionId: input.submissionId,
129596
- timestamp: timestamp6
130767
+ itemResults
129597
130768
  }) : null;
129598
130769
  const finalization = buildAssessmentResultSubmission({
129599
130770
  result: attempt.result,
@@ -129656,9 +130827,11 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
129656
130827
  ...submission.emission.attempt,
129657
130828
  metadata: submission.emission.attempt.metadata
129658
130829
  } : null;
130830
+ const { result, metadata: metadata2 } = submission.emission.attempt;
129659
130831
  await Promise.allSettled([
129660
130832
  this.emitSessionBestEffort(submission.emission),
129661
- ...diagnosticCompletion ? [this.restoreDiagnosticChildResults(diagnosticCompletion)] : []
130833
+ ...diagnosticCompletion ? [this.restoreDiagnosticChildResults(diagnosticCompletion)] : [],
130834
+ ...metadata2.purpose === "review" ? [this.restoreAllCompletedReviewChildResponses(result, metadata2)] : []
129662
130835
  ]);
129663
130836
  }
129664
130837
  return submission.response;
@@ -129808,17 +130981,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
129808
130981
  async finalizeDiagnosticSubmission(input) {
129809
130982
  const { attempt } = input;
129810
130983
  const routing = await this.loadAttemptDiagnosticManifest(attempt.metadata, input.db);
129811
- const state = this.replayAttemptDiagnosticRouting(attempt.metadata, routing.manifest);
129812
- const tracks = diagnosticTrackResults(state);
129813
- if (!tracks) {
129814
- throw AssessmentRuntimeError.from(assessmentFlowViolation("Diagnostic routing must be ready to complete before final submit.", { routingStatus: state.status }));
129815
- }
129816
- if (attempt.metadata.itemSubmissions.length === 0) {
129817
- throw AssessmentRuntimeError.from(assessmentFlowViolation("A diagnostic cannot be finalized without an administered item."));
129818
- }
129819
- const possible = attempt.metadata.itemSubmissions.reduce((total, submission) => total + submission.score.possible, 0);
129820
- const score = aggregateAssessmentScore(attempt.metadata.itemSubmissions.map((submission) => submission.score.earned), possible);
129821
- const correctQuestions = countCorrectQuestions(attempt.metadata.itemSubmissions.map((submission) => submission.isCorrect));
130984
+ const { tracks, score, correctQuestions } = summarizeDiagnosticSubmission(attempt.metadata, routing.manifest);
129822
130985
  const timestamp6 = new Date().toISOString();
129823
130986
  const finalization = buildAssessmentResultSubmission({
129824
130987
  result: attempt.result,
@@ -129906,10 +131069,13 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
129906
131069
  }) : undefined;
129907
131070
  const diagnostic = definition?.diagnosticKey ? await this.initializeHostedDiagnostic(definition, loaded.assessment) : null;
129908
131071
  const reviewBank = purpose === "review" ? await buildReviewBankIndex(loaded.assessment, loaded.questions, { customOnly: true }) : null;
131072
+ const scoring = compileQtiScoringArtifact(loaded.questions);
131073
+ const fullyPlatformScored = scoring.items.every((item) => item.strategy === "platform");
129909
131074
  return {
129910
131075
  ...test,
129911
131076
  live: true,
129912
131077
  assessment: loaded.assessment,
131078
+ ...fullyPlatformScored ? { scoring } : {},
129913
131079
  ...diagnostic ? {
129914
131080
  diagnostic: {
129915
131081
  diagnosticKey: diagnostic.definition.diagnosticKey,
@@ -130254,21 +131420,31 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
130254
131420
  if (resumed.metadata.purpose !== "review") {
130255
131421
  throw new Error("A standards-review request selected a non-review attempt");
130256
131422
  }
130257
- if (this.isSettled(resumed.result)) {
130258
- return this.snapshotForResult(resumed.result, resumed.metadata, context2.integration);
130259
- }
130260
- const source = await this.loadPinnedReviewBankSource(resumed.metadata);
130261
- const assessment = projectReviewAssessment(source.assessment, source.bank, resumed.metadata.review.selections);
130262
- await this.ensureReviewChildResults({
130263
- result: resumed.result,
130264
- metadata: resumed.metadata,
130265
- integration: context2.integration,
130266
- source,
130267
- assessment,
130268
- existingChildResults,
130269
- lookupMissingResults: true
131423
+ const reviewMetadata = resumed.metadata;
131424
+ let assessment;
131425
+ const current = await reconcileReviewChildResults({
131426
+ initial: resumed,
131427
+ writeOpen: async () => {
131428
+ const source = await this.loadPinnedReviewBankSource(reviewMetadata);
131429
+ assessment = projectReviewAssessment(source.assessment, source.bank, reviewMetadata.review.selections);
131430
+ await this.ensureReviewChildResults({
131431
+ result: resumed.result,
131432
+ metadata: reviewMetadata,
131433
+ integration: context2.integration,
131434
+ source,
131435
+ assessment,
131436
+ existingChildResults,
131437
+ lookupMissingResults: true
131438
+ });
131439
+ },
131440
+ crossBarrier: () => crossAssessmentAttemptLockBarrier(this.deps.assessmentRuntimeLock, this.deps.db, resumed.result.sourcedId),
131441
+ readLatest: () => this.peekAttempt({
131442
+ studentId: resumed.result.student.sourcedId,
131443
+ attemptId: resumed.result.sourcedId
131444
+ }),
131445
+ restoreTerminal: (latest) => this.restoreAllCompletedReviewChildResponses(latest.result, latest.metadata)
130270
131446
  });
130271
- return this.snapshot(resumed.result, resumed.metadata, assessment, context2.integration);
131447
+ return assessmentChildProjectionIsTerminal(current.result) ? this.snapshotForResult(current.result, current.metadata, context2.integration) : this.snapshot(current.result, current.metadata, assessment, context2.integration);
130272
131448
  }
130273
131449
  recordReviewPreparationPhase(phase, startedAt, counts = {}) {
130274
131450
  addEvent("assessment.review_preparation_phase", {
@@ -130490,20 +131666,9 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
130490
131666
  }
130491
131667
  async initializeHostedDiagnostic(definition, assessment) {
130492
131668
  const initialized = await this.initializeHostedDiagnosticManifest(definition);
130493
- this.assertDiagnosticItemsAvailable(definition, initialized.manifest, assessment);
131669
+ assertDiagnosticItemsAvailable(definition.id, initialized.manifest, assessment);
130494
131670
  return initialized;
130495
131671
  }
130496
- assertDiagnosticItemsAvailable(definition, manifest, assessment) {
130497
- const itemIdentifiers = new Set(assessment.items.map((item) => item.identifier));
130498
- const unknownItem = manifest.nodes.find((node) => !itemIdentifiers.has(node.itemIdentifier));
130499
- if (unknownItem) {
130500
- throw new AssessmentRuntimeError("SELECTED_TEST_VERSION_UNAVAILABLE", `Diagnostic routing references unavailable item ${unknownItem.itemIdentifier}.`, {
130501
- definitionId: definition.id,
130502
- nodeKey: unknownItem.key,
130503
- itemIdentifier: unknownItem.itemIdentifier
130504
- });
130505
- }
130506
- }
130507
131672
  async initializeHostedDiagnosticManifest(definition) {
130508
131673
  const initialized = initializeDiagnosticRouting(definition.diagnosticRoutingManifest);
130509
131674
  if (!initialized.ok) {
@@ -130527,21 +131692,32 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
130527
131692
  }
130528
131693
  return initialized;
130529
131694
  }
130530
- replayAttemptDiagnosticRouting(metadata2, manifest) {
130531
- const replayed = replayDiagnosticRouting(manifest, metadata2.diagnostic.ledger);
130532
- if (!replayed.ok || canonicalJson2(replayed.state) !== canonicalJson2(metadata2.diagnostic.state)) {
130533
- throw AssessmentRuntimeError.from(assessmentFlowViolation("The persisted diagnostic routing state is corrupt.", {
130534
- definitionId: metadata2.diagnostic.definitionId,
130535
- ...replayed.ok ? {} : { failure: replayed.failure }
130536
- }));
130537
- }
130538
- return replayed.state;
130539
- }
130540
- async loadAttemptDiagnosticRouting(metadata2, assessment, db2 = this.deps.db) {
131695
+ async loadAttemptDiagnosticRouting(metadata2, assessment, db2 = this.deps.db, preparedArtifact) {
131696
+ const startedAt = Date.now();
131697
+ const artifactIdentity = assessmentArtifactIdentityFromAttempt(metadata2);
131698
+ const artifact = preparedArtifact === undefined ? await this.artifactLoader.loadMatchedScoring(assessment, artifactIdentity) : preparedArtifact;
131699
+ const compiled = await resolveCompiledDiagnosticRouting(metadata2, assessment, artifact);
131700
+ if (compiled) {
131701
+ addEvent("assessment.diagnostic_routing_load", {
131702
+ "app.assessment.diagnostic_definition_id": metadata2.diagnostic.definitionId,
131703
+ "app.assessment.routing_source": "platform-artifact",
131704
+ "app.assessment.routing_duration_ms": Date.now() - startedAt
131705
+ });
131706
+ return compiled;
131707
+ }
131708
+ addEvent("assessment.diagnostic_routing_fallback", {
131709
+ "app.assessment.diagnostic_definition_id": metadata2.diagnostic.definitionId,
131710
+ "app.assessment.fallback_reason": diagnosticRoutingFallbackReason(artifact, metadata2.diagnostic.routingRevision),
131711
+ "app.assessment.fallback_duration_ms": Date.now() - startedAt
131712
+ });
130541
131713
  const pinned = await this.loadAttemptDiagnosticManifest(metadata2, db2);
130542
- this.assertDiagnosticItemsAvailable(pinned.definition, pinned.manifest, assessment);
130543
- const state = this.replayAttemptDiagnosticRouting(metadata2, pinned.manifest);
130544
- return { ...pinned, state };
131714
+ assertDiagnosticItemsAvailable(pinned.definition.id, pinned.manifest, assessment);
131715
+ const state = replayAttemptDiagnosticRouting(metadata2, pinned.manifest);
131716
+ return {
131717
+ manifest: pinned.manifest,
131718
+ routingRevision: pinned.routingRevision,
131719
+ state
131720
+ };
130545
131721
  }
130546
131722
  async loadAssessmentSource(identifier, expectedRevision) {
130547
131723
  const client2 = this.requireClient();
@@ -130676,11 +131852,167 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
130676
131852
  }
130677
131853
  return lineItemId;
130678
131854
  }
131855
+ assessmentChildAttempt(attempt) {
131856
+ const { metadata: metadata2 } = attempt;
131857
+ const assessmentKey = metadata2.selectedTest.assessmentKey ?? assessmentKeyFromManagedQtiIdentifier(metadata2.selectedTest.identifier);
131858
+ if (!assessmentKey) {
131859
+ addEvent("assessment.child_result_legacy_attempt_skipped", {
131860
+ "app.assessment.attempt_id": attempt.result.sourcedId
131861
+ });
131862
+ return null;
131863
+ }
131864
+ return {
131865
+ attemptId: attempt.result.sourcedId,
131866
+ gameId: attempt.integration.gameId,
131867
+ studentId: attempt.result.student.sourcedId,
131868
+ activityId: metadata2.activityId,
131869
+ courseId: metadata2.courseId,
131870
+ integrationId: metadata2.integrationId,
131871
+ enrollmentId: metadata2.enrollmentId,
131872
+ selectedTest: {
131873
+ assessmentKey,
131874
+ identifier: metadata2.selectedTest.identifier,
131875
+ contentRevision: metadata2.selectedTest.contentRevision
131876
+ }
131877
+ };
131878
+ }
131879
+ reviewChildResultContext(metadata2, submission) {
131880
+ const selection = metadata2.review.selections.find((candidate) => candidate.itemIdentifier === submission.itemIdentifier);
131881
+ if (!selection) {
131882
+ throw new Error(`Committed review item ${submission.itemIdentifier} is not in its pinned selection`);
131883
+ }
131884
+ return {
131885
+ kind: "review",
131886
+ bankRevision: metadata2.review.bankRevision,
131887
+ standard: selection.standard
131888
+ };
131889
+ }
131890
+ prepareReviewAssessmentChildResult(input) {
131891
+ return this.prepareAssessmentChildResult({
131892
+ attempt: input.attempt,
131893
+ metadata: input.metadata,
131894
+ submission: input.submission,
131895
+ context: () => this.reviewChildResultContext(input.metadata, input.submission)
131896
+ });
131897
+ }
131898
+ prepareDiagnosticAssessmentChildResult(input) {
131899
+ return this.prepareAssessmentChildResult({
131900
+ attempt: input.attempt,
131901
+ metadata: input.metadata,
131902
+ submission: input.submission,
131903
+ context: () => ({
131904
+ kind: "platform-routed-diagnostic",
131905
+ definitionId: input.metadata.diagnostic.definitionId,
131906
+ diagnosticKey: input.metadata.diagnostic.diagnosticKey,
131907
+ routingRevision: input.metadata.diagnostic.routingRevision,
131908
+ transition: input.transition
131909
+ })
131910
+ });
131911
+ }
131912
+ async prepareAssessmentChildResult(input) {
131913
+ try {
131914
+ const attemptContext = this.assessmentChildAttempt({
131915
+ ...input.attempt,
131916
+ metadata: input.metadata
131917
+ });
131918
+ if (!attemptContext) {
131919
+ return null;
131920
+ }
131921
+ const parentLineItemId = input.attempt.result.assessmentLineItem.sourcedId;
131922
+ const { itemIdentifier } = input.submission;
131923
+ const context2 = input.context();
131924
+ const [command, childLineItemId] = await Promise.all([
131925
+ buildSubmissionChildResultCommand({
131926
+ attempt: attemptContext,
131927
+ submission: input.submission,
131928
+ responses: input.metadata.responses[itemIdentifier],
131929
+ grading: input.submission.grading ?? QTI_CHILD_GRADING,
131930
+ context: context2
131931
+ }),
131932
+ context2.kind === "review" ? reviewQuestionLineItemId(parentLineItemId, itemIdentifier) : diagnosticQuestionLineItemId(parentLineItemId, itemIdentifier)
131933
+ ]);
131934
+ return { command, childLineItemId };
131935
+ } catch (error88) {
131936
+ addEvent("assessment.child_result_prepare_failed", {
131937
+ "app.assessment.attempt_id": input.attempt.result.sourcedId,
131938
+ "app.assessment.submission_id": input.submission.submissionId,
131939
+ "app.assessment.qti_item_identifier": input.submission.itemIdentifier,
131940
+ "exception.type": errorType(error88),
131941
+ "app.error.message": errorMessage2(error88)
131942
+ });
131943
+ return null;
131944
+ }
131945
+ }
131946
+ async projectPreparedAssessmentChildResult(prepared) {
131947
+ if (!prepared) {
131948
+ return;
131949
+ }
131950
+ const startedAt = Date.now();
131951
+ const identity = {
131952
+ "app.assessment.attempt_id": prepared.command.payload.attempt.attemptId,
131953
+ "app.assessment.submission_id": prepared.command.payload.administration.submissionId
131954
+ };
131955
+ try {
131956
+ const disposition = await persistAssessmentChildResult({
131957
+ assessmentResults: this.requireClient().api.oneroster.assessmentResults,
131958
+ command: prepared.command,
131959
+ childLineItemId: prepared.childLineItemId
131960
+ });
131961
+ if (disposition.outcome === "conflict") {
131962
+ addEvent("assessment.child_result_conflict", {
131963
+ ...identity,
131964
+ "app.assessment.child_result_id": disposition.resultId,
131965
+ "app.assessment.child_result_conflict_reason": disposition.reason
131966
+ });
131967
+ return;
131968
+ }
131969
+ addEvent("assessment.child_result_persisted", {
131970
+ ...identity,
131971
+ "app.assessment.child_result_id": disposition.resultId,
131972
+ "app.assessment.child_result_outcome": disposition.outcome,
131973
+ "app.assessment.duration_ms": Date.now() - startedAt
131974
+ });
131975
+ } catch (error88) {
131976
+ addEvent("assessment.child_result_persist_failed", {
131977
+ ...identity,
131978
+ "app.assessment.duration_ms": Date.now() - startedAt,
131979
+ "exception.type": errorType(error88),
131980
+ "app.error.message": errorMessage2(error88)
131981
+ });
131982
+ }
131983
+ }
131984
+ assertAssessmentChildLineItem(lineItemId, stored, expected) {
131985
+ if (assessmentChildLineItemMatches(stored, expected)) {
131986
+ return;
131987
+ }
131988
+ addEvent("assessment.question_line_item_conflict", {
131989
+ "app.assessment.line_item_id": lineItemId
131990
+ });
131991
+ throw AssessmentRuntimeError.from(assessmentFlowViolation("A deterministic assessment question line item is occupied by incompatible data.", { lineItemId }));
131992
+ }
130679
131993
  async ensureDiagnosticQuestionLineItem(input) {
130680
131994
  const client2 = this.requireClient();
130681
131995
  const lineItemId = await diagnosticQuestionLineItemId(input.parentLineItemId, input.item.identifier);
131996
+ const expected = {
131997
+ status: ONEROSTER_STATUS2.active,
131998
+ parentAssessmentLineItem: { sourcedId: input.parentLineItemId },
131999
+ course: { sourcedId: input.integration.courseId },
132000
+ resultValueMin: 0,
132001
+ resultValueMax: input.item.maxScore,
132002
+ metadata: {
132003
+ playcademyDiagnosticQuestionDefinition: {
132004
+ version: 1,
132005
+ definitionId: input.metadata.diagnostic.definitionId,
132006
+ diagnosticKey: input.metadata.diagnostic.diagnosticKey,
132007
+ routingRevision: input.metadata.diagnostic.routingRevision,
132008
+ contentRevision: input.metadata.selectedTest.contentRevision,
132009
+ itemIdentifier: input.item.identifier
132010
+ }
132011
+ }
132012
+ };
130682
132013
  try {
130683
- await client2.api.oneroster.assessmentLineItems.get(lineItemId);
132014
+ const stored = await client2.api.oneroster.assessmentLineItems.get(lineItemId);
132015
+ this.assertAssessmentChildLineItem(lineItemId, stored, expected);
130684
132016
  return lineItemId;
130685
132017
  } catch (error88) {
130686
132018
  if (!isApiError(error88) || error88.statusCode !== 404) {
@@ -130690,35 +132022,23 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
130690
132022
  try {
130691
132023
  await client2.api.oneroster.assessmentLineItems.create({
130692
132024
  sourcedId: lineItemId,
130693
- status: ONEROSTER_STATUS2.active,
130694
132025
  title: input.item.title,
130695
132026
  description: "adaptive diagnostic question",
130696
- parentAssessmentLineItem: { sourcedId: input.parentLineItemId },
130697
- course: { sourcedId: input.integration.courseId },
130698
- resultValueMin: 0,
130699
- resultValueMax: input.item.maxScore,
130700
- metadata: {
130701
- playcademyDiagnosticQuestionDefinition: {
130702
- version: 1,
130703
- definitionId: input.metadata.diagnostic.definitionId,
130704
- diagnosticKey: input.metadata.diagnostic.diagnosticKey,
130705
- routingRevision: input.metadata.diagnostic.routingRevision,
130706
- contentRevision: input.metadata.selectedTest.contentRevision,
130707
- itemIdentifier: input.item.identifier
130708
- }
130709
- }
132027
+ ...expected
130710
132028
  });
130711
132029
  } catch (error88) {
130712
132030
  if (!isApiError(error88) || error88.statusCode !== 409) {
130713
132031
  throw error88;
130714
132032
  }
132033
+ const stored = await client2.api.oneroster.assessmentLineItems.get(lineItemId);
132034
+ this.assertAssessmentChildLineItem(lineItemId, stored, expected);
130715
132035
  addEvent("assessment.diagnostic_question_line_item_create_conflict", {
130716
132036
  "app.assessment.line_item_id": lineItemId
130717
132037
  });
130718
132038
  }
130719
132039
  return lineItemId;
130720
132040
  }
130721
- async projectDiagnosticItemResponse(projection, assessment) {
132041
+ async projectDiagnosticItemResponse(projection, assessment, checkpoint = null) {
130722
132042
  try {
130723
132043
  const loadedAssessment = assessment ?? await this.loadAttemptAssessment(projection.metadata);
130724
132044
  const item = loadedAssessment.items.find((candidate) => candidate.identifier === projection.ledgerEntry.itemIdentifier);
@@ -130731,6 +132051,9 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
130731
132051
  metadata: projection.metadata,
130732
132052
  item
130733
132053
  });
132054
+ if (checkpoint && checkpoint.childLineItemId !== childLineItemId) {
132055
+ throw new Error("Prepared diagnostic checkpoint resolved a different line item");
132056
+ }
130734
132057
  const childResultId = await diagnosticQuestionResultId(projection.result.sourcedId, childLineItemId);
130735
132058
  const finalized = buildFinalizedDiagnosticChildResult({
130736
132059
  childLineItemId,
@@ -130739,7 +132062,10 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
130739
132062
  metadata: projection.metadata,
130740
132063
  ledgerEntry: projection.ledgerEntry
130741
132064
  });
130742
- await this.requireClient().api.oneroster.assessmentResults.upsert(childResultId, finalized.resultUpdate);
132065
+ await Promise.all([
132066
+ this.projectPreparedAssessmentChildResult(checkpoint),
132067
+ this.requireClient().api.oneroster.assessmentResults.upsert(childResultId, finalized.resultUpdate)
132068
+ ]);
130743
132069
  } catch (error88) {
130744
132070
  addEvent("assessment.diagnostic_child_projection_failed", {
130745
132071
  "app.assessment.attempt_id": projection.result.sourcedId,
@@ -130800,41 +132126,43 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
130800
132126
  async ensureReviewQuestionLineItem(input) {
130801
132127
  const client2 = this.requireClient();
130802
132128
  const lineItemId = await reviewQuestionLineItemId(input.parentLineItemId, input.item.identifier);
132129
+ const expected = {
132130
+ status: ONEROSTER_STATUS2.active,
132131
+ parentAssessmentLineItem: { sourcedId: input.parentLineItemId },
132132
+ course: { sourcedId: input.integration.courseId },
132133
+ resultValueMin: 0,
132134
+ resultValueMax: input.item.maxScore,
132135
+ metadata: {
132136
+ playcademyReviewQuestionDefinition: {
132137
+ version: 1,
132138
+ bankRevision: input.bankRevision,
132139
+ itemIdentifier: input.item.identifier,
132140
+ standards: input.standards
132141
+ }
132142
+ }
132143
+ };
130803
132144
  try {
130804
- await client2.api.oneroster.assessmentLineItems.get(lineItemId);
132145
+ const stored = await client2.api.oneroster.assessmentLineItems.get(lineItemId);
132146
+ this.assertAssessmentChildLineItem(lineItemId, stored, expected);
130805
132147
  return lineItemId;
130806
132148
  } catch (error88) {
130807
132149
  if (!isApiError(error88) || error88.statusCode !== 404) {
130808
132150
  throw error88;
130809
132151
  }
130810
132152
  }
130811
- const indexed = input.bank.items.find((candidate) => candidate.itemIdentifier === input.item.identifier);
130812
- if (!indexed) {
130813
- throw new Error(`Review-bank index is missing item ${input.item.identifier}`);
130814
- }
130815
132153
  try {
130816
132154
  await client2.api.oneroster.assessmentLineItems.create({
130817
132155
  sourcedId: lineItemId,
130818
- status: ONEROSTER_STATUS2.active,
130819
132156
  title: input.item.title,
130820
132157
  description: "standards review question",
130821
- parentAssessmentLineItem: { sourcedId: input.parentLineItemId },
130822
- course: { sourcedId: input.integration.courseId },
130823
- resultValueMin: 0,
130824
- resultValueMax: input.item.maxScore,
130825
- metadata: {
130826
- playcademyReviewQuestionDefinition: {
130827
- version: 1,
130828
- bankRevision: input.bank.bankRevision,
130829
- itemIdentifier: input.item.identifier,
130830
- standards: indexed.standards
130831
- }
130832
- }
132158
+ ...expected
130833
132159
  });
130834
132160
  } catch (error88) {
130835
132161
  if (!isApiError(error88) || error88.statusCode !== 409) {
130836
132162
  throw error88;
130837
132163
  }
132164
+ const stored = await client2.api.oneroster.assessmentLineItems.get(lineItemId);
132165
+ this.assertAssessmentChildLineItem(lineItemId, stored, expected);
130838
132166
  addEvent("assessment.review_question_line_item_create_conflict", {
130839
132167
  "app.assessment.line_item_id": lineItemId
130840
132168
  });
@@ -130855,6 +132183,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
130855
132183
  }
130856
132184
  async ensureReviewChildResults(input) {
130857
132185
  const itemByIdentifier = new Map(input.assessment.items.map((item) => [item.identifier, item]));
132186
+ const bankItemByIdentifier = new Map(input.source.bank.items.map((item) => [item.itemIdentifier, item]));
130858
132187
  const physicalItems = new Set(input.metadata.review.selections.map((selection) => selection.itemIdentifier));
130859
132188
  if (physicalItems.size !== input.metadata.review.selections.length) {
130860
132189
  throw new Error("MVP review selection must use each physical item at most once");
@@ -130862,19 +132191,20 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
130862
132191
  const administeredItems = administeredItemIdentifiers(input.metadata.itemSubmissions);
130863
132192
  await runWithConcurrency(input.metadata.review.selections, TimebackAssessmentRuntimeService2.SCORING_CONCURRENCY, async (selection) => {
130864
132193
  const item = itemByIdentifier.get(selection.itemIdentifier);
130865
- if (!item) {
130866
- throw new Error(`Review assessment is missing selected item ${selection.itemIdentifier}`);
132194
+ const indexed = bankItemByIdentifier.get(selection.itemIdentifier);
132195
+ if (!item || !indexed) {
132196
+ throw new Error(`Review assessment or bank is missing selected item ${selection.itemIdentifier}`);
130867
132197
  }
132198
+ const childLineItemId = await this.ensureReviewQuestionLineItem({
132199
+ parentLineItemId: input.result.assessmentLineItem.sourcedId,
132200
+ integration: input.integration,
132201
+ bankRevision: input.metadata.review.bankRevision,
132202
+ standards: [...indexed.standards],
132203
+ item
132204
+ });
130868
132205
  if (!administeredItems.has(selection.itemIdentifier)) {
130869
- await this.ensureReviewQuestionLineItem({
130870
- parentLineItemId: input.result.assessmentLineItem.sourcedId,
130871
- integration: input.integration,
130872
- bank: input.source.bank,
130873
- item
130874
- });
130875
132206
  return;
130876
132207
  }
130877
- const childLineItemId = await reviewQuestionLineItemId(input.result.assessmentLineItem.sourcedId, item.identifier);
130878
132208
  const childResultId = await reviewQuestionResultId(input.result.sourcedId, childLineItemId);
130879
132209
  const streamedChildResult = input.existingChildResults.get(childResultId);
130880
132210
  let childResult = streamedChildResult;
@@ -130897,12 +132227,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
130897
132227
  if (resolution.action === "reuse") {
130898
132228
  return;
130899
132229
  }
130900
- await this.ensureReviewQuestionLineItem({
130901
- parentLineItemId: input.result.assessmentLineItem.sourcedId,
130902
- integration: input.integration,
130903
- bank: input.source.bank,
130904
- item
130905
- });
130906
132230
  if (resolution.action === "restore") {
130907
132231
  addEvent("assessment.review_child_result_restore", {
130908
132232
  "app.assessment.review_child_restore_reason": resolution.reason,
@@ -130920,24 +132244,43 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
130920
132244
  });
130921
132245
  });
130922
132246
  }
130923
- async putReviewChildResponse(result, metadata2, itemIdentifier) {
132247
+ async putReviewChildResponse(projection, checkpoint) {
132248
+ const { result, metadata: metadata2, itemIdentifier, integration } = projection;
130924
132249
  const selection = metadata2.review.selections.find((candidate) => candidate.itemIdentifier === itemIdentifier);
130925
132250
  if (!selection) {
130926
132251
  return;
130927
132252
  }
130928
132253
  try {
130929
- const childLineItemId = await reviewQuestionLineItemId(result.assessmentLineItem.sourcedId, selection.itemIdentifier);
132254
+ const source = await this.loadPinnedReviewBankSource(metadata2);
132255
+ const item = source.assessment.items.find((candidate) => candidate.identifier === selection.itemIdentifier);
132256
+ const indexed = source.bank.items.find((candidate) => candidate.itemIdentifier === selection.itemIdentifier);
132257
+ if (!item || !indexed) {
132258
+ throw new Error(`Review assessment or bank is missing selected item ${selection.itemIdentifier}`);
132259
+ }
132260
+ const childLineItemId = await this.ensureReviewQuestionLineItem({
132261
+ parentLineItemId: result.assessmentLineItem.sourcedId,
132262
+ integration,
132263
+ bankRevision: metadata2.review.bankRevision,
132264
+ standards: [...indexed.standards],
132265
+ item
132266
+ });
132267
+ if (checkpoint && checkpoint.childLineItemId !== childLineItemId) {
132268
+ throw new Error("Prepared review checkpoint resolved a different line item");
132269
+ }
130930
132270
  const childResultId = await reviewQuestionResultId(result.sourcedId, childLineItemId);
130931
132271
  const childMetadata = buildReviewItemResultMetadata({
130932
132272
  metadata: metadata2,
130933
132273
  selection,
130934
132274
  parentAttemptId: result.sourcedId
130935
132275
  });
130936
- await this.requireClient().api.oneroster.assessmentResults.upsert(childResultId, buildOpenReviewChildResult({
130937
- childLineItemId,
130938
- student: result.student,
130939
- metadata: childMetadata
130940
- }));
132276
+ await Promise.all([
132277
+ this.projectPreparedAssessmentChildResult(checkpoint),
132278
+ this.requireClient().api.oneroster.assessmentResults.upsert(childResultId, buildOpenReviewChildResult({
132279
+ childLineItemId,
132280
+ student: result.student,
132281
+ metadata: childMetadata
132282
+ }))
132283
+ ]);
130941
132284
  } catch (error88) {
130942
132285
  addEvent("assessment.review_child_response_projection_failed", {
130943
132286
  "app.assessment.attempt_id": result.sourcedId,
@@ -130947,14 +132290,26 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
130947
132290
  });
130948
132291
  }
130949
132292
  }
130950
- async projectReviewItemResponse(params, projection) {
130951
- await this.putReviewChildResponse(projection.result, projection.metadata, projection.itemIdentifier);
132293
+ async projectReviewItemResponse(params, projection, checkpoint) {
132294
+ const terminal = assessmentChildProjectionIsTerminal(projection.result);
130952
132295
  try {
130953
- await crossAssessmentAttemptLockBarrier(this.deps.assessmentRuntimeLock, this.deps.db, params.attemptId);
130954
- const latest = await this.peekAttempt(params);
130955
- if (this.isSettled(latest.result) && latest.metadata.purpose === "review") {
130956
- await this.restoreCompletedReviewChildResponses(latest.result, latest.metadata, new Set([projection.itemIdentifier]));
130957
- }
132296
+ await reconcileReviewChildResults({
132297
+ initial: projection,
132298
+ writeOpen: () => this.putReviewChildResponse(projection, checkpoint),
132299
+ crossBarrier: () => crossAssessmentAttemptLockBarrier(this.deps.assessmentRuntimeLock, this.deps.db, params.attemptId),
132300
+ readLatest: () => this.peekAttempt(params),
132301
+ restoreTerminal: async (latest) => {
132302
+ const repair = this.restoreCompletedReviewChildResponses(latest.result, latest.metadata, new Set([projection.itemIdentifier]));
132303
+ if (terminal) {
132304
+ await Promise.allSettled([
132305
+ this.projectPreparedAssessmentChildResult(checkpoint),
132306
+ repair
132307
+ ]);
132308
+ } else {
132309
+ await repair;
132310
+ }
132311
+ }
132312
+ });
130958
132313
  } catch (error88) {
130959
132314
  addEvent("assessment.review_child_projection_barrier_failed", {
130960
132315
  "app.assessment.attempt_id": params.attemptId,
@@ -130964,6 +132319,9 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
130964
132319
  });
130965
132320
  }
130966
132321
  }
132322
+ restoreAllCompletedReviewChildResponses(result, metadata2) {
132323
+ return this.restoreCompletedReviewChildResponses(result, metadata2, new Set(metadata2.itemSubmissions.map((item) => item.itemIdentifier)));
132324
+ }
130967
132325
  async restoreCompletedReviewChildResponses(result, metadata2, changedItemIdentifiers) {
130968
132326
  const outcomes = metadata2.completion?.itemOutcomes;
130969
132327
  const submissionId = metadata2.submissionId;
@@ -131007,28 +132365,22 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
131007
132365
  }
131008
132366
  });
131009
132367
  }
131010
- async finalizeReviewChildResults(input) {
132368
+ reviewItemOutcomes(input) {
131011
132369
  const resultByItem = new Map(input.itemResults.map((itemResult) => [itemResult.itemIdentifier, itemResult]));
131012
132370
  const administeredItems = administeredItemIdentifiers(input.metadata.itemSubmissions);
131013
- return runWithConcurrency(input.metadata.review.selections.filter((selection) => administeredItems.has(selection.itemIdentifier)), TimebackAssessmentRuntimeService2.SCORING_CONCURRENCY, async (selection) => {
132371
+ return input.metadata.review.selections.filter((selection) => administeredItems.has(selection.itemIdentifier)).map((selection) => {
131014
132372
  const itemResult = resultByItem.get(selection.itemIdentifier);
131015
- if (!itemResult) {
132373
+ const submission = itemAdministration(input.metadata.itemSubmissions, selection.itemIdentifier);
132374
+ if (!itemResult || !submission) {
131016
132375
  throw new Error(`Scoring omitted selected review item ${selection.itemIdentifier}`);
131017
132376
  }
131018
- const childLineItemId = await reviewQuestionLineItemId(input.result.assessmentLineItem.sourcedId, selection.itemIdentifier);
131019
- const childResultId = await reviewQuestionResultId(input.result.sourcedId, childLineItemId);
131020
- const finalized = buildFinalizedReviewChildResult({
131021
- childLineItemId,
131022
- student: input.result.student,
131023
- parentAttemptId: input.result.sourcedId,
131024
- metadata: input.metadata,
131025
- selection,
131026
- itemResult,
131027
- submissionId: input.submissionId,
131028
- timestamp: input.timestamp
131029
- });
131030
- await this.requireClient().api.oneroster.assessmentResults.upsert(childResultId, finalized.resultUpdate);
131031
- return finalized.outcome;
132377
+ return {
132378
+ itemIdentifier: selection.itemIdentifier,
132379
+ standard: selection.standard,
132380
+ answered: submission.answered,
132381
+ score: itemResult.score,
132382
+ isCorrect: itemResult.isCorrect
132383
+ };
131032
132384
  });
131033
132385
  }
131034
132386
  async resolveAttemptAuthorization({ gameId, studentId, attemptId, user }, options = {}) {
@@ -131060,11 +132412,12 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
131060
132412
  return { attempt: { result, metadata: metadata2, integration }, enrollment };
131061
132413
  }
131062
132414
  async authorizeAttempt(params, options = {}) {
131063
- const { attempt, enrollment } = await this.resolveAttemptAuthorization(params, options);
131064
- if (!enrollment || attempt.integration.status === "deactivated") {
131065
- throw this.unauthorizedAttempt(params.attemptId);
131066
- }
131067
- return { ...attempt, enrollment };
132415
+ const authorization = await this.resolveAttemptAuthorization(params, options);
132416
+ return this.requireActiveAttempt(authorization, params.attemptId);
132417
+ }
132418
+ requireActiveAttempt(authorization, attemptId) {
132419
+ const enrollment = requireAssessmentMutationAccess(attemptId, authorization.attempt.integration.status, authorization.enrollment);
132420
+ return { ...authorization.attempt, enrollment };
131068
132421
  }
131069
132422
  async authorizeAttemptRead(params) {
131070
132423
  const { attempt, enrollment } = await this.resolveAttemptAuthorization(params);
@@ -132933,7 +134286,7 @@ function getTotalXpFromTimebackConfig(config5) {
132933
134286
  return courseMetadata.metrics.totalXp;
132934
134287
  }
132935
134288
  const resourceMetadata = config5.resource.metadata;
132936
- if (isRecord5(resourceMetadata) && typeof resourceMetadata.xp === "number") {
134289
+ if (isRecord4(resourceMetadata) && typeof resourceMetadata.xp === "number") {
132937
134290
  return resourceMetadata.xp;
132938
134291
  }
132939
134292
  return null;
@@ -133949,8 +135302,8 @@ var init_timeback_service = __esm(async () => {
133949
135302
  });
133950
135303
  }
133951
135304
  static patchCourseMetadata(metadata2, totalXp, options) {
133952
- const nextMetadata = isRecord5(metadata2) ? { ...metadata2 } : {};
133953
- const currentMetrics = isRecord5(nextMetadata.metrics) ? nextMetadata.metrics : {};
135305
+ const nextMetadata = isRecord4(metadata2) ? { ...metadata2 } : {};
135306
+ const currentMetrics = isRecord4(nextMetadata.metrics) ? nextMetadata.metrics : {};
133954
135307
  const metrics2 = { ...currentMetrics };
133955
135308
  if (totalXp === null) {
133956
135309
  delete metrics2.totalXp;
@@ -133975,7 +135328,7 @@ var init_timeback_service = __esm(async () => {
133975
135328
  if (goals === null) {
133976
135329
  delete nextMetadata.goals;
133977
135330
  } else {
133978
- const currentGoals = isRecord5(nextMetadata.goals) ? nextMetadata.goals : {};
135331
+ const currentGoals = isRecord4(nextMetadata.goals) ? nextMetadata.goals : {};
133979
135332
  const nextGoals = { ...currentGoals };
133980
135333
  for (const [key, value] of Object.entries(goals)) {
133981
135334
  if (value === null) {
@@ -133994,7 +135347,7 @@ var init_timeback_service = __esm(async () => {
133994
135347
  if (options?.publishStatus !== undefined) {
133995
135348
  if (options.publishStatus === null) {
133996
135349
  delete nextMetadata.publishStatus;
133997
- const alphaLearn = isRecord5(nextMetadata.AlphaLearn) ? { ...nextMetadata.AlphaLearn } : {};
135350
+ const alphaLearn = isRecord4(nextMetadata.AlphaLearn) ? { ...nextMetadata.AlphaLearn } : {};
133998
135351
  delete alphaLearn.publishStatus;
133999
135352
  if (Object.keys(alphaLearn).length > 0) {
134000
135353
  nextMetadata.AlphaLearn = alphaLearn;
@@ -134003,7 +135356,7 @@ var init_timeback_service = __esm(async () => {
134003
135356
  }
134004
135357
  } else {
134005
135358
  nextMetadata.publishStatus = options.publishStatus;
134006
- const alphaLearn = isRecord5(nextMetadata.AlphaLearn) ? { ...nextMetadata.AlphaLearn } : {};
135359
+ const alphaLearn = isRecord4(nextMetadata.AlphaLearn) ? { ...nextMetadata.AlphaLearn } : {};
134007
135360
  alphaLearn.publishStatus = options.publishStatus === "published" ? "active" : options.publishStatus;
134008
135361
  nextMetadata.AlphaLearn = alphaLearn;
134009
135362
  }
@@ -134021,9 +135374,9 @@ var init_timeback_service = __esm(async () => {
134021
135374
  return Object.keys(nextMetadata).length > 0 ? nextMetadata : undefined;
134022
135375
  }
134023
135376
  static patchResourceMetadata(metadata2, options) {
134024
- const nextMetadata = isRecord5(metadata2) ? { ...metadata2 } : {};
134025
- const playcademyMetadata = isRecord5(nextMetadata.playcademy) ? { ...nextMetadata.playcademy } : {};
134026
- const masteryMetadata = isRecord5(playcademyMetadata.mastery) ? { ...playcademyMetadata.mastery } : {};
135377
+ const nextMetadata = isRecord4(metadata2) ? { ...metadata2 } : {};
135378
+ const playcademyMetadata = isRecord4(nextMetadata.playcademy) ? { ...nextMetadata.playcademy } : {};
135379
+ const masteryMetadata = isRecord4(playcademyMetadata.mastery) ? { ...playcademyMetadata.mastery } : {};
134027
135380
  nextMetadata.subject = options.subject;
134028
135381
  nextMetadata.grades = [options.grade];
134029
135382
  if (options.totalXp === null) {