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

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (3) hide show
  1. package/dist/cli.js +1798 -489
  2. package/dist/server.js +1798 -489
  3. package/package.json +1 -1
package/dist/server.js CHANGED
@@ -1127,7 +1127,7 @@ var package_default;
1127
1127
  var init_package = __esm(() => {
1128
1128
  package_default = {
1129
1129
  name: "@playcademy/sandbox",
1130
- version: "0.8.1-beta.4",
1130
+ version: "0.8.1-beta.5",
1131
1131
  description: "Local development server for Playcademy game development",
1132
1132
  type: "module",
1133
1133
  exports: {
@@ -9301,7 +9301,18 @@ function qtiDirectedPairValidationIssue(values, contract) {
9301
9301
  }
9302
9302
  return null;
9303
9303
  }
9304
- function isRecord2(value) {
9304
+ function validNumericMatchValue(value, baseType) {
9305
+ const syntax = baseType === "integer" ? /^[+-]?\d+$/ : /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i;
9306
+ if (!syntax.test(value.trim())) {
9307
+ return false;
9308
+ }
9309
+ const numeric = Number(value);
9310
+ return Number.isFinite(numeric) && (baseType === "float" || Number.isSafeInteger(numeric));
9311
+ }
9312
+ function sameScore(left, right) {
9313
+ return Number.isFinite(left) && Number.isFinite(right) && Math.abs(left - right) <= Number.EPSILON * Math.max(1, Math.abs(left), Math.abs(right));
9314
+ }
9315
+ function isRecord3(value) {
9305
9316
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
9306
9317
  }
9307
9318
  function stringField(value) {
@@ -9396,13 +9407,13 @@ function dedupeStandards(standards) {
9396
9407
  return [...deduped.values()];
9397
9408
  }
9398
9409
  function qtiAlignmentStandardsFromMetadata(metadata2) {
9399
- if (!isRecord2(metadata2)) {
9410
+ if (!isRecord3(metadata2)) {
9400
9411
  return [];
9401
9412
  }
9402
9413
  const standards = [];
9403
9414
  const alignmentGroups = Array.isArray(metadata2.alignment) ? metadata2.alignment : [];
9404
9415
  for (const group of alignmentGroups) {
9405
- if (isRecord2(group)) {
9416
+ if (isRecord3(group)) {
9406
9417
  const source = stringField(group.curriculum) || "QTI";
9407
9418
  const domains2 = Array.isArray(group.domains) ? group.domains : [];
9408
9419
  if (!domains2.length) {
@@ -9420,11 +9431,11 @@ function qtiAlignmentStandardsFromMetadata(metadata2) {
9420
9431
  }
9421
9432
  } else {
9422
9433
  for (const domain of domains2) {
9423
- if (isRecord2(domain)) {
9434
+ if (isRecord3(domain)) {
9424
9435
  const domainName = stringField(domain.name) || undefined;
9425
9436
  const domainStandards = Array.isArray(domain.standards) ? domain.standards : [];
9426
9437
  for (const standard of domainStandards) {
9427
- if (isRecord2(standard)) {
9438
+ if (isRecord3(standard)) {
9428
9439
  const identifier = stringField(standard.identifier).trim();
9429
9440
  const id = stringField(standard.id).trim() || identifier;
9430
9441
  if (id) {
@@ -9475,12 +9486,9 @@ function isAssessmentAttemptSuperseded(attempt) {
9475
9486
  return attempt.inProgress === ASSESSMENT_ATTEMPT_SUPERSEDED.inProgress && attempt.scoreStatus === ASSESSMENT_ATTEMPT_SUPERSEDED.scoreStatus;
9476
9487
  }
9477
9488
  function classifyAssessmentSubmission(attempt, submissionId) {
9478
- if (isAssessmentAttemptAwaitingAward(attempt)) {
9489
+ if (isAssessmentAttemptAwaitingAward(attempt) || isAssessmentAttemptCompleted(attempt)) {
9479
9490
  return attempt.submissionId === submissionId ? "replay" : "reject";
9480
9491
  }
9481
- if (isAssessmentAttemptCompleted(attempt)) {
9482
- return "reject";
9483
- }
9484
9492
  return isAssessmentAttemptOpen(attempt) ? "submit" : "reject";
9485
9493
  }
9486
9494
  function isSameAssessmentAward(recorded, input) {
@@ -9572,10 +9580,13 @@ function canonicalJson(value) {
9572
9580
  const entries = Object.entries(value).filter(([, entryValue]) => entryValue !== undefined).toSorted(([left], [right]) => compareCodeUnits(left, right)).map(([key, entryValue]) => `${JSON.stringify(key)}:${canonicalJson(entryValue)}`);
9573
9581
  return `{${entries.join(",")}}`;
9574
9582
  }
9575
- async function diagnosticRoutingRevision(manifest) {
9576
- const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(canonicalJson(manifest)));
9583
+ async function canonicalJsonSha256(value) {
9584
+ const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(canonicalJson(value)));
9577
9585
  return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
9578
9586
  }
9587
+ function diagnosticRoutingRevision(manifest) {
9588
+ return canonicalJsonSha256(manifest);
9589
+ }
9579
9590
  function issue(severity, code, message, path, details) {
9580
9591
  return { severity, code, message, ...path ? { path } : {}, ...details ? { details } : {} };
9581
9592
  }
@@ -10632,11 +10643,11 @@ function itemResponsesEqual(left, right) {
10632
10643
  function itemResponseUpdateMatches(current, update) {
10633
10644
  return itemResponsesEqual(current, applyAssessmentItemResponseUpdate(current, update));
10634
10645
  }
10635
- function prepareAssessmentItemSubmission(state, input) {
10646
+ function resolveAssessmentItemReplay(state, input) {
10636
10647
  const flow = assessmentFlowForPurpose(state.purpose);
10637
10648
  if (flow !== "item-submit") {
10638
10649
  return {
10639
- action: "reject",
10650
+ action: "conflict",
10640
10651
  failure: assessmentFlowViolation("Items cannot be submitted individually in this flow.", { flow })
10641
10652
  };
10642
10653
  }
@@ -10645,7 +10656,7 @@ function prepareAssessmentItemSubmission(state, input) {
10645
10656
  const sameRequest = priorSubmission.itemIdentifier === input.itemIdentifier && itemResponseUpdateMatches(state.responses[input.itemIdentifier], input.responses);
10646
10657
  if (!sameRequest) {
10647
10658
  return {
10648
- action: "reject",
10659
+ action: "conflict",
10649
10660
  failure: assessmentFlowViolation("This item submission ID was already used for a different request.", {
10650
10661
  submissionId: input.submissionId,
10651
10662
  itemIdentifier: input.itemIdentifier,
@@ -10661,6 +10672,16 @@ function prepareAssessmentItemSubmission(state, input) {
10661
10672
  submission: priorSubmission
10662
10673
  };
10663
10674
  }
10675
+ return { action: "missing" };
10676
+ }
10677
+ function prepareAssessmentItemSubmission(state, input) {
10678
+ const replay = resolveAssessmentItemReplay(state, input);
10679
+ if (replay.action === "conflict") {
10680
+ return { action: "reject", failure: replay.failure };
10681
+ }
10682
+ if (replay.action === "replay") {
10683
+ return replay;
10684
+ }
10664
10685
  if (state.responseVersion !== input.expectedResponseVersion) {
10665
10686
  return {
10666
10687
  action: "reject",
@@ -10676,7 +10697,7 @@ function prepareAssessmentItemSubmission(state, input) {
10676
10697
  failure: assessmentFlowViolation("Items must be submitted once and in candidate order.", {
10677
10698
  itemIdentifier: input.itemIdentifier,
10678
10699
  previousItemIdentifier: previousItemIdentifier ?? null,
10679
- flow
10700
+ flow: assessmentFlowForPurpose(state.purpose)
10680
10701
  })
10681
10702
  };
10682
10703
  }
@@ -10706,7 +10727,8 @@ function completeAssessmentItemSubmission(state, input, prepared, scoring, submi
10706
10727
  responseVersion: prepared.responseVersion,
10707
10728
  answered: prepared.answered,
10708
10729
  score: scoring.score,
10709
- isCorrect: scoring.isCorrect
10730
+ isCorrect: scoring.isCorrect,
10731
+ ...scoring.grading ? { grading: scoring.grading } : {}
10710
10732
  };
10711
10733
  return {
10712
10734
  responseVersion: prepared.responseVersion,
@@ -10861,6 +10883,145 @@ function assessmentPresentationForAttempt(assessment, attemptId) {
10861
10883
  });
10862
10884
  return { ...assessment, items };
10863
10885
  }
10886
+ function normalizedMatchText(value, baseType) {
10887
+ const normalized = value.replace(ZERO_WIDTH_CHARACTERS, "").replace(CURLY_SINGLE_QUOTES, "'").replace(CURLY_DOUBLE_QUOTES, '"').replace(UNICODE_DASHES, "-");
10888
+ return baseType === "directedPair" ? normalized : normalized.replace(/[\s\u00A0]+/g, " ").trim();
10889
+ }
10890
+ function normalizedArithmeticText(value) {
10891
+ return value.replace(THOUSANDS_GROUP, (number) => number.replaceAll(",", "")).replace(ARITHMETIC_OPERATOR_SPACING, "$1").replaceAll("÷", "/").replace(/[\u00D7\u00B7]/g, "*");
10892
+ }
10893
+ function parsedMatchValue(value, baseType) {
10894
+ if (typeof value !== "string") {
10895
+ return null;
10896
+ }
10897
+ const normalized = normalizedMatchText(value, baseType);
10898
+ if (baseType === "identifier" && !IDENTIFIER_VALUE.test(normalized)) {
10899
+ return null;
10900
+ }
10901
+ if (baseType === "directedPair" && !DIRECTED_PAIR_VALUE.test(normalized)) {
10902
+ return null;
10903
+ }
10904
+ return normalized;
10905
+ }
10906
+ function matchValuesEqual(left, right, baseType) {
10907
+ return left === right || baseType !== "identifier" && baseType !== "directedPair" && normalizedArithmeticText(left) === normalizedArithmeticText(right);
10908
+ }
10909
+ function parsedMatchValues(values, baseType) {
10910
+ const parsed = values.map((value) => parsedMatchValue(value, baseType));
10911
+ return parsed.every((value) => value !== null) ? parsed : null;
10912
+ }
10913
+ function responseValues2(value) {
10914
+ if (typeof value === "string") {
10915
+ return [value];
10916
+ }
10917
+ return Array.isArray(value) ? value : null;
10918
+ }
10919
+ function unorderedMatch(responses, correct, baseType) {
10920
+ if (responses.length !== correct.length) {
10921
+ return false;
10922
+ }
10923
+ const matched = new Set;
10924
+ for (const response of responses) {
10925
+ const match = correct.findIndex((candidate, index) => !matched.has(index) && matchValuesEqual(response, candidate, baseType));
10926
+ if (match === -1) {
10927
+ return false;
10928
+ }
10929
+ matched.add(match);
10930
+ }
10931
+ return true;
10932
+ }
10933
+ function matchComparisonCorrect(rule, values) {
10934
+ if (rule.comparison.kind !== "match" || rule.comparison.correctValues.length === 0) {
10935
+ return false;
10936
+ }
10937
+ const responses = parsedMatchValues(values, rule.baseType);
10938
+ const correct = parsedMatchValues(rule.comparison.correctValues, rule.baseType);
10939
+ if (!responses || !correct) {
10940
+ return false;
10941
+ }
10942
+ if (rule.cardinality === "single") {
10943
+ if (responses.length !== 1) {
10944
+ return false;
10945
+ }
10946
+ if (rule.baseType === "string") {
10947
+ return correct.some((candidate) => matchValuesEqual(responses[0], candidate, rule.baseType));
10948
+ }
10949
+ return correct.length === 1 && matchValuesEqual(responses[0], correct[0], rule.baseType);
10950
+ }
10951
+ if (responses.length !== correct.length) {
10952
+ return false;
10953
+ }
10954
+ if (rule.cardinality === "ordered") {
10955
+ return responses.every((value, index) => matchValuesEqual(value, correct[index], rule.baseType));
10956
+ }
10957
+ return unorderedMatch(responses, correct, rule.baseType);
10958
+ }
10959
+ function roundedNumericValue(value, roundingMode, figures) {
10960
+ if (!Number.isFinite(value) || !Number.isSafeInteger(figures) || (roundingMode === "decimalPlaces" ? figures < 0 : figures <= 0)) {
10961
+ return null;
10962
+ }
10963
+ if (roundingMode === "decimalPlaces") {
10964
+ const multiplier2 = 10 ** figures;
10965
+ const rounded2 = Math.round(value * multiplier2) / multiplier2;
10966
+ return Number.isFinite(rounded2) ? rounded2 : null;
10967
+ }
10968
+ if (value === 0) {
10969
+ return 0;
10970
+ }
10971
+ const magnitude = Math.floor(Math.log10(Math.abs(value)));
10972
+ const multiplier = 10 ** (figures - magnitude - 1);
10973
+ const rounded = Math.round(value * multiplier) / multiplier;
10974
+ return Number.isFinite(rounded) ? rounded : null;
10975
+ }
10976
+ function numericResponseValue(value) {
10977
+ if (typeof value !== "string") {
10978
+ return null;
10979
+ }
10980
+ const numeric = Number(value);
10981
+ return Number.isFinite(numeric) ? numeric : null;
10982
+ }
10983
+ function numericComparisonCorrect(rule, values) {
10984
+ if (values.length !== 1 || rule.cardinality !== "single" || rule.baseType !== "integer" && rule.baseType !== "float" || rule.comparison.kind === "match") {
10985
+ return false;
10986
+ }
10987
+ const response = numericResponseValue(values[0]);
10988
+ const correct = rule.comparison.correctValue;
10989
+ if (response === null || !Number.isFinite(correct) || rule.baseType === "integer" && !Number.isSafeInteger(correct)) {
10990
+ return false;
10991
+ }
10992
+ if (rule.comparison.kind === "numeric-equal") {
10993
+ return response === correct;
10994
+ }
10995
+ const { roundingMode, figures } = rule.comparison;
10996
+ const roundedResponse = roundedNumericValue(response, roundingMode, figures);
10997
+ const roundedCorrect = roundedNumericValue(correct, roundingMode, figures);
10998
+ return roundedResponse !== null && roundedCorrect !== null && roundedResponse === roundedCorrect;
10999
+ }
11000
+ function ruleCorrect(rule, responses) {
11001
+ const response = responses?.[rule.responseIdentifier];
11002
+ if (response === undefined) {
11003
+ return false;
11004
+ }
11005
+ const values = responseValues2(response);
11006
+ if (!values) {
11007
+ return false;
11008
+ }
11009
+ return rule.comparison.kind === "match" ? matchComparisonCorrect(rule, values) : numericComparisonCorrect(rule, values);
11010
+ }
11011
+ function scorePlatformAssessmentItem(item, responses) {
11012
+ const verdicts = item.rules.map((rule) => ruleCorrect(rule, responses));
11013
+ const rawEarned = item.rules.reduce((total, rule, index) => total + (verdicts[index] ? rule.points : 0), 0);
11014
+ const earned = Math.min(item.maxScore, Math.max(0, rawEarned));
11015
+ return {
11016
+ itemIdentifier: item.itemIdentifier,
11017
+ score: {
11018
+ earned,
11019
+ possible: item.maxScore,
11020
+ normalized: item.maxScore <= 0 ? 0 : Math.min(1, Math.max(0, earned / item.maxScore))
11021
+ },
11022
+ isCorrect: verdicts.length > 0 && verdicts.every(Boolean)
11023
+ };
11024
+ }
10864
11025
  function reviewStandardFieldsWithinLimits(input) {
10865
11026
  return input.framework.trim().length <= TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.standardFieldLength && input.identifier.trim().length <= TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.standardFieldLength;
10866
11027
  }
@@ -11347,6 +11508,64 @@ function assessmentItemEarnedScore(interactionPoints, maxScore) {
11347
11508
  const earned = interactionPoints.reduce((sum, points) => sum + Math.max(0, points), 0);
11348
11509
  return Math.min(earned, maxScore);
11349
11510
  }
11511
+ async function assessmentChildResultPayloadHash(payload) {
11512
+ return canonicalJsonSha256({
11513
+ kind: ASSESSMENT_CHILD_RESULT_KIND,
11514
+ version: ASSESSMENT_CHILD_RESULT_VERSION,
11515
+ payload
11516
+ });
11517
+ }
11518
+ async function buildAssessmentChildResultCommand(payload, recordedAt) {
11519
+ const parsedPayload = AssessmentChildResultPayloadV1Schema.parse(payload);
11520
+ const command = {
11521
+ kind: ASSESSMENT_CHILD_RESULT_KIND,
11522
+ version: ASSESSMENT_CHILD_RESULT_VERSION,
11523
+ recordedAt,
11524
+ payloadHash: await assessmentChildResultPayloadHash(parsedPayload),
11525
+ payload: parsedPayload
11526
+ };
11527
+ return AssessmentChildResultCommandV1Schema.parse(command);
11528
+ }
11529
+ function normalizeOmittedResponses(value) {
11530
+ if (!isRecord(value) || !isRecord(value.payload) || value.payload.responses !== undefined) {
11531
+ return value;
11532
+ }
11533
+ return {
11534
+ ...value,
11535
+ payload: {
11536
+ ...value.payload,
11537
+ responses: {}
11538
+ }
11539
+ };
11540
+ }
11541
+ function parseAssessmentChildResultCommand(value) {
11542
+ const direct = AssessmentChildResultCommandV1Schema.safeParse(normalizeOmittedResponses(value));
11543
+ if (direct.success) {
11544
+ return direct.data;
11545
+ }
11546
+ if (!isRecord(value)) {
11547
+ return null;
11548
+ }
11549
+ const nested = AssessmentChildResultCommandV1Schema.safeParse(normalizeOmittedResponses(value[PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY]));
11550
+ return nested.success ? nested.data : null;
11551
+ }
11552
+ async function verifyAssessmentChildResultCommand(metadataOrCommand) {
11553
+ const command = parseAssessmentChildResultCommand(metadataOrCommand);
11554
+ if (!command) {
11555
+ return { ok: false, reason: "malformed" };
11556
+ }
11557
+ if (await assessmentChildResultPayloadHash(command.payload) !== command.payloadHash) {
11558
+ return { ok: false, reason: "payload_hash_mismatch" };
11559
+ }
11560
+ return { ok: true, command };
11561
+ }
11562
+ function assessmentChildResultId(attemptId, submissionId) {
11563
+ return deterministicUUID([
11564
+ ASSESSMENT_CHILD_RESULT_ID_NAMESPACE,
11565
+ IdentitySchema.parse(attemptId),
11566
+ IdentitySchema.parse(submissionId)
11567
+ ].join("\x00"));
11568
+ }
11350
11569
  function normalizeMasteryStandard(input) {
11351
11570
  const standard = canonicalReviewStandardRef(input);
11352
11571
  if (!standard) {
@@ -11387,7 +11606,19 @@ function isReviewItemOutcome(value) {
11387
11606
  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");
11388
11607
  }
11389
11608
  function isAssessmentItemSubmission(value) {
11390
- 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");
11609
+ 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));
11610
+ }
11611
+ function isAssessmentItemGrading(value) {
11612
+ if (!isRecord(value) || !isNonEmptyString(value.graderVersion)) {
11613
+ return false;
11614
+ }
11615
+ if (value.source === "timeback-qti") {
11616
+ return true;
11617
+ }
11618
+ if (value.source === "platform-qti-adapter") {
11619
+ return isNonEmptyString(value.qtiGraderVersion);
11620
+ }
11621
+ return value.source === "platform-artifact" && isNonEmptyString(value.artifactVersion);
11391
11622
  }
11392
11623
  function isDiagnosticTrackResult(value) {
11393
11624
  return isRecord(value) && isNonEmptyString(value.trackKey) && (value.groupKey === undefined || isNonEmptyString(value.groupKey)) && DIAGNOSTIC_TRACK_OUTCOMES.some((outcome) => outcome === value.outcome) && isNonEmptyString(value.resultKey);
@@ -11421,8 +11652,11 @@ function isDiagnosticRoutingState(value) {
11421
11652
  const nextValid = value.next === null || isRecord(value.next) && isNonEmptyString(value.next.stageKey) && isNonEmptyString(value.next.trackKey) && isNonEmptyString(value.next.nodeKey) && isNonEmptyString(value.next.itemIdentifier);
11422
11653
  return stagesValid && tracksValid && nextValid && (value.status === "in-progress" && value.next !== null || value.status === "ready-to-complete" && value.next === null);
11423
11654
  }
11655
+ function isDiagnosticRoutingSnapshot(value, revision) {
11656
+ 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);
11657
+ }
11424
11658
  function isRoutedDiagnosticAttemptMetadata(value, itemSubmissions, responseVersion) {
11425
- 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) {
11659
+ 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) {
11426
11660
  return false;
11427
11661
  }
11428
11662
  const itemIdentifiers = new Set;
@@ -11508,6 +11742,12 @@ function playcademyAssessmentResultMetadata(value) {
11508
11742
  diagnostic: { ...normalized.diagnostic, ledger: [] }
11509
11743
  };
11510
11744
  }
11745
+ if (isRecord(normalized.diagnostic) && Array.isArray(normalized.diagnostic.ledger) && normalized.diagnostic.ledger.length === 0 && normalized.diagnostic.routingSnapshots === undefined) {
11746
+ normalized = {
11747
+ ...normalized,
11748
+ diagnostic: { ...normalized.diagnostic, routingSnapshots: [] }
11749
+ };
11750
+ }
11511
11751
  if (isRecord(normalized.diagnostic) && isRecord(normalized.diagnostic.state)) {
11512
11752
  const state = normalized.diagnostic.state;
11513
11753
  const tracks = Array.isArray(state.tracks) ? state.tracks.map((track) => isRecord(track) ? {
@@ -11515,10 +11755,12 @@ function playcademyAssessmentResultMetadata(value) {
11515
11755
  currentNodeKey: track.currentNodeKey ?? null,
11516
11756
  terminal: track.terminal ?? null
11517
11757
  } : track) : state.tracks;
11758
+ const routingSnapshots = Array.isArray(normalized.diagnostic.routingSnapshots) ? normalized.diagnostic.routingSnapshots.map((snapshot) => isRecord(snapshot) ? { ...snapshot, next: snapshot.next ?? null } : snapshot) : normalized.diagnostic.routingSnapshots;
11518
11759
  normalized = {
11519
11760
  ...normalized,
11520
11761
  diagnostic: {
11521
11762
  ...normalized.diagnostic,
11763
+ ...routingSnapshots === undefined ? {} : { routingSnapshots },
11522
11764
  state: {
11523
11765
  ...state,
11524
11766
  ...tracks === undefined ? {} : { tracks },
@@ -11564,7 +11806,7 @@ function playcademyDiagnosticAssessmentItemResultMetadata(value) {
11564
11806
  const normalized = metadata2.responses === undefined ? { ...metadata2, responses: {} } : metadata2;
11565
11807
  return isPlaycademyDiagnosticAssessmentItemResultMetadataV1(normalized) ? normalized : null;
11566
11808
  }
11567
- var SCRIPT_SCHEMES, DATA_RASTER_IMAGE_PATTERN, QTI_MATHML_ALLOWED_TAGS, metadataSymbol, parser, SUPPORTED_QTI_INTERACTION_TYPES, URL_ATTRIBUTES, IMAGE_URL_ATTRIBUTES, INTERACTION_TYPES, EMPHASIS_TAGS, STRONG_TAGS, CONTENT_SKIPPED_TAGS, BLOCK_CONTENT_KINDS, GAP_MATCH_TOKEN_TAGS, STAGE_GRAPHIC_EXCLUDED_CONTAINERS, BLANK_SENTINEL = "￿", MARKED_BLANK_PATTERN, MARKED_BLANK_GLOBAL_PATTERN, MATCH_CORRECT_TEMPLATE = "http://www.imsglobal.org/question/qti_v3p0/rptemplates/match_correct", MAP_RESPONSE_TEMPLATE = "https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/map_response", MAP_RESPONSE_TEMPLATES, RESPONSE_PROCESSING_TEMPLATES, VOID_BODY_ELEMENTS, URL_ATTRIBUTES2, IMAGE_URL_ATTRIBUTES2, VOID_CHOICE_ELEMENTS, NUMERIC_COMPARISON_ATTRIBUTES, RESPONSE_CARDINALITIES, RESPONSE_BASE_TYPES, POINT_INTERACTION_TYPES, INDEPENDENT_PROCESSING_TAGS, SUPPORTED_BASE_TYPES, SUPPORTED_CARDINALITIES, MATCH_CORRECT_TEMPLATES, UUID_PATTERN, COMMON_CORE_MATH_FRAMEWORK_ALIASES, COMMON_CORE_ELA_FRAMEWORK_ALIASES, COMMON_CORE_FRAMEWORK_ALIASES, ASSESSMENT_ATTEMPT_OPEN, ASSESSMENT_ATTEMPT_COMPLETED, ASSESSMENT_ATTEMPT_AWAITING_AWARD, ASSESSMENT_ATTEMPT_SUPERSEDED, ASSESSMENT_RUNTIME_ERROR_STATUS, ROUTING_KEY_MAX_LENGTH = 128, ROUTING_RESULT_KEY_MAX_LENGTH = 256, ROUTING_STAGE_LIMIT = 32, ROUTING_TRACK_LIMIT = 128, ROUTING_NODE_LIMIT = 1000, ROUTING_TERMINAL_LIMIT = 1000, ROUTING_GROUP_LIMIT = 128, ROUTING_PREDICATE_DEPTH_LIMIT = 8, ROUTING_EXPLORATION_STATE_LIMIT = 200000, RoutingKeySchema, RoutingResultKeySchema, DiagnosticTrackOutcomeSchema, DiagnosticActivationPredicateSchema, DiagnosticRoutingTransitionSchema, DiagnosticRoutingManifestV1Schema, GRADE_VALUES, POINT_RESPONSE_PATTERN, REVIEW_SELECTION_POLICY_VERSION = "unseen-first-least-recently-delivered-v1", PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY = "playcademyReviewBank", PLAYCADEMY_REVIEW_BANK_MANIFEST_VERSION = 1, DEFAULT_REVIEW_SELECTION_POLICY, ASSESSMENT_RUNTIME_FIXTURE_BUNDLE_VERSION = 8, RuntimeSubjectSchema, RuntimeGradeSchema, OptionalQueryGradeSchema, AssessmentResponseValueSchema, AssessmentRuntimeIdentitySchema, ResponseKeySchema, StartAssessmentBaseSchema, AssessmentStandardRefSchema, LatestAssessmentFilterBaseSchema, LatestAssessmentFiltersSchema, LatestRuntimeAssessmentQuerySchema, StartAssessmentBodySchema, AssessmentPreparationReceiptSchema, StartAssessmentRequestBodySchema, SaveAssessmentBodySchema, SubmitAssessmentItemBodySchema, SubmitAssessmentBodySchema, FinalizeAssessmentBodySchema, PrepareRuntimeAssessmentRequestSchema, StartRuntimeAssessmentRequestSchema, SaveRuntimeAssessmentRequestSchema, SubmitAssessmentItemRuntimeRequestSchema, SubmitRuntimeAssessmentRequestSchema, FinalizeRuntimeAssessmentRequestSchema;
11809
+ var SCRIPT_SCHEMES, DATA_RASTER_IMAGE_PATTERN, QTI_MATHML_ALLOWED_TAGS, metadataSymbol, parser, SUPPORTED_QTI_INTERACTION_TYPES, URL_ATTRIBUTES, IMAGE_URL_ATTRIBUTES, INTERACTION_TYPES, EMPHASIS_TAGS, STRONG_TAGS, CONTENT_SKIPPED_TAGS, BLOCK_CONTENT_KINDS, GAP_MATCH_TOKEN_TAGS, STAGE_GRAPHIC_EXCLUDED_CONTAINERS, BLANK_SENTINEL = "￿", MARKED_BLANK_PATTERN, MARKED_BLANK_GLOBAL_PATTERN, MATCH_CORRECT_TEMPLATE = "http://www.imsglobal.org/question/qti_v3p0/rptemplates/match_correct", MAP_RESPONSE_TEMPLATE = "https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/map_response", MAP_RESPONSE_TEMPLATES, RESPONSE_PROCESSING_TEMPLATES, VOID_BODY_ELEMENTS, URL_ATTRIBUTES2, IMAGE_URL_ATTRIBUTES2, VOID_CHOICE_ELEMENTS, NUMERIC_COMPARISON_ATTRIBUTES, RESPONSE_CARDINALITIES, RESPONSE_BASE_TYPES, POINT_INTERACTION_TYPES, INDEPENDENT_PROCESSING_TAGS, PRIVATE_PLAYABLE_KEYS, trimmedNonEmptyString, finiteNumber2, positiveFiniteNumber, IDENTIFIER_VALUE, DIRECTED_PAIR_VALUE, scoringMatchComparisonSchema, scoringNumericEqualComparisonSchema, scoringNumericEqualRoundedComparisonSchema, scoringRuleSchema, scoringRulesSchema, scoringItemSchema, assessmentScoringArtifactItemsSchema, assessmentScoringArtifactSchema, SUPPORTED_BASE_TYPES, SUPPORTED_CARDINALITIES, MATCH_CORRECT_TEMPLATES, UUID_PATTERN, COMMON_CORE_MATH_FRAMEWORK_ALIASES, COMMON_CORE_ELA_FRAMEWORK_ALIASES, COMMON_CORE_FRAMEWORK_ALIASES, ASSESSMENT_ATTEMPT_OPEN, ASSESSMENT_ATTEMPT_COMPLETED, ASSESSMENT_ATTEMPT_AWAITING_AWARD, ASSESSMENT_ATTEMPT_SUPERSEDED, ASSESSMENT_RUNTIME_ERROR_STATUS, ROUTING_KEY_MAX_LENGTH = 128, ROUTING_RESULT_KEY_MAX_LENGTH = 256, ROUTING_STAGE_LIMIT = 32, ROUTING_TRACK_LIMIT = 128, ROUTING_NODE_LIMIT = 1000, ROUTING_TERMINAL_LIMIT = 1000, ROUTING_GROUP_LIMIT = 128, ROUTING_PREDICATE_DEPTH_LIMIT = 8, ROUTING_EXPLORATION_STATE_LIMIT = 200000, RoutingKeySchema, RoutingResultKeySchema, DiagnosticTrackOutcomeSchema, DiagnosticActivationPredicateSchema, DiagnosticRoutingTransitionSchema, DiagnosticRoutingManifestV1Schema, GRADE_VALUES, POINT_RESPONSE_PATTERN, ZERO_WIDTH_CHARACTERS, CURLY_SINGLE_QUOTES, CURLY_DOUBLE_QUOTES, UNICODE_DASHES, ARITHMETIC_OPERATOR_SPACING, THOUSANDS_GROUP, REVIEW_SELECTION_POLICY_VERSION = "unseen-first-least-recently-delivered-v1", PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY = "playcademyReviewBank", PLAYCADEMY_REVIEW_BANK_MANIFEST_VERSION = 1, DEFAULT_REVIEW_SELECTION_POLICY, ASSESSMENT_RUNTIME_FIXTURE_BUNDLE_VERSION = 8, ASSESSMENT_CHILD_RESULT_KIND = "assessment-child-result", ASSESSMENT_CHILD_RESULT_VERSION = 1, ASSESSMENT_CHILD_RESULT_ID_NAMESPACE = "playcademy:assessment-child-result:v1", IdentitySchema, RecordedAtSchema, ResponseKeySchema, AssessmentResponseValueSchema, AssessmentScoreSchema, AssessmentStandardRefSchema, DiagnosticRoutingLedgerEntryV1Schema, AssessmentChildResultAttemptV1Schema, AssessmentChildResultAdministrationV1Schema, AssessmentChildResultGradingV1Schema, ReviewChildResultContextV1Schema, DiagnosticChildResultContextV1Schema, CommonPayloadShape, ReviewChildResultPayloadV1Schema, DiagnosticChildResultPayloadV1Schema, AssessmentChildResultPayloadV1Schema, AssessmentChildResultCommandV1Schema, RuntimeSubjectSchema, RuntimeGradeSchema, OptionalQueryGradeSchema, AssessmentResponseValueSchema2, AssessmentRuntimeIdentitySchema, ResponseKeySchema2, StartAssessmentBaseSchema, AssessmentStandardRefSchema2, LatestAssessmentFilterBaseSchema, LatestAssessmentFiltersSchema, LatestRuntimeAssessmentQuerySchema, StartAssessmentBodySchema, AssessmentPreparationReceiptSchema, StartAssessmentRequestBodySchema, SaveAssessmentBodySchema, SubmitAssessmentItemBodySchema, SubmitAssessmentBodySchema, FinalizeAssessmentBodySchema, PrepareRuntimeAssessmentRequestSchema, StartRuntimeAssessmentRequestSchema, SaveRuntimeAssessmentRequestSchema, SubmitAssessmentItemRuntimeRequestSchema, SubmitRuntimeAssessmentRequestSchema, FinalizeRuntimeAssessmentRequestSchema;
11568
11810
  var init_assessment_runtime2 = __esm(() => {
11569
11811
  init_timeback3();
11570
11812
  init_timeback3();
@@ -11573,6 +11815,7 @@ var init_assessment_runtime2 = __esm(() => {
11573
11815
  init_timeback3();
11574
11816
  init_timeback3();
11575
11817
  init_esm();
11818
+ init_esm();
11576
11819
  init_timeback4();
11577
11820
  init_src();
11578
11821
  init_timeback3();
@@ -11580,6 +11823,9 @@ var init_assessment_runtime2 = __esm(() => {
11580
11823
  init_uuid();
11581
11824
  init_src();
11582
11825
  init_uuid();
11826
+ init_esm();
11827
+ init_src();
11828
+ init_uuid();
11583
11829
  init_src();
11584
11830
  init_timeback4();
11585
11831
  init_esm();
@@ -11718,6 +11964,93 @@ var init_assessment_runtime2 = __esm(() => {
11718
11964
  "qti-sum",
11719
11965
  "qti-base-value"
11720
11966
  ]);
11967
+ PRIVATE_PLAYABLE_KEYS = new Set([
11968
+ "answer",
11969
+ "answerkey",
11970
+ "correctanswer",
11971
+ "correctresponse",
11972
+ "correctresponses",
11973
+ "correct",
11974
+ "correctvalue",
11975
+ "correctvalues",
11976
+ "iscorrect",
11977
+ "responseprocessing",
11978
+ "rule",
11979
+ "rules",
11980
+ "scoring",
11981
+ "scoringrules",
11982
+ "solution",
11983
+ "expected",
11984
+ "expectedanswer",
11985
+ "expectedvalue",
11986
+ "expectedvalues"
11987
+ ]);
11988
+ trimmedNonEmptyString = exports_external.string().min(1).refine((value) => value === value.trim());
11989
+ finiteNumber2 = exports_external.number().finite();
11990
+ positiveFiniteNumber = finiteNumber2.positive();
11991
+ IDENTIFIER_VALUE = /^[A-Za-z_][\w.-]*$/;
11992
+ DIRECTED_PAIR_VALUE = /^[A-Za-z_][\w.-]* [A-Za-z_][\w.-]*$/;
11993
+ scoringMatchComparisonSchema = exports_external.object({
11994
+ kind: exports_external.literal("match"),
11995
+ correctValues: exports_external.array(trimmedNonEmptyString).min(1).refine((values) => new Set(values).size === values.length)
11996
+ }).strict();
11997
+ scoringNumericEqualComparisonSchema = exports_external.object({ kind: exports_external.literal("numeric-equal"), correctValue: finiteNumber2 }).strict();
11998
+ scoringNumericEqualRoundedComparisonSchema = exports_external.object({
11999
+ kind: exports_external.literal("numeric-equal-rounded"),
12000
+ correctValue: finiteNumber2,
12001
+ roundingMode: exports_external.enum(["decimalPlaces", "significantFigures"]),
12002
+ figures: exports_external.number().int().safe().nonnegative()
12003
+ }).strict();
12004
+ scoringRuleSchema = exports_external.object({
12005
+ responseIdentifier: trimmedNonEmptyString,
12006
+ cardinality: exports_external.enum(["single", "multiple", "ordered"]),
12007
+ baseType: exports_external.enum(["string", "identifier", "integer", "float", "directedPair"]),
12008
+ points: positiveFiniteNumber,
12009
+ comparison: exports_external.discriminatedUnion("kind", [
12010
+ scoringMatchComparisonSchema,
12011
+ scoringNumericEqualComparisonSchema,
12012
+ scoringNumericEqualRoundedComparisonSchema
12013
+ ])
12014
+ }).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) => {
12015
+ if (rule.comparison.kind !== "match") {
12016
+ return rule.baseType !== "integer" || Number.isSafeInteger(rule.comparison.correctValue);
12017
+ }
12018
+ if (rule.baseType === "integer" || rule.baseType === "float") {
12019
+ return rule.comparison.correctValues.every((value) => validNumericMatchValue(value, rule.baseType));
12020
+ }
12021
+ if (rule.baseType === "identifier") {
12022
+ return rule.comparison.correctValues.every((value) => IDENTIFIER_VALUE.test(value));
12023
+ }
12024
+ return rule.baseType !== "directedPair" || rule.comparison.correctValues.every((value) => DIRECTED_PAIR_VALUE.test(value));
12025
+ });
12026
+ scoringRulesSchema = exports_external.array(scoringRuleSchema).min(1).refine((rules) => new Set(rules.map((rule) => rule.responseIdentifier)).size === rules.length);
12027
+ scoringItemSchema = exports_external.discriminatedUnion("strategy", [
12028
+ exports_external.object({
12029
+ strategy: exports_external.literal("qti"),
12030
+ itemIdentifier: trimmedNonEmptyString,
12031
+ maxScore: positiveFiniteNumber,
12032
+ reason: exports_external.enum([
12033
+ "response-mapping",
12034
+ "area-mapping",
12035
+ "unsupported-base-type",
12036
+ "unsupported-cardinality",
12037
+ "invalid-correct-response",
12038
+ "no-scorable-response",
12039
+ "unsupported-processing-template",
12040
+ "unsupported-response-processing",
12041
+ "cross-response-processing",
12042
+ "inconsistent-max-score"
12043
+ ])
12044
+ }).strict(),
12045
+ exports_external.object({
12046
+ strategy: exports_external.literal("platform"),
12047
+ itemIdentifier: trimmedNonEmptyString,
12048
+ maxScore: positiveFiniteNumber,
12049
+ rules: scoringRulesSchema
12050
+ }).strict()
12051
+ ]).refine((item) => item.strategy === "qti" || sameScore(item.rules.reduce((total, rule) => total + rule.points, 0), item.maxScore));
12052
+ assessmentScoringArtifactItemsSchema = exports_external.array(scoringItemSchema).refine((items) => new Set(items.map((item) => item.itemIdentifier)).size === items.length);
12053
+ assessmentScoringArtifactSchema = exports_external.object({ items: assessmentScoringArtifactItemsSchema }).strict();
11721
12054
  SUPPORTED_BASE_TYPES = new Set([
11722
12055
  "string",
11723
12056
  "identifier",
@@ -11844,9 +12177,140 @@ var init_assessment_runtime2 = __esm(() => {
11844
12177
  }).strict();
11845
12178
  GRADE_VALUES = TIMEBACK_GRADES;
11846
12179
  POINT_RESPONSE_PATTERN = /^-?\d+(?:\.\d+)? -?\d+(?:\.\d+)?$/;
12180
+ ZERO_WIDTH_CHARACTERS = /[\u200B\uFEFF\u200C\u200D]/g;
12181
+ CURLY_SINGLE_QUOTES = /[\u2018\u2019\u201A\u201B]/g;
12182
+ CURLY_DOUBLE_QUOTES = /[\u201C\u201D\u201E\u201F]/g;
12183
+ UNICODE_DASHES = /[\u2010\u2011\u2012\u2013\u2014\u2015\u2212]/g;
12184
+ ARITHMETIC_OPERATOR_SPACING = /\s*([()+\-*/xX\u00D7\u00F7\u00B7])\s*/g;
12185
+ THOUSANDS_GROUP = /\b\d{1,3}(?:,\d{3})+(?:\.\d+)?\b/g;
11847
12186
  DEFAULT_REVIEW_SELECTION_POLICY = {
11848
12187
  version: REVIEW_SELECTION_POLICY_VERSION
11849
12188
  };
12189
+ IdentitySchema = exports_external.string().min(1).refine((value) => value.trim() === value, {
12190
+ message: "Identifiers must not contain surrounding whitespace"
12191
+ });
12192
+ RecordedAtSchema = exports_external.string().datetime().refine((value) => new Date(value).toISOString() === value, {
12193
+ message: "recordedAt must be a canonical UTC timestamp"
12194
+ });
12195
+ ResponseKeySchema = IdentitySchema;
12196
+ AssessmentResponseValueSchema = exports_external.custom(isAssessmentResponseValue, {
12197
+ message: `Response values are non-empty text, at most ${TIMEBACK_ASSESSMENT_RESPONSE_VALUE_MAX_LENGTH} characters each`
12198
+ });
12199
+ AssessmentScoreSchema = exports_external.object({
12200
+ earned: exports_external.number().finite().nonnegative(),
12201
+ possible: exports_external.number().finite().nonnegative(),
12202
+ normalized: exports_external.number().finite().min(0).max(1)
12203
+ }).strict().superRefine((score, context) => {
12204
+ const expectedNormalized = score.possible === 0 ? 0 : score.earned / score.possible;
12205
+ if (score.earned > score.possible) {
12206
+ context.addIssue({
12207
+ code: exports_external.ZodIssueCode.custom,
12208
+ message: "Earned score must not exceed possible score",
12209
+ path: ["earned"]
12210
+ });
12211
+ }
12212
+ if (score.normalized !== expectedNormalized) {
12213
+ context.addIssue({
12214
+ code: exports_external.ZodIssueCode.custom,
12215
+ message: "Normalized score must equal earned divided by possible",
12216
+ path: ["normalized"]
12217
+ });
12218
+ }
12219
+ });
12220
+ AssessmentStandardRefSchema = exports_external.object({
12221
+ framework: IdentitySchema,
12222
+ identifier: IdentitySchema
12223
+ }).strict();
12224
+ DiagnosticRoutingLedgerEntryV1Schema = exports_external.object({
12225
+ stageKey: IdentitySchema,
12226
+ trackKey: IdentitySchema,
12227
+ routingNodeKey: IdentitySchema,
12228
+ itemIdentifier: IdentitySchema,
12229
+ isCorrect: exports_external.boolean()
12230
+ }).strict();
12231
+ AssessmentChildResultAttemptV1Schema = exports_external.object({
12232
+ attemptId: IdentitySchema,
12233
+ gameId: IdentitySchema,
12234
+ studentId: IdentitySchema,
12235
+ activityId: IdentitySchema,
12236
+ courseId: IdentitySchema,
12237
+ integrationId: IdentitySchema,
12238
+ enrollmentId: IdentitySchema,
12239
+ selectedTest: exports_external.object({
12240
+ assessmentKey: IdentitySchema,
12241
+ identifier: IdentitySchema,
12242
+ contentRevision: IdentitySchema
12243
+ }).strict()
12244
+ }).strict();
12245
+ AssessmentChildResultAdministrationV1Schema = exports_external.object({
12246
+ submissionId: IdentitySchema,
12247
+ responseVersion: exports_external.number().int().positive(),
12248
+ itemIdentifier: IdentitySchema
12249
+ }).strict();
12250
+ AssessmentChildResultGradingV1Schema = exports_external.discriminatedUnion("source", [
12251
+ exports_external.object({
12252
+ source: exports_external.literal("timeback-qti"),
12253
+ graderVersion: IdentitySchema
12254
+ }).strict(),
12255
+ exports_external.object({
12256
+ source: exports_external.literal("platform-qti-adapter"),
12257
+ graderVersion: IdentitySchema,
12258
+ qtiGraderVersion: IdentitySchema
12259
+ }).strict(),
12260
+ exports_external.object({
12261
+ source: exports_external.literal("platform-artifact"),
12262
+ artifactVersion: IdentitySchema,
12263
+ graderVersion: IdentitySchema
12264
+ }).strict()
12265
+ ]);
12266
+ ReviewChildResultContextV1Schema = exports_external.object({
12267
+ kind: exports_external.literal("review"),
12268
+ bankRevision: IdentitySchema,
12269
+ standard: AssessmentStandardRefSchema
12270
+ }).strict();
12271
+ DiagnosticChildResultContextV1Schema = exports_external.object({
12272
+ kind: exports_external.literal("platform-routed-diagnostic"),
12273
+ definitionId: IdentitySchema,
12274
+ diagnosticKey: IdentitySchema,
12275
+ routingRevision: IdentitySchema,
12276
+ transition: DiagnosticRoutingLedgerEntryV1Schema
12277
+ }).strict();
12278
+ CommonPayloadShape = {
12279
+ attempt: AssessmentChildResultAttemptV1Schema,
12280
+ administration: AssessmentChildResultAdministrationV1Schema,
12281
+ responses: exports_external.record(ResponseKeySchema, AssessmentResponseValueSchema),
12282
+ score: AssessmentScoreSchema,
12283
+ grading: AssessmentChildResultGradingV1Schema
12284
+ };
12285
+ ReviewChildResultPayloadV1Schema = exports_external.object({
12286
+ ...CommonPayloadShape,
12287
+ isCorrect: exports_external.boolean().optional(),
12288
+ context: ReviewChildResultContextV1Schema
12289
+ }).strict();
12290
+ DiagnosticChildResultPayloadV1Schema = exports_external.object({
12291
+ ...CommonPayloadShape,
12292
+ isCorrect: exports_external.boolean(),
12293
+ context: DiagnosticChildResultContextV1Schema
12294
+ }).strict().superRefine((payload, context) => {
12295
+ if (payload.context.transition.itemIdentifier !== payload.administration.itemIdentifier || payload.context.transition.isCorrect !== payload.isCorrect) {
12296
+ context.addIssue({
12297
+ code: exports_external.ZodIssueCode.custom,
12298
+ message: "Diagnostic transition must match the administered item and correctness",
12299
+ path: ["context", "transition"]
12300
+ });
12301
+ }
12302
+ });
12303
+ AssessmentChildResultPayloadV1Schema = exports_external.union([
12304
+ ReviewChildResultPayloadV1Schema,
12305
+ DiagnosticChildResultPayloadV1Schema
12306
+ ]);
12307
+ AssessmentChildResultCommandV1Schema = exports_external.object({
12308
+ kind: exports_external.literal(ASSESSMENT_CHILD_RESULT_KIND),
12309
+ version: exports_external.literal(ASSESSMENT_CHILD_RESULT_VERSION),
12310
+ recordedAt: RecordedAtSchema,
12311
+ payloadHash: exports_external.string().regex(/^[a-f0-9]{64}$/),
12312
+ payload: AssessmentChildResultPayloadV1Schema
12313
+ }).strict();
11850
12314
  RuntimeSubjectSchema = exports_external.enum(TIMEBACK_SUBJECTS);
11851
12315
  RuntimeGradeSchema = exports_external.number().refine(isTimebackGrade, {
11852
12316
  message: `Grade must be one of: ${TIMEBACK_GRADES.join(", ")}`
@@ -11857,14 +12321,14 @@ var init_assessment_runtime2 = __esm(() => {
11857
12321
  }
11858
12322
  return typeof value === "string" && value.trim() !== "" ? Number(value) : Number.NaN;
11859
12323
  }, RuntimeGradeSchema.optional());
11860
- AssessmentResponseValueSchema = exports_external.custom(isAssessmentResponseValue, {
12324
+ AssessmentResponseValueSchema2 = exports_external.custom(isAssessmentResponseValue, {
11861
12325
  message: `Response values are non-empty text, at most ${TIMEBACK_ASSESSMENT_RESPONSE_VALUE_MAX_LENGTH} characters each`
11862
12326
  });
11863
12327
  AssessmentRuntimeIdentitySchema = exports_external.object({
11864
12328
  gameId: exports_external.string().uuid(),
11865
12329
  studentId: exports_external.string().trim().min(1)
11866
12330
  });
11867
- ResponseKeySchema = exports_external.string().refine((key) => key.length > 0 && key.trim() === key, {
12331
+ ResponseKeySchema2 = exports_external.string().refine((key) => key.length > 0 && key.trim() === key, {
11868
12332
  message: "Response keys must be non-empty without surrounding whitespace"
11869
12333
  });
11870
12334
  StartAssessmentBaseSchema = exports_external.object({
@@ -11872,7 +12336,7 @@ var init_assessment_runtime2 = __esm(() => {
11872
12336
  subject: RuntimeSubjectSchema.optional(),
11873
12337
  grade: RuntimeGradeSchema.optional()
11874
12338
  });
11875
- AssessmentStandardRefSchema = exports_external.object({
12339
+ AssessmentStandardRefSchema2 = exports_external.object({
11876
12340
  framework: exports_external.string().trim().min(1).max(TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.standardFieldLength),
11877
12341
  identifier: exports_external.string().trim().min(1).max(TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.standardFieldLength)
11878
12342
  }).refine((standard) => {
@@ -11889,7 +12353,7 @@ var init_assessment_runtime2 = __esm(() => {
11889
12353
  }),
11890
12354
  LatestAssessmentFilterBaseSchema.extend({
11891
12355
  purpose: exports_external.literal("mastery"),
11892
- standard: AssessmentStandardRefSchema
12356
+ standard: AssessmentStandardRefSchema2
11893
12357
  })
11894
12358
  ]);
11895
12359
  LatestRuntimeAssessmentQuerySchema = exports_external.intersection(AssessmentRuntimeIdentitySchema, LatestAssessmentFiltersSchema);
@@ -11903,26 +12367,26 @@ var init_assessment_runtime2 = __esm(() => {
11903
12367
  }),
11904
12368
  StartAssessmentBaseSchema.extend({
11905
12369
  purpose: exports_external.literal("review"),
11906
- standards: exports_external.array(AssessmentStandardRefSchema).min(1).max(TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.standards),
12370
+ standards: exports_external.array(AssessmentStandardRefSchema2).min(1).max(TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.standards),
11907
12371
  candidateItemsPerStandard: exports_external.number().int().positive().max(TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.candidateItemsPerStandard).optional()
11908
12372
  }),
11909
12373
  StartAssessmentBaseSchema.extend({
11910
12374
  purpose: exports_external.literal("mastery"),
11911
- standard: AssessmentStandardRefSchema
12375
+ standard: AssessmentStandardRefSchema2
11912
12376
  })
11913
12377
  ]);
11914
12378
  AssessmentPreparationReceiptSchema = exports_external.string().max(32000);
11915
12379
  StartAssessmentRequestBodySchema = exports_external.intersection(StartAssessmentBodySchema, exports_external.object({ preparationReceipt: AssessmentPreparationReceiptSchema.optional() }));
11916
12380
  SaveAssessmentBodySchema = exports_external.object({
11917
12381
  expectedResponseVersion: exports_external.number().int().nonnegative(),
11918
- responses: exports_external.record(ResponseKeySchema, exports_external.record(ResponseKeySchema, AssessmentResponseValueSchema.nullable()))
12382
+ responses: exports_external.record(ResponseKeySchema2, exports_external.record(ResponseKeySchema2, AssessmentResponseValueSchema2.nullable()))
11919
12383
  });
11920
12384
  SubmitAssessmentItemBodySchema = exports_external.object({
11921
12385
  expectedResponseVersion: exports_external.number().int().nonnegative(),
11922
12386
  submissionId: exports_external.string().trim().min(1),
11923
- itemIdentifier: ResponseKeySchema,
11924
- routingNodeKey: ResponseKeySchema.optional(),
11925
- responses: exports_external.record(ResponseKeySchema, AssessmentResponseValueSchema.nullable())
12387
+ itemIdentifier: ResponseKeySchema2,
12388
+ routingNodeKey: ResponseKeySchema2.optional(),
12389
+ responses: exports_external.record(ResponseKeySchema2, AssessmentResponseValueSchema2.nullable())
11926
12390
  });
11927
12391
  SubmitAssessmentBodySchema = exports_external.object({
11928
12392
  expectedResponseVersion: exports_external.number().int().nonnegative(),
@@ -36814,7 +37278,7 @@ function kebabToTitleCase(kebabStr) {
36814
37278
  }
36815
37279
 
36816
37280
  // ../api-core/src/utils/timeback.util.ts
36817
- function isRecord5(value) {
37281
+ function isRecord4(value) {
36818
37282
  return typeof value === "object" && value !== null;
36819
37283
  }
36820
37284
  function resolveIntegrationSubject(patchSubject, liveSubject, storedSubject) {
@@ -36891,9 +37355,9 @@ function getGeneratedMetricValue(event, type) {
36891
37355
  return Number.isFinite(value) ? value : undefined;
36892
37356
  }
36893
37357
  function getMergedCaliperExtensions(event) {
36894
- const objectActivityExtensions = isRecord5(event.object.activity?.extensions) ? event.object.activity.extensions : undefined;
36895
- const generatedExtensions = isRecord5(event.generated?.extensions) ? event.generated.extensions : undefined;
36896
- const eventExtensions = isRecord5(event.extensions) ? event.extensions : undefined;
37358
+ const objectActivityExtensions = isRecord4(event.object.activity?.extensions) ? event.object.activity.extensions : undefined;
37359
+ const generatedExtensions = isRecord4(event.generated?.extensions) ? event.generated.extensions : undefined;
37360
+ const eventExtensions = isRecord4(event.extensions) ? event.extensions : undefined;
36897
37361
  return {
36898
37362
  ...objectActivityExtensions,
36899
37363
  ...generatedExtensions,
@@ -36902,7 +37366,7 @@ function getMergedCaliperExtensions(event) {
36902
37366
  }
36903
37367
  function getPlaycademyMetadata(event) {
36904
37368
  const extensions = getMergedCaliperExtensions(event);
36905
- return isRecord5(extensions.playcademy) ? extensions.playcademy : undefined;
37369
+ return isRecord4(extensions.playcademy) ? extensions.playcademy : undefined;
36906
37370
  }
36907
37371
  function getActivityId(event, playcademy) {
36908
37372
  const metadataActivityId = getStringValue(playcademy?.activityId);
@@ -36954,7 +37418,7 @@ function buildResourceMetadata({
36954
37418
  }
36955
37419
  function getDurationSecondsFromExtensions(event) {
36956
37420
  const extensions = getMergedCaliperExtensions(event);
36957
- const playcademy = isRecord5(extensions.playcademy) ? extensions.playcademy : undefined;
37421
+ const playcademy = isRecord4(extensions.playcademy) ? extensions.playcademy : undefined;
36958
37422
  const rawValue = extensions.durationSeconds ?? playcademy?.durationSeconds;
36959
37423
  const value = typeof rawValue === "number" ? rawValue : Number(rawValue);
36960
37424
  return Number.isFinite(value) ? value : undefined;
@@ -47457,7 +47921,7 @@ function rejectSystemManagedReview(input, context2) {
47457
47921
  });
47458
47922
  }
47459
47923
  }
47460
- var TimebackGradeSchema, TimebackSubjectSchema, CourseGoalsSchema, UpdateGameTimebackIntegrationRequestSchema, CreateGameTimebackIntegrationRequestSchema, TimebackActivityDataSchema, EndActivityRequestSchema, GameRunMetricsSchema, GameCourseMetricsSchema, GameMetricsResponseSchema, AdvanceCourseRequestSchema, UnenrollCourseRequestSchema, HeartbeatRequestSchema, PopulateStudentRequestSchema, DerivedPlatformCourseConfigSchema, TimebackBaseConfigSchema, PlatformTimebackSetupRequestSchema, AdminTimebackMutationBaseSchema, AdminAttributionDateSchema, ADMIN_GRANT_XP_MIN = -1e5, ADMIN_GRANT_XP_MAX = 1e5, ADMIN_GRANT_XP_AMOUNT_RANGE_MESSAGE, GrantTimebackXpRequestSchema, AdjustTimebackTimeRequestSchema, AdjustTimebackMasteryRequestSchema, ReconcileMasteryForConfigChangeSchema, EnrollStudentRequestSchema, UnenrollStudentRequestSchema, ReactivateEnrollmentRequestSchema, VerifyTimebackMetricDiscrepancyRequestSchema, AssessmentPurposeSchema, AssessmentStatusSchema, AssessmentKeySchema, DiagnosticKeySchema, DiagnosticRoutingManifestSchema, DiagnosticAssessmentDefinitionInputSchema, AssessmentStandardRefSchema2, CreateAssessmentRequestSchema, UpdateAssessmentRequestSchema, CopyAssessmentRequestSchema, AssessmentAssociationImportEntrySchema, AttachExistingAssessmentsRequestSchema, ReorderQuestionsRequestSchema;
47924
+ var TimebackGradeSchema, TimebackSubjectSchema, CourseGoalsSchema, UpdateGameTimebackIntegrationRequestSchema, CreateGameTimebackIntegrationRequestSchema, TimebackActivityDataSchema, EndActivityRequestSchema, GameRunMetricsSchema, GameCourseMetricsSchema, GameMetricsResponseSchema, AdvanceCourseRequestSchema, UnenrollCourseRequestSchema, HeartbeatRequestSchema, PopulateStudentRequestSchema, DerivedPlatformCourseConfigSchema, TimebackBaseConfigSchema, PlatformTimebackSetupRequestSchema, AdminTimebackMutationBaseSchema, AdminAttributionDateSchema, ADMIN_GRANT_XP_MIN = -1e5, ADMIN_GRANT_XP_MAX = 1e5, ADMIN_GRANT_XP_AMOUNT_RANGE_MESSAGE, GrantTimebackXpRequestSchema, AdjustTimebackTimeRequestSchema, AdjustTimebackMasteryRequestSchema, ReconcileMasteryForConfigChangeSchema, EnrollStudentRequestSchema, UnenrollStudentRequestSchema, ReactivateEnrollmentRequestSchema, VerifyTimebackMetricDiscrepancyRequestSchema, AssessmentPurposeSchema, AssessmentStatusSchema, AssessmentKeySchema, DiagnosticKeySchema, DiagnosticRoutingManifestSchema, DiagnosticAssessmentDefinitionInputSchema, AssessmentStandardRefSchema3, CreateAssessmentRequestSchema, UpdateAssessmentRequestSchema, CopyAssessmentRequestSchema, AssessmentAssociationImportEntrySchema, AttachExistingAssessmentsRequestSchema, ReorderQuestionsRequestSchema;
47461
47925
  var init_schemas4 = __esm(() => {
47462
47926
  init_esm();
47463
47927
  init_src();
@@ -47709,7 +48173,7 @@ var init_schemas4 = __esm(() => {
47709
48173
  diagnosticKey: DiagnosticKeySchema,
47710
48174
  routingManifest: DiagnosticRoutingManifestSchema
47711
48175
  });
47712
- AssessmentStandardRefSchema2 = exports_external.object({
48176
+ AssessmentStandardRefSchema3 = exports_external.object({
47713
48177
  framework: exports_external.string().trim().min(1).max(TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.standardFieldLength),
47714
48178
  identifier: exports_external.string().trim().min(1).max(TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.standardFieldLength)
47715
48179
  });
@@ -47717,7 +48181,7 @@ var init_schemas4 = __esm(() => {
47717
48181
  assessmentKey: AssessmentKeySchema,
47718
48182
  title: exports_external.string().min(1, "Assessment title is required"),
47719
48183
  purpose: AssessmentPurposeSchema,
47720
- standard: AssessmentStandardRefSchema2.optional()
48184
+ standard: AssessmentStandardRefSchema3.optional()
47721
48185
  }).superRefine((input, context2) => {
47722
48186
  requireMasteryStandard(input, context2);
47723
48187
  rejectSystemManagedReview(input, context2);
@@ -47725,7 +48189,7 @@ var init_schemas4 = __esm(() => {
47725
48189
  UpdateAssessmentRequestSchema = exports_external.object({
47726
48190
  title: exports_external.string().trim().min(1, "Assessment title is required").optional(),
47727
48191
  purpose: AssessmentPurposeSchema.optional(),
47728
- standard: AssessmentStandardRefSchema2.optional(),
48192
+ standard: AssessmentStandardRefSchema3.optional(),
47729
48193
  diagnostic: DiagnosticAssessmentDefinitionInputSchema.nullable().optional(),
47730
48194
  status: AssessmentStatusSchema.optional()
47731
48195
  }).refine((input) => input.title !== undefined || input.purpose !== undefined || input.standard !== undefined || input.diagnostic !== undefined || input.status !== undefined, {
@@ -47735,7 +48199,7 @@ var init_schemas4 = __esm(() => {
47735
48199
  assessmentKey: AssessmentKeySchema,
47736
48200
  testIdentifier: exports_external.string().trim().min(1, "Assessment identifier is required"),
47737
48201
  purpose: AssessmentPurposeSchema,
47738
- standard: AssessmentStandardRefSchema2.optional()
48202
+ standard: AssessmentStandardRefSchema3.optional()
47739
48203
  }).superRefine((input, context2) => {
47740
48204
  requireMasteryStandard(input, context2);
47741
48205
  rejectSystemManagedReview(input, context2);
@@ -47744,7 +48208,7 @@ var init_schemas4 = __esm(() => {
47744
48208
  assessmentKey: AssessmentKeySchema,
47745
48209
  qtiTestIdentifier: exports_external.string().trim().min(1, "Assessment identifier is required"),
47746
48210
  purpose: AssessmentPurposeSchema,
47747
- standard: AssessmentStandardRefSchema2.optional()
48211
+ standard: AssessmentStandardRefSchema3.optional()
47748
48212
  }).superRefine((input, context2) => {
47749
48213
  requireMasteryStandard(input, context2);
47750
48214
  rejectSystemManagedReview(input, context2);
@@ -94751,16 +95215,16 @@ function qtiResponseMappingMaximum(input) {
94751
95215
  return null;
94752
95216
  }
94753
95217
  const defaultValue = mapping.defaultValue ?? 0;
94754
- const responseValues2 = input.responseValues ? [...new Set(input.responseValues)].map((value) => mapping.entries[value] ?? defaultValue) : entryValues;
95218
+ const responseValues3 = input.responseValues ? [...new Set(input.responseValues)].map((value) => mapping.entries[value] ?? defaultValue) : entryValues;
94755
95219
  let maximum;
94756
95220
  if (input.cardinality === "single") {
94757
- maximum = Math.max(0, defaultValue, ...responseValues2);
95221
+ maximum = Math.max(0, defaultValue, ...responseValues3);
94758
95222
  } else {
94759
95223
  if (!input.responseValues && defaultValue > 0) {
94760
95224
  return null;
94761
95225
  }
94762
- const limit = Math.min(input.maximumResponses ?? responseValues2.length, responseValues2.length);
94763
- maximum = responseValues2.toSorted((left, right) => right - left).slice(0, limit).reduce((sum, value) => sum + Math.max(0, value), 0);
95226
+ const limit = Math.min(input.maximumResponses ?? responseValues3.length, responseValues3.length);
95227
+ maximum = responseValues3.toSorted((left, right) => right - left).slice(0, limit).reduce((sum, value) => sum + Math.max(0, value), 0);
94764
95228
  }
94765
95229
  if (mapping.lowerBound !== undefined) {
94766
95230
  maximum = Math.max(maximum, mapping.lowerBound);
@@ -95322,16 +95786,16 @@ function authoredMappingContext(interaction, declaration) {
95322
95786
  return {};
95323
95787
  }
95324
95788
  const type = qtiAuthoringInteractionType(interaction);
95325
- let responseValues2;
95789
+ let responseValues3;
95326
95790
  if (type === "choice" || type === "inline-choice" || type === "order" || type === "hotspot") {
95327
- responseValues2 = interaction.choices?.map((choice) => String(choice.identifier)) ?? [];
95791
+ responseValues3 = interaction.choices?.map((choice) => String(choice.identifier)) ?? [];
95328
95792
  } else if (type === "hottext") {
95329
- responseValues2 = (interaction.textSegments ?? []).flatMap((segment) => segment.selectable && segment.identifier ? [segment.identifier] : []);
95793
+ responseValues3 = (interaction.textSegments ?? []).flatMap((segment) => segment.selectable && segment.identifier ? [segment.identifier] : []);
95330
95794
  } else if (type === "match" || type === "graphic-gap-match") {
95331
95795
  const [left = [], right = []] = interaction.choiceSets ?? [];
95332
- responseValues2 = authoredResponsePairs(left.map((choice) => String(choice.identifier)), right.map((choice) => String(choice.identifier)));
95796
+ responseValues3 = authoredResponsePairs(left.map((choice) => String(choice.identifier)), right.map((choice) => String(choice.identifier)));
95333
95797
  }
95334
- const capacity = responseValues2?.length ?? Object.keys(declaration.mapping?.entries ?? {}).length;
95798
+ const capacity = responseValues3?.length ?? Object.keys(declaration.mapping?.entries ?? {}).length;
95335
95799
  const limitAttribute = type === "match" || type === "gap-match" || type === "graphic-associate" || type === "graphic-gap-match" ? "max-associations" : "max-choices";
95336
95800
  const rawLimit = interaction.attributes?.[limitAttribute];
95337
95801
  const declaredLimit = rawLimit === undefined ? undefined : Number(rawLimit);
@@ -95350,7 +95814,7 @@ function authoredMappingContext(interaction, declaration) {
95350
95814
  maximumResponses = capacity;
95351
95815
  }
95352
95816
  return {
95353
- ...responseValues2 ? { responseValues: responseValues2 } : {},
95817
+ ...responseValues3 ? { responseValues: responseValues3 } : {},
95354
95818
  ...maximumResponses > 0 ? { maximumResponses } : {}
95355
95819
  };
95356
95820
  }
@@ -95534,6 +95998,13 @@ function qtiGapContentMatchesGaps(nodes, gaps) {
95534
95998
  const identifiers = gapContentIdentifiers(nodes);
95535
95999
  return identifiers.length === gaps.length && identifiers.every((identifier, index2) => identifier === gaps[index2]);
95536
96000
  }
96001
+ function qtiFloatValue(value) {
96002
+ if (value === undefined || !QTI_FLOAT_VALUE.test(value.trim())) {
96003
+ return;
96004
+ }
96005
+ const number7 = Number(value);
96006
+ return Number.isFinite(number7) ? number7 : undefined;
96007
+ }
95537
96008
  function normalizedInteractionType(value) {
95538
96009
  if (typeof value !== "string") {
95539
96010
  return;
@@ -95752,24 +96223,24 @@ function interactionResponseSpace(item, declaration) {
95752
96223
  }
95753
96224
  case "match": {
95754
96225
  const [source = [], target = []] = interaction.choiceSets;
95755
- const responseValues2 = responsePairs(source.map((choice) => choice.identifier), target.map((choice) => choice.identifier));
96226
+ const responseValues3 = responsePairs(source.map((choice) => choice.identifier), target.map((choice) => choice.identifier));
95756
96227
  return {
95757
- responseValues: responseValues2,
95758
- maximumResponses: qtiAttributeMaximum(attributes2, "max-associations", 1, responseValues2.length)
96228
+ responseValues: responseValues3,
96229
+ maximumResponses: qtiAttributeMaximum(attributes2, "max-associations", 1, responseValues3.length)
95759
96230
  };
95760
96231
  }
95761
96232
  case "hottext": {
95762
- const responseValues2 = interaction.textSegments.flatMap((segment) => segment.selectable && segment.identifier ? [segment.identifier] : []);
96233
+ const responseValues3 = interaction.textSegments.flatMap((segment) => segment.selectable && segment.identifier ? [segment.identifier] : []);
95763
96234
  return {
95764
- responseValues: responseValues2,
95765
- maximumResponses: qtiAttributeMaximum(attributes2, "max-choices", 1, responseValues2.length)
96235
+ responseValues: responseValues3,
96236
+ maximumResponses: qtiAttributeMaximum(attributes2, "max-choices", 1, responseValues3.length)
95766
96237
  };
95767
96238
  }
95768
96239
  case "gap-match": {
95769
- const responseValues2 = responsePairs(interaction.gapTexts.map((choice) => choice.identifier), interaction.gaps);
96240
+ const responseValues3 = responsePairs(interaction.gapTexts.map((choice) => choice.identifier), interaction.gaps);
95770
96241
  return {
95771
- responseValues: responseValues2,
95772
- maximumResponses: qtiAttributeMaximum(attributes2, "max-associations", 0, responseValues2.length)
96242
+ responseValues: responseValues3,
96243
+ maximumResponses: qtiAttributeMaximum(attributes2, "max-associations", 0, responseValues3.length)
95773
96244
  };
95774
96245
  }
95775
96246
  case "hotspot": {
@@ -95779,10 +96250,10 @@ function interactionResponseSpace(item, declaration) {
95779
96250
  };
95780
96251
  }
95781
96252
  case "graphic-gap-match": {
95782
- const responseValues2 = responsePairs(interaction.gapImages.map((choice) => choice.identifier), interaction.hotspots.map((hotspot) => hotspot.identifier));
96253
+ const responseValues3 = responsePairs(interaction.gapImages.map((choice) => choice.identifier), interaction.hotspots.map((hotspot) => hotspot.identifier));
95783
96254
  return {
95784
- responseValues: responseValues2,
95785
- maximumResponses: qtiAttributeMaximum(attributes2, "max-associations", 0, responseValues2.length)
96255
+ responseValues: responseValues3,
96256
+ maximumResponses: qtiAttributeMaximum(attributes2, "max-associations", 0, responseValues3.length)
95786
96257
  };
95787
96258
  }
95788
96259
  case "graphic-associate": {
@@ -95827,8 +96298,8 @@ function responseMappingMaximum(item, declaration, space) {
95827
96298
  function qtiItemScoringValidationMessage(item) {
95828
96299
  for (const declaration of item.responseDeclarations) {
95829
96300
  const space = interactionResponseSpace(item, declaration);
95830
- const { responseValues: responseValues2, maximumResponses } = space;
95831
- if (responseValues2 && declaration.correctValues.some((value) => !responseValues2.includes(value))) {
96301
+ const { responseValues: responseValues3, maximumResponses } = space;
96302
+ if (responseValues3 && declaration.correctValues.some((value) => !responseValues3.includes(value))) {
95832
96303
  return `Response ${declaration.identifier} declares a correct value its interaction cannot produce.`;
95833
96304
  }
95834
96305
  const interaction = item.interactions.find((candidate) => candidate.responseIdentifier === declaration.identifier);
@@ -95868,13 +96339,13 @@ function qtiItemScoringValidationMessage(item) {
95868
96339
  }
95869
96340
  function qtiItemMaxScore(item) {
95870
96341
  const maxScore = item.outcomeDeclarations.find((declaration) => declaration.identifier.toUpperCase() === "MAXSCORE");
95871
- const declaredMaxScore = Number(maxScore?.defaultValues[0]);
95872
- if (Number.isFinite(declaredMaxScore) && declaredMaxScore > 0) {
96342
+ const declaredMaxScore = qtiFloatValue(maxScore?.defaultValues[0]);
96343
+ if (declaredMaxScore !== undefined && declaredMaxScore > 0) {
95873
96344
  return declaredMaxScore;
95874
96345
  }
95875
96346
  const score = item.outcomeDeclarations.find((declaration) => declaration.identifier.toUpperCase() === "SCORE");
95876
- const normalMaximum = Number(score?.attributes["normal-maximum"]);
95877
- if (Number.isFinite(normalMaximum) && normalMaximum > 0) {
96347
+ const normalMaximum = qtiFloatValue(score?.attributes["normal-maximum"]);
96348
+ if (normalMaximum !== undefined && normalMaximum > 0) {
95878
96349
  return normalMaximum;
95879
96350
  }
95880
96351
  const inferredMaximum = item.responseDeclarations.reduce((total, declaration) => {
@@ -95902,7 +96373,7 @@ function scoresResponsesIndependently(item, scoredIdentifiers) {
95902
96373
  if (!processing) {
95903
96374
  return true;
95904
96375
  }
95905
- 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)))) {
96376
+ 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)) {
95906
96377
  return false;
95907
96378
  }
95908
96379
  const declaredIdentifiers = new Set(item.responseDeclarations.map((declaration) => declaration.identifier));
@@ -95990,6 +96461,40 @@ function supportedQtiQuestionInteractionType(question) {
95990
96461
  return null;
95991
96462
  }
95992
96463
  }
96464
+ function validNumericMatchValue2(value, baseType) {
96465
+ const syntax = baseType === "integer" ? /^[+-]?\d+$/ : /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i;
96466
+ if (!syntax.test(value.trim())) {
96467
+ return false;
96468
+ }
96469
+ const numeric3 = Number(value);
96470
+ return Number.isFinite(numeric3) && (baseType === "float" || Number.isSafeInteger(numeric3));
96471
+ }
96472
+ function sameScore2(left, right) {
96473
+ return Number.isFinite(left) && Number.isFinite(right) && Math.abs(left - right) <= Number.EPSILON * Math.max(1, Math.abs(left), Math.abs(right));
96474
+ }
96475
+ function scoringItemMatchesPlayableItem(scoringItem, item) {
96476
+ if (!sameScore2(scoringItem.maxScore, item.maxScore)) {
96477
+ return false;
96478
+ }
96479
+ if (scoringItem.strategy === "qti") {
96480
+ return true;
96481
+ }
96482
+ const playableResponses = new Set(item.interactions.map((interaction) => interaction.responseIdentifier));
96483
+ const scoringResponses = new Set(scoringItem.rules.map((rule) => rule.responseIdentifier));
96484
+ return playableResponses.size === item.interactions.length && scoringResponses.size === scoringItem.rules.length && playableResponses.size === scoringResponses.size && [...playableResponses].every((identifier) => scoringResponses.has(identifier));
96485
+ }
96486
+ function normalizedKey(key) {
96487
+ return key.replaceAll(/[^a-z0-9]/gi, "").toLowerCase();
96488
+ }
96489
+ function containsPrivatePlayableAssessmentData(value) {
96490
+ if (Array.isArray(value)) {
96491
+ return value.some(containsPrivatePlayableAssessmentData);
96492
+ }
96493
+ if (!isRecord(value)) {
96494
+ return false;
96495
+ }
96496
+ return Object.entries(value).some(([key, entry]) => PRIVATE_PLAYABLE_KEYS2.has(normalizedKey(key)) || containsPrivatePlayableAssessmentData(entry));
96497
+ }
95993
96498
  function childElements(node, localName2) {
95994
96499
  return node.children.filter((child) => child.kind === "element" && (!localName2 || child.localName === localName2));
95995
96500
  }
@@ -96031,6 +96536,22 @@ function numericCorrectValue(declaration) {
96031
96536
  }
96032
96537
  return parsedNumericValue(declaration.correctValues[0], declaration.baseType);
96033
96538
  }
96539
+ function rawCorrectValues(declaration) {
96540
+ const correctResponses = childElements(declaration, "qti-correct-response");
96541
+ const correctResponse = correctResponses.length === 1 ? correctResponses[0] : undefined;
96542
+ if (!correctResponse || !onlyWhitespaceText(correctResponse.children)) {
96543
+ return null;
96544
+ }
96545
+ const values = childElements(correctResponse);
96546
+ if (values.length === 0 || values.some((value) => value.localName !== "qti-value" || !onlyAttributes(value, []))) {
96547
+ return null;
96548
+ }
96549
+ const rawValues = values.map((value) => {
96550
+ const text3 = directText(value);
96551
+ return text3 === null ? null : decodeXmlEntities(text3).trim();
96552
+ });
96553
+ return rawValues.every((value) => value !== null) ? rawValues : null;
96554
+ }
96034
96555
  function declarationResult(raw, normalized) {
96035
96556
  const identifier = raw.attributes.identifier?.trim();
96036
96557
  if (!normalized || !identifier || normalized.identifier !== identifier) {
@@ -96047,16 +96568,23 @@ function declarationResult(raw, normalized) {
96047
96568
  if (baseType === "directedPair" && cardinality !== "multiple") {
96048
96569
  return { reason: "unsupported-cardinality" };
96049
96570
  }
96571
+ const correctValues = rawCorrectValues(raw);
96572
+ if (!correctValues) {
96573
+ return { reason: "invalid-correct-response" };
96574
+ }
96050
96575
  const declaration = {
96051
96576
  identifier,
96052
96577
  cardinality,
96053
96578
  baseType,
96054
- correctValues: [...normalized.correctValues]
96579
+ correctValues
96055
96580
  };
96056
- if (declaration.correctValues.length === 0 || declaration.correctValues.some((value) => value.trim() === "") || declaration.cardinality === "single" && declaration.correctValues.length !== 1 && declaration.baseType !== "string") {
96581
+ 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") {
96582
+ return { reason: "invalid-correct-response" };
96583
+ }
96584
+ if (declaration.baseType === "identifier" && declaration.correctValues.some((value) => !IDENTIFIER_VALUE2.test(value))) {
96057
96585
  return { reason: "invalid-correct-response" };
96058
96586
  }
96059
- if (declaration.baseType === "directedPair" && (declaration.correctValues.some((value) => !DIRECTED_PAIR_VALUE.test(value)) || new Set(declaration.correctValues).size !== declaration.correctValues.length)) {
96587
+ if (declaration.baseType === "directedPair" && declaration.correctValues.some((value) => !DIRECTED_PAIR_VALUE2.test(value))) {
96060
96588
  return { reason: "invalid-correct-response" };
96061
96589
  }
96062
96590
  if ((declaration.baseType === "integer" || declaration.baseType === "float") && declaration.correctValues.some((value) => parsedNumericValue(value, declaration.baseType) === null)) {
@@ -96072,16 +96600,14 @@ function scoringDeclarations(rawDeclarations, normalizedDeclarations) {
96072
96600
  const declarations = new Map;
96073
96601
  for (const [index2, rawDeclaration] of rawDeclarations.entries()) {
96074
96602
  const normalized = normalizedDeclarations[index2];
96075
- if (normalized?.correctValues.length) {
96076
- const result = declarationResult(rawDeclaration, normalized);
96077
- if ("reason" in result) {
96078
- return result;
96079
- }
96080
- if (declarations.has(result.declaration.identifier)) {
96081
- return { reason: "invalid-correct-response" };
96082
- }
96083
- declarations.set(result.declaration.identifier, result.declaration);
96603
+ const result = declarationResult(rawDeclaration, normalized);
96604
+ if ("reason" in result) {
96605
+ return result;
96606
+ }
96607
+ if (declarations.has(result.declaration.identifier)) {
96608
+ return { reason: "invalid-correct-response" };
96084
96609
  }
96610
+ declarations.set(result.declaration.identifier, result.declaration);
96085
96611
  }
96086
96612
  return declarations.size > 0 ? { declarations } : { reason: "no-scorable-response" };
96087
96613
  }
@@ -96091,15 +96617,12 @@ function declarationConstructReason(declarations) {
96091
96617
  }
96092
96618
  return declarations.some((declaration) => childElements(declaration, "qti-area-mapping").length > 0) ? "area-mapping" : null;
96093
96619
  }
96094
- function sameScore(left, right) {
96095
- return Math.abs(left - right) <= Number.EPSILON * Math.max(1, Math.abs(left), Math.abs(right));
96096
- }
96097
96620
  function platformResult(itemIdentifier, parsedMaxScore, rules) {
96098
96621
  const ruleMaximum = rules.reduce((sum, rule) => sum + rule.points, 0);
96099
96622
  if (!Number.isFinite(ruleMaximum) || ruleMaximum <= 0) {
96100
96623
  return fallback(itemIdentifier, parsedMaxScore, "no-scorable-response");
96101
96624
  }
96102
- if (!sameScore(parsedMaxScore, ruleMaximum)) {
96625
+ if (!sameScore2(parsedMaxScore, ruleMaximum)) {
96103
96626
  return fallback(itemIdentifier, parsedMaxScore, "inconsistent-max-score");
96104
96627
  }
96105
96628
  return {
@@ -96160,9 +96683,8 @@ function comparisonResult(comparison, declaration) {
96160
96683
  return null;
96161
96684
  }
96162
96685
  const roundingMode = comparison.attributes["rounding-mode"];
96163
- const figures = Number(comparison.attributes.figures);
96164
- const validFigures = Number.isInteger(figures) && (roundingMode === "decimalPlaces" ? figures >= 0 : figures > 0);
96165
- if (roundingMode !== "decimalPlaces" && roundingMode !== "significantFigures" || !validFigures) {
96686
+ const figures = parsedNumericValue(comparison.attributes.figures ?? "", "integer");
96687
+ if (roundingMode !== "decimalPlaces" && roundingMode !== "significantFigures" || figures === null || (roundingMode === "decimalPlaces" ? figures < 0 : figures <= 0)) {
96166
96688
  return null;
96167
96689
  }
96168
96690
  return {
@@ -96194,8 +96716,47 @@ function additivePoints(setOutcome) {
96194
96716
  return null;
96195
96717
  }
96196
96718
  const rawPoints = directText(pointValue);
96197
- const points = rawPoints === null ? Number.NaN : Number(rawPoints);
96198
- return Number.isFinite(points) && points > 0 ? points : null;
96719
+ const points = rawPoints === null ? null : parsedNumericValue(rawPoints, "float");
96720
+ return points !== null && points > 0 ? points : null;
96721
+ }
96722
+ function constantOutcomeValue(setOutcome, identifier, baseType) {
96723
+ if (!setOutcome || setOutcome.localName !== "qti-set-outcome-value" || !onlyAttributes(setOutcome, ["identifier"]) || setOutcome.attributes.identifier !== identifier || !onlyWhitespaceText(setOutcome.children)) {
96724
+ return null;
96725
+ }
96726
+ const values = childElements(setOutcome);
96727
+ const value = values.length === 1 ? values[0] : undefined;
96728
+ return value && value.localName === "qti-base-value" && onlyAttributes(value, ["base-type"]) && value.attributes["base-type"] === baseType ? directText(value) : null;
96729
+ }
96730
+ function feedbackOutcomeIsDeclared(item) {
96731
+ const feedbackOutcomes = childElements(item, "qti-outcome-declaration").filter((outcome) => outcome.attributes.identifier === "FEEDBACK");
96732
+ const feedback = feedbackOutcomes[0];
96733
+ 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);
96734
+ }
96735
+ function binaryBranchRule(condition, declaration) {
96736
+ if (!onlyAttributes(condition, []) || !onlyWhitespaceText(condition.children)) {
96737
+ return null;
96738
+ }
96739
+ const branches = childElements(condition);
96740
+ const responseIf = branches[0];
96741
+ const responseElse = branches[1];
96742
+ if (branches.length !== 2 || responseIf?.localName !== "qti-response-if" || responseElse?.localName !== "qti-response-else" || !onlyAttributes(responseIf, []) || !onlyAttributes(responseElse, []) || !onlyWhitespaceText(responseIf.children) || !onlyWhitespaceText(responseElse.children)) {
96743
+ return null;
96744
+ }
96745
+ const ifChildren = childElements(responseIf);
96746
+ const elseChildren = childElements(responseElse);
96747
+ const [comparison, ifFeedback, ifScore] = ifChildren;
96748
+ const [elseFeedback, elseScore] = elseChildren;
96749
+ const comparisonPayload = comparison ? comparisonResult(comparison, declaration) : null;
96750
+ 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") {
96751
+ return null;
96752
+ }
96753
+ return {
96754
+ responseIdentifier: declaration.identifier,
96755
+ cardinality: declaration.cardinality,
96756
+ baseType: declaration.baseType,
96757
+ points: 1,
96758
+ comparison: comparisonPayload
96759
+ };
96199
96760
  }
96200
96761
  function additiveRule(condition, declarations) {
96201
96762
  const responseIdentifiers = new Set(declarations.keys());
@@ -96273,7 +96834,7 @@ function scoreDefaultIsZero(item) {
96273
96834
  }
96274
96835
  const values = childElements(defaults[0], "qti-value");
96275
96836
  const defaultText = values.length === 1 ? directText(values[0]) : null;
96276
- return defaultText !== null && defaultText !== "" && Number(defaultText) === 0;
96837
+ return defaultText !== null && parsedNumericValue(defaultText, "float") === 0;
96277
96838
  }
96278
96839
  function compileResponseProcessing(item, itemIdentifier, maxScore, declarations) {
96279
96840
  const processingElements = childElements(item, "qti-response-processing");
@@ -96296,7 +96857,17 @@ function compileResponseProcessing(item, itemIdentifier, maxScore, declarations)
96296
96857
  const response = declarations.get("RESPONSE");
96297
96858
  return response && declarations.size === 1 ? platformResult(itemIdentifier, maxScore, [matchRule(response, 1)]) : fallback(itemIdentifier, maxScore, "unsupported-response-processing");
96298
96859
  }
96299
- if (!onlyAttributes(processing, []) || !onlyWhitespaceText(processing.children) || processingRules.length === 0 || processingRules.some((rule) => rule.localName !== "qti-response-condition") || !scoreDefaultIsZero(item)) {
96860
+ if (!onlyAttributes(processing, []) || !onlyWhitespaceText(processing.children) || processingRules.length === 0 || processingRules.some((rule) => rule.localName !== "qti-response-condition")) {
96861
+ return fallback(itemIdentifier, maxScore, "unsupported-response-processing");
96862
+ }
96863
+ if (processingRules.length === 1 && declarations.size === 1 && scoreDefaultIsZero(item) && feedbackOutcomeIsDeclared(item)) {
96864
+ const declaration = [...declarations.values()][0];
96865
+ const rule = binaryBranchRule(processingRules[0], declaration);
96866
+ if (rule) {
96867
+ return platformResult(itemIdentifier, maxScore, [rule]);
96868
+ }
96869
+ }
96870
+ if (!scoreDefaultIsZero(item)) {
96300
96871
  return fallback(itemIdentifier, maxScore, "unsupported-response-processing");
96301
96872
  }
96302
96873
  const compiled = additiveRules(processingRules, declarations);
@@ -96349,7 +96920,7 @@ function compileQtiScoringArtifact(questions) {
96349
96920
  });
96350
96921
  return { items };
96351
96922
  }
96352
- function isRecord22(value) {
96923
+ function isRecord32(value) {
96353
96924
  return Boolean(value) && typeof value === "object" && !Array.isArray(value);
96354
96925
  }
96355
96926
  function stringField2(value) {
@@ -96433,13 +97004,13 @@ function dedupeStandards2(standards) {
96433
97004
  return [...deduped.values()];
96434
97005
  }
96435
97006
  function qtiAlignmentStandardsFromMetadata2(metadata2) {
96436
- if (!isRecord22(metadata2)) {
97007
+ if (!isRecord32(metadata2)) {
96437
97008
  return [];
96438
97009
  }
96439
97010
  const standards = [];
96440
97011
  const alignmentGroups = Array.isArray(metadata2.alignment) ? metadata2.alignment : [];
96441
97012
  for (const group of alignmentGroups) {
96442
- if (isRecord22(group)) {
97013
+ if (isRecord32(group)) {
96443
97014
  const source = stringField2(group.curriculum) || "QTI";
96444
97015
  const domains2 = Array.isArray(group.domains) ? group.domains : [];
96445
97016
  if (!domains2.length) {
@@ -96457,11 +97028,11 @@ function qtiAlignmentStandardsFromMetadata2(metadata2) {
96457
97028
  }
96458
97029
  } else {
96459
97030
  for (const domain3 of domains2) {
96460
- if (isRecord22(domain3)) {
97031
+ if (isRecord32(domain3)) {
96461
97032
  const domainName = stringField2(domain3.name) || undefined;
96462
97033
  const domainStandards = Array.isArray(domain3.standards) ? domain3.standards : [];
96463
97034
  for (const standard of domainStandards) {
96464
- if (isRecord22(standard)) {
97035
+ if (isRecord32(standard)) {
96465
97036
  const identifier = stringField2(standard.identifier).trim();
96466
97037
  const id = stringField2(standard.id).trim() || identifier;
96467
97038
  if (id) {
@@ -96484,13 +97055,13 @@ function qtiAlignmentStandardsFromMetadata2(metadata2) {
96484
97055
  return dedupeStandards2(standards);
96485
97056
  }
96486
97057
  function qtiStandardsFromMetadata(metadata2) {
96487
- if (!isRecord22(metadata2)) {
97058
+ if (!isRecord32(metadata2)) {
96488
97059
  return [];
96489
97060
  }
96490
97061
  const standards = [];
96491
97062
  const objectiveSets = Array.isArray(metadata2.learningObjectiveSet) ? metadata2.learningObjectiveSet : [];
96492
97063
  for (const set3 of objectiveSets) {
96493
- if (isRecord22(set3)) {
97064
+ if (isRecord32(set3)) {
96494
97065
  const source = stringField2(set3.source) || "QTI";
96495
97066
  const ids = Array.isArray(set3.learningObjectiveIds) ? set3.learningObjectiveIds : [];
96496
97067
  for (const id of ids) {
@@ -96504,7 +97075,7 @@ function qtiStandardsFromMetadata(metadata2) {
96504
97075
  standards.push(...qtiAlignmentStandardsFromMetadata2(metadata2));
96505
97076
  return dedupeStandards2(standards);
96506
97077
  }
96507
- var EVENT_HANDLER_ATTRIBUTE, SCRIPT_SCHEMES2, DATA_RASTER_IMAGE_PATTERN2, QTI_MATHML_ALLOWED_TAGS2, metadataSymbol2, parser2, SUPPORTED_QTI_INTERACTION_TYPES2, URL_ATTRIBUTES3, IMAGE_URL_ATTRIBUTES3, INTERACTION_TYPES2, XML_NAMED_ENTITIES, EMPHASIS_TAGS2, STRONG_TAGS2, CONTENT_SKIPPED_TAGS2, BLOCK_CONTENT_KINDS2, GAP_MATCH_TOKEN_TAGS2, STAGE_GRAPHIC_EXCLUDED_CONTAINERS2, BLANK_SENTINEL2 = "￿", MARKED_BLANK_PATTERN2, MARKED_BLANK_GLOBAL_PATTERN2, REGION_SHAPE_COORDS, QTI_NAMESPACE = "http://www.imsglobal.org/xsd/imsqtiasi_v3p0", QTI_AUTHORING_BLANK_MARKER = "___", MATCH_CORRECT_TEMPLATE2 = "http://www.imsglobal.org/question/qti_v3p0/rptemplates/match_correct", MAP_RESPONSE_TEMPLATE2 = "https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/map_response", MAP_RESPONSE_TEMPLATES2, RESPONSE_PROCESSING_TEMPLATES2, GLOBAL_BODY_ATTRIBUTES, BODY_ELEMENT_ATTRIBUTES, MATHML_ATTRIBUTES, VOID_BODY_ELEMENTS2, URL_ATTRIBUTES22, IMAGE_URL_ATTRIBUTES22, INTERACTION_ATTRIBUTES, CHOICE_ATTRIBUTES, CHOICE_ELEMENT_ATTRIBUTES, VOID_CHOICE_ELEMENTS2, NUMERIC_COMPARISON_ATTRIBUTES2, RESPONSE_CARDINALITIES2, RESPONSE_BASE_TYPES2, BLANK_SENTINEL22 = "￿", PLAYABLE_POINT_VALUE, POINT_INTERACTION_TYPES2, INDEPENDENT_PROCESSING_TAGS2, SUPPORTED_BASE_TYPES2, SUPPORTED_CARDINALITIES2, MATCH_CORRECT_TEMPLATES2, DIRECTED_PAIR_VALUE, QtiScoringArtifactValidationError, COMMON_CORE_MATH_FRAMEWORK_ALIASES2, COMMON_CORE_ELA_FRAMEWORK_ALIASES2, COMMON_CORE_FRAMEWORK_ALIASES2;
97078
+ var EVENT_HANDLER_ATTRIBUTE, SCRIPT_SCHEMES2, DATA_RASTER_IMAGE_PATTERN2, QTI_MATHML_ALLOWED_TAGS2, metadataSymbol2, parser2, SUPPORTED_QTI_INTERACTION_TYPES2, URL_ATTRIBUTES3, IMAGE_URL_ATTRIBUTES3, INTERACTION_TYPES2, XML_NAMED_ENTITIES, EMPHASIS_TAGS2, STRONG_TAGS2, CONTENT_SKIPPED_TAGS2, BLOCK_CONTENT_KINDS2, GAP_MATCH_TOKEN_TAGS2, STAGE_GRAPHIC_EXCLUDED_CONTAINERS2, BLANK_SENTINEL2 = "￿", MARKED_BLANK_PATTERN2, MARKED_BLANK_GLOBAL_PATTERN2, REGION_SHAPE_COORDS, QTI_NAMESPACE = "http://www.imsglobal.org/xsd/imsqtiasi_v3p0", QTI_AUTHORING_BLANK_MARKER = "___", MATCH_CORRECT_TEMPLATE2 = "http://www.imsglobal.org/question/qti_v3p0/rptemplates/match_correct", MAP_RESPONSE_TEMPLATE2 = "https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/map_response", MAP_RESPONSE_TEMPLATES2, RESPONSE_PROCESSING_TEMPLATES2, GLOBAL_BODY_ATTRIBUTES, BODY_ELEMENT_ATTRIBUTES, MATHML_ATTRIBUTES, VOID_BODY_ELEMENTS2, URL_ATTRIBUTES22, IMAGE_URL_ATTRIBUTES22, INTERACTION_ATTRIBUTES, CHOICE_ATTRIBUTES, CHOICE_ELEMENT_ATTRIBUTES, VOID_CHOICE_ELEMENTS2, NUMERIC_COMPARISON_ATTRIBUTES2, RESPONSE_CARDINALITIES2, RESPONSE_BASE_TYPES2, BLANK_SENTINEL22 = "￿", QTI_FLOAT_VALUE, PLAYABLE_POINT_VALUE, POINT_INTERACTION_TYPES2, INDEPENDENT_PROCESSING_TAGS2, PRIVATE_PLAYABLE_KEYS2, trimmedNonEmptyString2, finiteNumber22, positiveFiniteNumber2, IDENTIFIER_VALUE2, DIRECTED_PAIR_VALUE2, scoringMatchComparisonSchema2, scoringNumericEqualComparisonSchema2, scoringNumericEqualRoundedComparisonSchema2, scoringRuleSchema2, scoringRulesSchema2, scoringItemSchema2, assessmentScoringArtifactItemsSchema2, assessmentScoringArtifactSchema2, SUPPORTED_BASE_TYPES2, SUPPORTED_CARDINALITIES2, MATCH_CORRECT_TEMPLATES2, QtiScoringArtifactValidationError, COMMON_CORE_MATH_FRAMEWORK_ALIASES2, COMMON_CORE_ELA_FRAMEWORK_ALIASES2, COMMON_CORE_FRAMEWORK_ALIASES2;
96508
97079
  var init_qti = __esm(() => {
96509
97080
  init_timeback3();
96510
97081
  init_timeback3();
@@ -96512,6 +97083,7 @@ var init_qti = __esm(() => {
96512
97083
  init_timeback3();
96513
97084
  init_timeback3();
96514
97085
  init_timeback3();
97086
+ init_esm();
96515
97087
  EVENT_HANDLER_ATTRIBUTE = /^on/i;
96516
97088
  SCRIPT_SCHEMES2 = {
96517
97089
  javascript: ["java", "script:"].join(""),
@@ -96736,6 +97308,7 @@ var init_qti = __esm(() => {
96736
97308
  "string",
96737
97309
  "uri"
96738
97310
  ]);
97311
+ QTI_FLOAT_VALUE = /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i;
96739
97312
  PLAYABLE_POINT_VALUE = /^-?\d+(?:\.\d+)? -?\d+(?:\.\d+)?$/;
96740
97313
  POINT_INTERACTION_TYPES2 = new Set([
96741
97314
  "select-point",
@@ -96754,6 +97327,93 @@ var init_qti = __esm(() => {
96754
97327
  "qti-sum",
96755
97328
  "qti-base-value"
96756
97329
  ]);
97330
+ PRIVATE_PLAYABLE_KEYS2 = new Set([
97331
+ "answer",
97332
+ "answerkey",
97333
+ "correctanswer",
97334
+ "correctresponse",
97335
+ "correctresponses",
97336
+ "correct",
97337
+ "correctvalue",
97338
+ "correctvalues",
97339
+ "iscorrect",
97340
+ "responseprocessing",
97341
+ "rule",
97342
+ "rules",
97343
+ "scoring",
97344
+ "scoringrules",
97345
+ "solution",
97346
+ "expected",
97347
+ "expectedanswer",
97348
+ "expectedvalue",
97349
+ "expectedvalues"
97350
+ ]);
97351
+ trimmedNonEmptyString2 = exports_external.string().min(1).refine((value) => value === value.trim());
97352
+ finiteNumber22 = exports_external.number().finite();
97353
+ positiveFiniteNumber2 = finiteNumber22.positive();
97354
+ IDENTIFIER_VALUE2 = /^[A-Za-z_][\w.-]*$/;
97355
+ DIRECTED_PAIR_VALUE2 = /^[A-Za-z_][\w.-]* [A-Za-z_][\w.-]*$/;
97356
+ scoringMatchComparisonSchema2 = exports_external.object({
97357
+ kind: exports_external.literal("match"),
97358
+ correctValues: exports_external.array(trimmedNonEmptyString2).min(1).refine((values) => new Set(values).size === values.length)
97359
+ }).strict();
97360
+ scoringNumericEqualComparisonSchema2 = exports_external.object({ kind: exports_external.literal("numeric-equal"), correctValue: finiteNumber22 }).strict();
97361
+ scoringNumericEqualRoundedComparisonSchema2 = exports_external.object({
97362
+ kind: exports_external.literal("numeric-equal-rounded"),
97363
+ correctValue: finiteNumber22,
97364
+ roundingMode: exports_external.enum(["decimalPlaces", "significantFigures"]),
97365
+ figures: exports_external.number().int().safe().nonnegative()
97366
+ }).strict();
97367
+ scoringRuleSchema2 = exports_external.object({
97368
+ responseIdentifier: trimmedNonEmptyString2,
97369
+ cardinality: exports_external.enum(["single", "multiple", "ordered"]),
97370
+ baseType: exports_external.enum(["string", "identifier", "integer", "float", "directedPair"]),
97371
+ points: positiveFiniteNumber2,
97372
+ comparison: exports_external.discriminatedUnion("kind", [
97373
+ scoringMatchComparisonSchema2,
97374
+ scoringNumericEqualComparisonSchema2,
97375
+ scoringNumericEqualRoundedComparisonSchema2
97376
+ ])
97377
+ }).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) => {
97378
+ if (rule.comparison.kind !== "match") {
97379
+ return rule.baseType !== "integer" || Number.isSafeInteger(rule.comparison.correctValue);
97380
+ }
97381
+ if (rule.baseType === "integer" || rule.baseType === "float") {
97382
+ return rule.comparison.correctValues.every((value) => validNumericMatchValue2(value, rule.baseType));
97383
+ }
97384
+ if (rule.baseType === "identifier") {
97385
+ return rule.comparison.correctValues.every((value) => IDENTIFIER_VALUE2.test(value));
97386
+ }
97387
+ return rule.baseType !== "directedPair" || rule.comparison.correctValues.every((value) => DIRECTED_PAIR_VALUE2.test(value));
97388
+ });
97389
+ scoringRulesSchema2 = exports_external.array(scoringRuleSchema2).min(1).refine((rules) => new Set(rules.map((rule) => rule.responseIdentifier)).size === rules.length);
97390
+ scoringItemSchema2 = exports_external.discriminatedUnion("strategy", [
97391
+ exports_external.object({
97392
+ strategy: exports_external.literal("qti"),
97393
+ itemIdentifier: trimmedNonEmptyString2,
97394
+ maxScore: positiveFiniteNumber2,
97395
+ reason: exports_external.enum([
97396
+ "response-mapping",
97397
+ "area-mapping",
97398
+ "unsupported-base-type",
97399
+ "unsupported-cardinality",
97400
+ "invalid-correct-response",
97401
+ "no-scorable-response",
97402
+ "unsupported-processing-template",
97403
+ "unsupported-response-processing",
97404
+ "cross-response-processing",
97405
+ "inconsistent-max-score"
97406
+ ])
97407
+ }).strict(),
97408
+ exports_external.object({
97409
+ strategy: exports_external.literal("platform"),
97410
+ itemIdentifier: trimmedNonEmptyString2,
97411
+ maxScore: positiveFiniteNumber2,
97412
+ rules: scoringRulesSchema2
97413
+ }).strict()
97414
+ ]).refine((item) => item.strategy === "qti" || sameScore2(item.rules.reduce((total, rule) => total + rule.points, 0), item.maxScore));
97415
+ assessmentScoringArtifactItemsSchema2 = exports_external.array(scoringItemSchema2).refine((items) => new Set(items.map((item) => item.itemIdentifier)).size === items.length);
97416
+ assessmentScoringArtifactSchema2 = exports_external.object({ items: assessmentScoringArtifactItemsSchema2 }).strict();
96757
97417
  SUPPORTED_BASE_TYPES2 = new Set([
96758
97418
  "string",
96759
97419
  "identifier",
@@ -96771,7 +97431,6 @@ var init_qti = __esm(() => {
96771
97431
  "https://www.imsglobal.org/question/qti_v3p0/rptemplates/match_correct",
96772
97432
  "https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/match_correct"
96773
97433
  ].flatMap((template) => [template, `${template}.xml`]));
96774
- DIRECTED_PAIR_VALUE = /^[A-Za-z_][\w.-]* [A-Za-z_][\w.-]*$/;
96775
97434
  QtiScoringArtifactValidationError = class QtiScoringArtifactValidationError extends Error {
96776
97435
  constructor(message) {
96777
97436
  super(message);
@@ -99139,50 +99798,38 @@ var init_timeback_admin_service = __esm(async () => {
99139
99798
  });
99140
99799
 
99141
99800
  // ../api-core/src/utils/assessment-artifact-runtime.util.ts
99142
- function normalizedKey(key) {
99143
- return key.replaceAll(/[^a-z0-9]/gi, "").toLowerCase();
99144
- }
99145
- function containsPrivatePlayableKey(value) {
99146
- if (Array.isArray(value)) {
99147
- return value.some(containsPrivatePlayableKey);
99148
- }
99149
- if (!isRecord(value)) {
99150
- return false;
99151
- }
99152
- return Object.entries(value).some(([key, entry]) => PRIVATE_PLAYABLE_KEYS.has(normalizedKey(key)) || containsPrivatePlayableKey(entry));
99153
- }
99154
99801
  function playableSchema(identity) {
99155
99802
  return exports_external.object({
99156
99803
  contractVersion: exports_external.literal(PLAYCADEMY_ASSESSMENT_CONTRACT_VERSION),
99157
99804
  identifier: exports_external.literal(identity.qtiTestIdentifier),
99158
99805
  contentRevision: exports_external.literal(identity.kind === "assessment" ? identity.contentRevision : identity.sourceContentRevision),
99159
- title: trimmedNonEmptyString,
99806
+ title: trimmedNonEmptyString3,
99160
99807
  items: exports_external.array(playableItemSchema)
99161
99808
  }).strict();
99162
99809
  }
99163
99810
  function scoringSchema(identity) {
99164
99811
  if (identity.kind === "assessment") {
99165
99812
  return exports_external.object({
99166
- items: uniqueScoringItemsSchema,
99813
+ items: assessmentScoringArtifactItemsSchema2,
99167
99814
  reviewBank: exports_external.undefined(),
99168
99815
  diagnostic: diagnosticContextSchema.optional()
99169
- });
99816
+ }).strict();
99170
99817
  }
99171
99818
  return exports_external.object({
99172
- items: uniqueScoringItemsSchema,
99819
+ items: assessmentScoringArtifactItemsSchema2,
99173
99820
  reviewBank: exports_external.object({
99174
99821
  sourceContentRevision: exports_external.literal(identity.sourceContentRevision),
99175
99822
  bankRevision: exports_external.literal(identity.bankRevision),
99176
99823
  items: exports_external.array(exports_external.object({
99177
- itemIdentifier: trimmedNonEmptyString,
99824
+ itemIdentifier: trimmedNonEmptyString3,
99178
99825
  standards: exports_external.array(exports_external.unknown())
99179
- }))
99180
- }),
99826
+ }).strict())
99827
+ }).strict(),
99181
99828
  diagnostic: exports_external.undefined()
99182
- }).refine((payload) => payload.reviewBank.items.length === payload.items.length && payload.reviewBank.items.every((item, index2) => item.itemIdentifier === payload.items[index2]?.itemIdentifier));
99829
+ }).strict().refine((payload) => payload.reviewBank.items.length === payload.items.length && payload.reviewBank.items.every((item, index2) => item.itemIdentifier === payload.items[index2]?.itemIdentifier));
99183
99830
  }
99184
99831
  function assertPlayableAssessmentArtifact(payload, identity) {
99185
- if (!playableSchema(identity).safeParse(payload).success || containsPrivatePlayableKey(payload)) {
99832
+ if (!playableSchema(identity).safeParse(payload).success || containsPrivatePlayableAssessmentData(payload)) {
99186
99833
  throw new ValidationError("Playable assessment artifact payload is invalid or private");
99187
99834
  }
99188
99835
  }
@@ -99192,8 +99839,11 @@ function assertScoringAssessmentArtifact(payload, identity) {
99192
99839
  }
99193
99840
  }
99194
99841
  function assertScoringArtifactMatchesPlayable(assessment, scoring, identity) {
99195
- const scoringByIdentifier = new Map(scoring.items.map((item) => [item.itemIdentifier, item.maxScore]));
99196
- const playableMatches = assessment.items.every((item) => scoringByIdentifier.get(item.identifier) === item.maxScore);
99842
+ const scoringByIdentifier = new Map(scoring.items.map((item) => [item.itemIdentifier, item]));
99843
+ const playableMatches = assessment.items.every((item) => {
99844
+ const scoringItem = scoringByIdentifier.get(item.identifier);
99845
+ return scoringItem !== undefined && scoringItemMatchesPlayableItem(scoringItem, item);
99846
+ });
99197
99847
  const hasExactManagedMembership = identity.kind === "review-bank" || scoring.items.length === assessment.items.length;
99198
99848
  if (!playableMatches || !hasExactManagedMembership) {
99199
99849
  throw new ValidationError("Scoring artifact does not match the playable assessment");
@@ -99231,73 +99881,54 @@ function pinnedReviewBankFromAttempt(metadata2, bankIdentifier) {
99231
99881
  }))
99232
99882
  };
99233
99883
  }
99234
- var PRIVATE_PLAYABLE_KEYS, trimmedNonEmptyString, finiteNumber2, nonnegativeInteger, playableContentNodesSchema, playableChoiceSchema, playableMatchChoiceSchema, playableGraphicSchema, playableHotspotSchema, playableGapTextSchema, playableGapImageSchema, playableInteractionBase, playableInteractionSchema, playableItemSchema, scoringRuleSchema, scoringItemSchema, uniqueScoringItemsSchema, diagnosticContextSchema;
99884
+ var trimmedNonEmptyString3, finiteNumber3, positiveFiniteNumber3, nonnegativeInteger, playableContentNodesSchema, playableChoiceSchema, playableMatchChoiceSchema, playableGraphicSchema, playableHotspotSchema, playableGapTextSchema, playableGapImageSchema, playableInteractionBase, playableInteractionSchema, playableItemSchema, diagnosticContextSchema;
99235
99885
  var init_assessment_artifact_runtime_util = __esm(() => {
99236
99886
  init_esm();
99237
99887
  init_src();
99888
+ init_qti();
99238
99889
  init_errors2();
99239
- PRIVATE_PLAYABLE_KEYS = new Set([
99240
- "answer",
99241
- "answerkey",
99242
- "correctanswer",
99243
- "correctresponse",
99244
- "correctresponses",
99245
- "correct",
99246
- "correctvalue",
99247
- "correctvalues",
99248
- "iscorrect",
99249
- "responseprocessing",
99250
- "rule",
99251
- "rules",
99252
- "scoring",
99253
- "scoringrules",
99254
- "solution",
99255
- "expected",
99256
- "expectedanswer",
99257
- "expectedvalue",
99258
- "expectedvalues"
99259
- ]);
99260
- trimmedNonEmptyString = exports_external.string().min(1).refine((value) => value === value.trim());
99261
- finiteNumber2 = exports_external.number().finite();
99890
+ trimmedNonEmptyString3 = exports_external.string().min(1).refine((value) => value === value.trim());
99891
+ finiteNumber3 = exports_external.number().finite();
99892
+ positiveFiniteNumber3 = finiteNumber3.positive();
99262
99893
  nonnegativeInteger = exports_external.number().int().nonnegative();
99263
99894
  playableContentNodesSchema = exports_external.array(exports_external.unknown());
99264
99895
  playableChoiceSchema = exports_external.object({
99265
- identifier: trimmedNonEmptyString,
99896
+ identifier: trimmedNonEmptyString3,
99266
99897
  content: exports_external.string(),
99267
99898
  contentNodes: playableContentNodesSchema.optional()
99268
99899
  }).strict();
99269
99900
  playableMatchChoiceSchema = exports_external.object({
99270
- identifier: trimmedNonEmptyString,
99901
+ identifier: trimmedNonEmptyString3,
99271
99902
  content: exports_external.string(),
99272
99903
  contentNodes: playableContentNodesSchema.optional(),
99273
99904
  matchMax: nonnegativeInteger
99274
99905
  }).strict();
99275
99906
  playableGraphicSchema = exports_external.object({
99276
- src: trimmedNonEmptyString,
99277
- width: finiteNumber2.optional(),
99278
- height: finiteNumber2.optional(),
99907
+ src: trimmedNonEmptyString3,
99908
+ width: finiteNumber3.optional(),
99909
+ height: finiteNumber3.optional(),
99279
99910
  description: exports_external.string().optional()
99280
99911
  }).strict();
99281
99912
  playableHotspotSchema = exports_external.object({
99282
- identifier: trimmedNonEmptyString,
99913
+ identifier: trimmedNonEmptyString3,
99283
99914
  shape: exports_external.enum(["circle", "ellipse", "rect", "poly", "default"]),
99284
- coords: exports_external.array(finiteNumber2),
99915
+ coords: exports_external.array(finiteNumber3),
99285
99916
  matchMax: nonnegativeInteger.optional(),
99286
99917
  description: exports_external.string().optional()
99287
99918
  }).strict();
99288
99919
  playableGapTextSchema = exports_external.object({
99289
- identifier: trimmedNonEmptyString,
99920
+ identifier: trimmedNonEmptyString3,
99290
99921
  content: exports_external.string(),
99291
99922
  matchMax: nonnegativeInteger
99292
99923
  }).strict();
99293
99924
  playableGapImageSchema = exports_external.object({
99294
- identifier: trimmedNonEmptyString,
99925
+ identifier: trimmedNonEmptyString3,
99295
99926
  image: playableGraphicSchema.optional(),
99296
99927
  matchMax: nonnegativeInteger,
99297
99928
  description: exports_external.string().optional()
99298
99929
  }).strict();
99299
99930
  playableInteractionBase = {
99300
- responseIdentifier: trimmedNonEmptyString,
99931
+ responseIdentifier: trimmedNonEmptyString3,
99301
99932
  prompt: exports_external.string().optional()
99302
99933
  };
99303
99934
  playableInteractionSchema = exports_external.discriminatedUnion("type", [
@@ -99337,7 +99968,7 @@ var init_assessment_artifact_runtime_util = __esm(() => {
99337
99968
  ...playableInteractionBase,
99338
99969
  type: exports_external.literal("hottext"),
99339
99970
  segments: exports_external.array(exports_external.object({
99340
- identifier: trimmedNonEmptyString.optional(),
99971
+ identifier: trimmedNonEmptyString3.optional(),
99341
99972
  content: exports_external.string(),
99342
99973
  selectable: exports_external.boolean()
99343
99974
  }).strict()),
@@ -99357,7 +99988,7 @@ var init_assessment_artifact_runtime_util = __esm(() => {
99357
99988
  content: exports_external.string(),
99358
99989
  contentNodes: playableContentNodesSchema,
99359
99990
  gapTexts: exports_external.array(playableGapTextSchema),
99360
- gaps: exports_external.array(trimmedNonEmptyString),
99991
+ gaps: exports_external.array(trimmedNonEmptyString3),
99361
99992
  maxAssociations: nonnegativeInteger
99362
99993
  }).strict(),
99363
99994
  exports_external.object({
@@ -99391,59 +100022,17 @@ var init_assessment_artifact_runtime_util = __esm(() => {
99391
100022
  }).strict()
99392
100023
  ]);
99393
100024
  playableItemSchema = exports_external.object({
99394
- identifier: trimmedNonEmptyString,
100025
+ identifier: trimmedNonEmptyString3,
99395
100026
  title: exports_external.string(),
99396
100027
  prompt: exports_external.string(),
99397
100028
  promptContent: playableContentNodesSchema.optional(),
99398
- maxScore: finiteNumber2,
100029
+ maxScore: positiveFiniteNumber3,
99399
100030
  interactions: exports_external.array(playableInteractionSchema)
99400
100031
  }).strict();
99401
- scoringRuleSchema = exports_external.object({
99402
- responseIdentifier: trimmedNonEmptyString,
99403
- cardinality: exports_external.enum(["single", "multiple", "ordered"]),
99404
- baseType: exports_external.enum(["string", "identifier", "integer", "float", "directedPair"]),
99405
- points: finiteNumber2,
99406
- comparison: exports_external.discriminatedUnion("kind", [
99407
- exports_external.object({ kind: exports_external.literal("match"), correctValues: exports_external.array(exports_external.string()) }),
99408
- exports_external.object({ kind: exports_external.literal("numeric-equal"), correctValue: finiteNumber2 }),
99409
- exports_external.object({
99410
- kind: exports_external.literal("numeric-equal-rounded"),
99411
- correctValue: finiteNumber2,
99412
- roundingMode: exports_external.enum(["decimalPlaces", "significantFigures"]),
99413
- figures: exports_external.number().int().nonnegative()
99414
- })
99415
- ])
99416
- });
99417
- scoringItemSchema = exports_external.discriminatedUnion("strategy", [
99418
- exports_external.object({
99419
- strategy: exports_external.literal("qti"),
99420
- itemIdentifier: trimmedNonEmptyString,
99421
- maxScore: finiteNumber2,
99422
- reason: exports_external.enum([
99423
- "response-mapping",
99424
- "area-mapping",
99425
- "unsupported-base-type",
99426
- "unsupported-cardinality",
99427
- "invalid-correct-response",
99428
- "no-scorable-response",
99429
- "unsupported-processing-template",
99430
- "unsupported-response-processing",
99431
- "cross-response-processing",
99432
- "inconsistent-max-score"
99433
- ])
99434
- }),
99435
- exports_external.object({
99436
- strategy: exports_external.literal("platform"),
99437
- itemIdentifier: trimmedNonEmptyString,
99438
- maxScore: finiteNumber2,
99439
- rules: exports_external.array(scoringRuleSchema)
99440
- })
99441
- ]);
99442
- uniqueScoringItemsSchema = exports_external.array(scoringItemSchema).refine((items) => new Set(items.map((item) => item.itemIdentifier)).size === items.length);
99443
100032
  diagnosticContextSchema = exports_external.object({
99444
- routingRevision: trimmedNonEmptyString,
100033
+ routingRevision: trimmedNonEmptyString3,
99445
100034
  routingManifest: exports_external.record(exports_external.unknown())
99446
- });
100035
+ }).strict();
99447
100036
  });
99448
100037
 
99449
100038
  // ../api-core/src/utils/assessment-artifact-loader.util.ts
@@ -100365,16 +100954,8 @@ function prepareDiagnosticAssessmentResponses(assessment, current, input) {
100365
100954
  const update2 = { [input.itemIdentifier]: input.responses };
100366
100955
  validateAssessmentResponseUpdate(assessment, update2);
100367
100956
  const responses = applyAssessmentResponseUpdate(current, update2);
100368
- const item = assessment.items.find((candidate) => candidate.identifier === input.itemIdentifier);
100369
100957
  const itemResponses = responses[input.itemIdentifier];
100370
- const missingResponse = item?.interactions.find((interaction) => itemResponses?.[interaction.responseIdentifier] === undefined);
100371
- if (!item || !itemResponses || missingResponse) {
100372
- throw new AssessmentRuntimeError("INVALID_RESPONSE", `Diagnostic item ${input.itemIdentifier} requires a response for every interaction.`, {
100373
- itemIdentifier: input.itemIdentifier,
100374
- ...missingResponse ? { responseIdentifier: missingResponse.responseIdentifier } : {}
100375
- });
100376
- }
100377
- validateAssessmentResponses(assessment, { [input.itemIdentifier]: itemResponses });
100958
+ validateAssessmentDraftResponses(assessment, itemResponses ? { [input.itemIdentifier]: itemResponses } : {});
100378
100959
  return responses;
100379
100960
  }
100380
100961
  function validateAssessmentResponseUpdate(assessment, update2) {
@@ -101838,6 +102419,164 @@ var init_timeback_assessment_artifacts_util = __esm(() => {
101838
102419
  init_timeback_review_mapping_util();
101839
102420
  });
101840
102421
 
102422
+ // ../api-core/src/utils/timeback-assessment-child-result.util.ts
102423
+ async function buildSubmissionChildResultCommand(input) {
102424
+ const { submission, context: context2 } = input;
102425
+ const { isCorrect } = submission;
102426
+ const common2 = {
102427
+ attempt: input.attempt,
102428
+ administration: {
102429
+ submissionId: submission.submissionId,
102430
+ responseVersion: submission.responseVersion,
102431
+ itemIdentifier: submission.itemIdentifier
102432
+ },
102433
+ responses: input.responses ?? {},
102434
+ score: submission.score,
102435
+ grading: input.grading
102436
+ };
102437
+ if (context2.kind === "review") {
102438
+ return buildAssessmentChildResultCommand({
102439
+ ...common2,
102440
+ ...typeof isCorrect === "boolean" ? { isCorrect } : {},
102441
+ context: context2
102442
+ }, submission.submittedAt);
102443
+ }
102444
+ if (typeof isCorrect !== "boolean") {
102445
+ throw new Error("Diagnostic child results require determinate correctness");
102446
+ }
102447
+ return buildAssessmentChildResultCommand({ ...common2, isCorrect, context: context2 }, submission.submittedAt);
102448
+ }
102449
+ function reviewDefinitionWithoutStandards(definition) {
102450
+ return Object.fromEntries(Object.entries(definition).filter(([field]) => field !== "standards"));
102451
+ }
102452
+ function assessmentChildLineItemMetadataMatches(key, stored, expected) {
102453
+ if (key !== "playcademyReviewQuestionDefinition" || typeof stored !== "object" || stored === null || typeof expected !== "object" || expected === null) {
102454
+ return canonicalJson2(stored) === canonicalJson2(expected);
102455
+ }
102456
+ const storedDefinition = stored;
102457
+ const expectedDefinition = expected;
102458
+ const storedStandards = storedDefinition.standards;
102459
+ const expectedStandards = expectedDefinition.standards;
102460
+ if (!Array.isArray(storedStandards) || !Array.isArray(expectedStandards)) {
102461
+ return false;
102462
+ }
102463
+ const sameDefinition = canonicalJson2(reviewDefinitionWithoutStandards(storedDefinition)) === canonicalJson2(reviewDefinitionWithoutStandards(expectedDefinition));
102464
+ function contains(standards, candidate) {
102465
+ return standards.some((standard) => canonicalJson2(standard) === canonicalJson2(candidate));
102466
+ }
102467
+ return sameDefinition && (expectedStandards.every((standard) => contains(storedStandards, standard)) || storedStandards.every((standard) => contains(expectedStandards, standard)));
102468
+ }
102469
+ function assessmentChildLineItemMatches(stored, expected) {
102470
+ 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) {
102471
+ return false;
102472
+ }
102473
+ return Object.entries(expected.metadata).every(([key, value]) => assessmentChildLineItemMetadataMatches(key, stored.metadata[key], value));
102474
+ }
102475
+ function buildAssessmentChildResultUpdate(command, childLineItemId) {
102476
+ return {
102477
+ status: "active",
102478
+ assessmentLineItem: { sourcedId: childLineItemId },
102479
+ student: { sourcedId: command.payload.attempt.studentId },
102480
+ score: command.payload.score.earned,
102481
+ scoreDate: command.recordedAt,
102482
+ ...ASSESSMENT_ATTEMPT_COMPLETED,
102483
+ metadata: {
102484
+ [PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY]: command
102485
+ }
102486
+ };
102487
+ }
102488
+ async function classifyAssessmentChildResult(input) {
102489
+ const { result } = input;
102490
+ if (typeof result.metadata !== "object" || result.metadata === null || !(PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY in result.metadata)) {
102491
+ return { outcome: "conflict", reason: "malformed-command" };
102492
+ }
102493
+ const verification2 = await verifyAssessmentChildResultCommand(result.metadata);
102494
+ if (!verification2.ok) {
102495
+ return {
102496
+ outcome: "conflict",
102497
+ reason: verification2.reason === "payload_hash_mismatch" ? "payload-hash-mismatch" : "malformed-command"
102498
+ };
102499
+ }
102500
+ if (verification2.command.payloadHash !== input.command.payloadHash) {
102501
+ return { outcome: "conflict", reason: "payload-conflict" };
102502
+ }
102503
+ if (result.status === "tobedeleted") {
102504
+ return { outcome: "conflict", reason: "tombstoned" };
102505
+ }
102506
+ const expected = buildAssessmentChildResultUpdate(verification2.command, input.childLineItemId);
102507
+ const projectionChecks = [
102508
+ ["status-mismatch", result.status, expected.status],
102509
+ ["line-item-mismatch", result.assessmentLineItem?.sourcedId, input.childLineItemId],
102510
+ ["student-mismatch", result.student?.sourcedId, expected.student.sourcedId],
102511
+ ["score-mismatch", result.score, expected.score],
102512
+ ["score-date-mismatch", result.scoreDate, expected.scoreDate],
102513
+ ["score-status-mismatch", result.scoreStatus, expected.scoreStatus],
102514
+ ["in-progress-mismatch", result.inProgress, expected.inProgress]
102515
+ ];
102516
+ const mismatch = projectionChecks.find(([, stored, wanted]) => stored !== wanted);
102517
+ if (mismatch) {
102518
+ return { outcome: "conflict", reason: mismatch[0] };
102519
+ }
102520
+ const auxiliaryFields = [
102521
+ result.textScore,
102522
+ result.scoreScale,
102523
+ result.scorePercentile,
102524
+ result.comment,
102525
+ result.learningObjectiveSet,
102526
+ result.incomplete,
102527
+ result.late,
102528
+ result.missing
102529
+ ];
102530
+ if (auxiliaryFields.some((value) => value !== undefined && value !== null)) {
102531
+ return { outcome: "conflict", reason: "auxiliary-field-mismatch" };
102532
+ }
102533
+ return { outcome: "replay" };
102534
+ }
102535
+ function hasApiStatus(error88, statusCode) {
102536
+ return isApiError(error88) && error88.statusCode === statusCode;
102537
+ }
102538
+ async function persistAssessmentChildResult(input) {
102539
+ const { assessmentResults, childLineItemId } = input;
102540
+ const verification2 = await verifyAssessmentChildResultCommand(input.command);
102541
+ if (!verification2.ok) {
102542
+ throw new Error(`Invalid assessment child result command: ${verification2.reason}`);
102543
+ }
102544
+ const { command } = verification2;
102545
+ const resultId = await assessmentChildResultId(command.payload.attempt.attemptId, command.payload.administration.submissionId);
102546
+ async function classifyOccupant() {
102547
+ const resolution = await classifyAssessmentChildResult({
102548
+ result: await assessmentResults.get(resultId),
102549
+ command,
102550
+ childLineItemId
102551
+ });
102552
+ return resolution.outcome === "replay" ? { outcome: "replay", resultId } : { outcome: "conflict", resultId, reason: resolution.reason };
102553
+ }
102554
+ try {
102555
+ return await classifyOccupant();
102556
+ } catch (error88) {
102557
+ if (!hasApiStatus(error88, 404)) {
102558
+ throw error88;
102559
+ }
102560
+ }
102561
+ try {
102562
+ await assessmentResults.create({
102563
+ sourcedId: resultId,
102564
+ ...buildAssessmentChildResultUpdate(command, childLineItemId)
102565
+ });
102566
+ return { outcome: "created", resultId };
102567
+ } catch (error88) {
102568
+ if (!hasApiStatus(error88, 409)) {
102569
+ throw error88;
102570
+ }
102571
+ }
102572
+ return classifyOccupant();
102573
+ }
102574
+ var init_timeback_assessment_child_result_util = __esm(async () => {
102575
+ init_src();
102576
+ init_assessment_runtime2();
102577
+ await init_errors8();
102578
+ });
102579
+
101841
102580
  // ../api-core/src/utils/timeback-assessment-preparation.util.ts
101842
102581
  class AssessmentPreparationFlights {
101843
102582
  limit;
@@ -101970,6 +102709,7 @@ function buildPreparedAssessmentMetadata(plan, timestamp6, testName) {
101970
102709
  testName,
101971
102710
  routingRevision: plan.diagnostic.routingRevision,
101972
102711
  ledger: [],
102712
+ routingSnapshots: [],
101973
102713
  state: plan.diagnostic.initialState
101974
102714
  }
101975
102715
  };
@@ -102023,7 +102763,7 @@ function declarationNeutralQtiXml(source) {
102023
102763
  }
102024
102764
  function managedMetadataRecord(metadata2, key) {
102025
102765
  const managed = metadata2?.[key];
102026
- return isRecord5(managed) ? managed : null;
102766
+ return isRecord4(managed) ? managed : null;
102027
102767
  }
102028
102768
  function sourceMetadata(metadata2) {
102029
102769
  return Object.fromEntries(Object.entries(metadata2 ?? {}).filter(([key]) => !GENERATED_METADATA_KEYS.has(key)));
@@ -102224,7 +102964,7 @@ function assertManagedAssessmentPublication(test, expected) {
102224
102964
  function managedAssessmentDiagnosticRoutingManifest(test, assessmentKey) {
102225
102965
  const managed = managedMetadataRecord(test.metadata, PLAYCADEMY_MANAGED_ASSESSMENT_METADATA_KEY);
102226
102966
  const manifest = managed?.diagnosticRoutingManifest;
102227
- return managed?.assessmentKey === assessmentKey && isRecord5(manifest) ? manifest : null;
102967
+ return managed?.assessmentKey === assessmentKey && isRecord4(manifest) ? manifest : null;
102228
102968
  }
102229
102969
  async function assertManagedAssessmentItem(item, expected) {
102230
102970
  const managed = managedMetadataRecord(item.metadata, PLAYCADEMY_MANAGED_ITEM_METADATA_KEY);
@@ -102264,6 +103004,198 @@ var init_timeback_assessment_publication_util = __esm(() => {
102264
103004
  MANAGED_ASSET_URL = new RegExp(`https?://[^\\s"')]+/(${ASSESSMENT_ASSET_KEY_PREFIX}v1/sha256/[a-f0-9]{64}\\.[a-z0-9]+)`, "gi");
102265
103005
  });
102266
103006
 
103007
+ // ../api-core/src/utils/timeback-assessment-reconciliation.util.ts
103008
+ function assessmentChildProjectionIsTerminal(result) {
103009
+ const state = { inProgress: result.inProgress ?? "", scoreStatus: result.scoreStatus };
103010
+ return isAssessmentAttemptAwaitingAward(state) || isAssessmentAttemptCompleted(state);
103011
+ }
103012
+ async function reconcileReviewChildResults({
103013
+ initial,
103014
+ writeOpen,
103015
+ crossBarrier,
103016
+ readLatest,
103017
+ restoreTerminal
103018
+ }) {
103019
+ if (initial.metadata.purpose !== "review") {
103020
+ throw new Error("A standards-review request selected a non-review attempt");
103021
+ }
103022
+ if (assessmentChildProjectionIsTerminal(initial.result)) {
103023
+ const settled = { result: initial.result, metadata: initial.metadata };
103024
+ await restoreTerminal(settled);
103025
+ return settled;
103026
+ }
103027
+ await writeOpen();
103028
+ await crossBarrier();
103029
+ const current = await readLatest();
103030
+ if (current.metadata.purpose !== "review") {
103031
+ throw new Error("A standards-review request selected a non-review attempt");
103032
+ }
103033
+ const reviewed = { result: current.result, metadata: current.metadata };
103034
+ if (assessmentChildProjectionIsTerminal(current.result)) {
103035
+ await restoreTerminal(reviewed);
103036
+ }
103037
+ return reviewed;
103038
+ }
103039
+ var init_timeback_assessment_reconciliation_util = __esm(() => {
103040
+ init_assessment_runtime2();
103041
+ });
103042
+
103043
+ // ../api-core/src/utils/timeback-assessment-replay.util.ts
103044
+ function resolveDiagnosticItemReplay(metadata2, input) {
103045
+ const priorIndex = metadata2.itemSubmissions.findIndex((submission) => submission.submissionId === input.submissionId);
103046
+ if (priorIndex === -1) {
103047
+ return { action: "missing" };
103048
+ }
103049
+ const prior = metadata2.itemSubmissions[priorIndex];
103050
+ const ledgerEntry = metadata2.diagnostic.ledger[priorIndex];
103051
+ if (!prior || !ledgerEntry || prior.itemIdentifier !== input.itemIdentifier || ledgerEntry.routingNodeKey !== input.routingNodeKey || !itemResponseUpdateMatches(metadata2.responses[input.itemIdentifier], input.responses)) {
103052
+ return {
103053
+ action: "conflict",
103054
+ failure: assessmentFlowViolation("This diagnostic submission ID was already used for a different request.", {
103055
+ submissionId: input.submissionId,
103056
+ routingNodeKey: input.routingNodeKey,
103057
+ itemIdentifier: input.itemIdentifier
103058
+ })
103059
+ };
103060
+ }
103061
+ return {
103062
+ action: "replay",
103063
+ submission: prior,
103064
+ ledgerEntry,
103065
+ priorIndex,
103066
+ routing: metadata2.diagnostic.routingSnapshots?.[priorIndex]
103067
+ };
103068
+ }
103069
+ function buildAssessmentItemReplayResult(attemptId, metadata2, submission) {
103070
+ return {
103071
+ attemptId,
103072
+ responseVersion: metadata2.responseVersion,
103073
+ status: "in_progress",
103074
+ responses: metadata2.responses,
103075
+ itemSubmissions: metadata2.itemSubmissions,
103076
+ submission
103077
+ };
103078
+ }
103079
+ function diagnosticReceipt(submission, ledgerEntry) {
103080
+ return {
103081
+ submissionId: submission.submissionId,
103082
+ routingNodeKey: ledgerEntry.routingNodeKey,
103083
+ itemIdentifier: submission.itemIdentifier,
103084
+ submittedAt: submission.submittedAt,
103085
+ responseVersion: submission.responseVersion,
103086
+ answered: submission.answered,
103087
+ score: submission.score,
103088
+ isCorrect: ledgerEntry.isCorrect
103089
+ };
103090
+ }
103091
+ function buildDiagnosticItemReplayResult(attemptId, submission, ledgerEntry, routing) {
103092
+ return {
103093
+ attemptId,
103094
+ responseVersion: submission.responseVersion,
103095
+ status: "in_progress",
103096
+ routing,
103097
+ submission: diagnosticReceipt(submission, ledgerEntry)
103098
+ };
103099
+ }
103100
+ function requireAssessmentMutationAccess(attemptId, integrationStatus, enrollment) {
103101
+ if (!enrollment || integrationStatus === "deactivated") {
103102
+ throw AssessmentRuntimeError.from(assessmentAttemptUnauthorized(attemptId));
103103
+ }
103104
+ return enrollment;
103105
+ }
103106
+ function committedReviewItemResults(metadata2, assessment) {
103107
+ const available = new Set(assessment.items.map((item) => item.identifier));
103108
+ const seen = new Set;
103109
+ return metadata2.itemSubmissions.map((submission) => {
103110
+ if (!available.has(submission.itemIdentifier) || seen.has(submission.itemIdentifier)) {
103111
+ throw AssessmentRuntimeError.from(assessmentFlowViolation("The persisted review item-submission ledger is corrupt.", {
103112
+ itemIdentifier: submission.itemIdentifier
103113
+ }));
103114
+ }
103115
+ seen.add(submission.itemIdentifier);
103116
+ return {
103117
+ itemIdentifier: submission.itemIdentifier,
103118
+ score: submission.score,
103119
+ isCorrect: submission.isCorrect
103120
+ };
103121
+ });
103122
+ }
103123
+ var init_timeback_assessment_replay_util = __esm(() => {
103124
+ init_assessment_runtime2();
103125
+ init_errors2();
103126
+ });
103127
+
103128
+ // ../api-core/src/utils/timeback-assessment-routing.util.ts
103129
+ function diagnosticRoutingFallbackReason(artifact, routingRevision) {
103130
+ if (!artifact) {
103131
+ return "artifact-unavailable";
103132
+ }
103133
+ if (!artifact.diagnostic) {
103134
+ return "diagnostic-context-missing";
103135
+ }
103136
+ return artifact.diagnostic.routingRevision === routingRevision ? "routing-manifest-invalid" : "routing-revision-mismatch";
103137
+ }
103138
+ function assertDiagnosticItemsAvailable(definitionId, manifest, assessment) {
103139
+ const itemIdentifiers = new Set(assessment.items.map((item) => item.identifier));
103140
+ const unknownItem = manifest.nodes.find((node) => !itemIdentifiers.has(node.itemIdentifier));
103141
+ if (unknownItem) {
103142
+ throw new AssessmentRuntimeError("SELECTED_TEST_VERSION_UNAVAILABLE", `Diagnostic routing references unavailable item ${unknownItem.itemIdentifier}.`, {
103143
+ definitionId,
103144
+ nodeKey: unknownItem.key,
103145
+ itemIdentifier: unknownItem.itemIdentifier
103146
+ });
103147
+ }
103148
+ }
103149
+ function replayAttemptDiagnosticRouting(metadata2, manifest) {
103150
+ const replayed = replayDiagnosticRouting(manifest, metadata2.diagnostic.ledger);
103151
+ if (!replayed.ok || canonicalJson2(replayed.state) !== canonicalJson2(metadata2.diagnostic.state)) {
103152
+ throw AssessmentRuntimeError.from(assessmentFlowViolation("The persisted diagnostic routing state is corrupt.", {
103153
+ definitionId: metadata2.diagnostic.definitionId,
103154
+ ...replayed.ok ? {} : { failure: replayed.failure }
103155
+ }));
103156
+ }
103157
+ return replayed.state;
103158
+ }
103159
+ async function resolveCompiledDiagnosticRouting(metadata2, assessment, artifact) {
103160
+ const diagnostic = artifact?.diagnostic;
103161
+ if (diagnostic?.routingRevision !== metadata2.diagnostic.routingRevision) {
103162
+ return null;
103163
+ }
103164
+ const initialized = initializeDiagnosticRouting(diagnostic.routingManifest);
103165
+ if (!initialized.ok) {
103166
+ return null;
103167
+ }
103168
+ const routingRevision = await diagnosticRoutingRevision(initialized.manifest);
103169
+ if (routingRevision !== metadata2.diagnostic.routingRevision) {
103170
+ return null;
103171
+ }
103172
+ assertDiagnosticItemsAvailable(metadata2.diagnostic.definitionId, initialized.manifest, assessment);
103173
+ return {
103174
+ manifest: initialized.manifest,
103175
+ routingRevision,
103176
+ state: replayAttemptDiagnosticRouting(metadata2, initialized.manifest)
103177
+ };
103178
+ }
103179
+ function summarizeDiagnosticSubmission(metadata2, manifest) {
103180
+ const state = replayAttemptDiagnosticRouting(metadata2, manifest);
103181
+ const tracks = diagnosticTrackResults(state);
103182
+ if (!tracks) {
103183
+ throw AssessmentRuntimeError.from(assessmentFlowViolation("Diagnostic routing must be ready to complete before final submit.", { routingStatus: state.status }));
103184
+ }
103185
+ if (metadata2.itemSubmissions.length === 0) {
103186
+ throw AssessmentRuntimeError.from(assessmentFlowViolation("A diagnostic cannot be finalized without an administered item."));
103187
+ }
103188
+ const possible = metadata2.itemSubmissions.reduce((total, submission) => total + submission.score.possible, 0);
103189
+ const score = aggregateAssessmentScore(metadata2.itemSubmissions.map((submission) => submission.score.earned), possible);
103190
+ const correctQuestions = countCorrectQuestions(metadata2.itemSubmissions.map((submission) => submission.isCorrect));
103191
+ return { tracks, score, correctQuestions };
103192
+ }
103193
+ var init_timeback_assessment_routing_util = __esm(() => {
103194
+ init_assessment_runtime2();
103195
+ init_errors2();
103196
+ init_timeback_assessment_runtime_util();
103197
+ });
103198
+
102267
103199
  // ../api-core/src/utils/timeback-assessment-rules.util.ts
102268
103200
  function validateAssessmentStatusTransition(current, next) {
102269
103201
  if (current === next) {
@@ -102342,6 +103274,156 @@ var init_timeback_assessment_rules_util = __esm(() => {
102342
103274
  init_errors2();
102343
103275
  });
102344
103276
 
103277
+ // ../api-core/src/utils/timeback-assessment-scoring.util.ts
103278
+ async function scoreAssessmentItems({
103279
+ assessment,
103280
+ responses,
103281
+ artifactIdentity,
103282
+ purpose,
103283
+ itemIdentifiers,
103284
+ artifact,
103285
+ processResponse,
103286
+ onEvent
103287
+ }) {
103288
+ const scoringItems = assessmentScoringRequests(assessment, responses).filter((item) => !itemIdentifiers || itemIdentifiers.has(item.itemIdentifier));
103289
+ const artifactItems = new Map(artifact?.items.map((item) => [item.itemIdentifier, item]));
103290
+ const scored = new Map;
103291
+ const qti2 = [];
103292
+ for (const item of scoringItems) {
103293
+ const artifactItem = artifactItems.get(item.itemIdentifier);
103294
+ let fallback2 = null;
103295
+ if (!artifact) {
103296
+ fallback2 = { reason: "artifact-unavailable" };
103297
+ } else if (!artifactItem) {
103298
+ fallback2 = { reason: "artifact-item-missing" };
103299
+ } else if (artifactItem.strategy === "qti") {
103300
+ fallback2 = { reason: "artifact-qti", artifactReason: artifactItem.reason };
103301
+ } else {
103302
+ const startedAt = Date.now();
103303
+ try {
103304
+ scored.set(item.itemIdentifier, {
103305
+ ...scorePlatformAssessmentItem(artifactItem, responses[item.itemIdentifier]),
103306
+ grading: PLATFORM_CHILD_GRADING
103307
+ });
103308
+ onEvent("assessment.item_scoring", {
103309
+ "app.assessment.qti_item_identifier": item.itemIdentifier,
103310
+ "app.assessment.purpose": purpose,
103311
+ "app.assessment.artifact_identity_kind": artifactIdentity.kind,
103312
+ "app.assessment.grading_source": "platform-artifact",
103313
+ "app.assessment.grading_duration_ms": Date.now() - startedAt
103314
+ });
103315
+ } catch (error88) {
103316
+ fallback2 = { reason: "platform-grader-error", error: error88 };
103317
+ }
103318
+ }
103319
+ if (fallback2) {
103320
+ qti2.push({ item, ...fallback2 });
103321
+ }
103322
+ }
103323
+ if (qti2.length > 0) {
103324
+ const fallbackTasks = qti2.flatMap(({ item, reason, artifactReason, error: graderError }) => {
103325
+ const interactionResults = [];
103326
+ let startedAt;
103327
+ let completedInteractions = 0;
103328
+ let failed = false;
103329
+ const omittedResponseCount = item.requests.filter((request) => responses[item.itemIdentifier]?.[request.identifier] === undefined).length;
103330
+ let qtiRemoteInteractionCount = 0;
103331
+ let syntheticIncorrectInteractionCount = 0;
103332
+ function fallbackEvent(outcome) {
103333
+ onEvent("assessment.item_scoring_fallback", {
103334
+ "app.assessment.qti_item_identifier": item.itemIdentifier,
103335
+ "app.assessment.purpose": purpose,
103336
+ "app.assessment.artifact_identity_kind": artifactIdentity.kind,
103337
+ "app.assessment.grading_source": omittedResponseCount > 0 ? "platform-qti-adapter" : "timeback-qti",
103338
+ "app.assessment.fallback_reason": reason,
103339
+ "app.assessment.fallback_duration_ms": startedAt === undefined ? 0 : Date.now() - startedAt,
103340
+ "app.assessment.qti_remote_interaction_count": qtiRemoteInteractionCount,
103341
+ "app.assessment.synthetic_incorrect_interaction_count": syntheticIncorrectInteractionCount,
103342
+ ...artifactReason ? { "app.assessment.artifact_fallback_reason": artifactReason } : {},
103343
+ ...graderError ? {
103344
+ "app.assessment.platform_grader_exception_type": errorType(graderError)
103345
+ } : {},
103346
+ ...outcome
103347
+ });
103348
+ }
103349
+ function completeItem() {
103350
+ const earned = assessmentItemEarnedScore(interactionResults.map((result) => Number.isFinite(result.score) ? result.score : 0), item.maxScore);
103351
+ const possible = item.maxScore;
103352
+ fallbackEvent({ "app.assessment.fallback_outcome": "succeeded" });
103353
+ scored.set(item.itemIdentifier, {
103354
+ itemIdentifier: item.itemIdentifier,
103355
+ score: {
103356
+ earned,
103357
+ possible,
103358
+ normalized: possible === 0 ? 0 : earned / possible
103359
+ },
103360
+ isCorrect: assessmentItemCorrectness(interactionResults.map((result) => result.isCorrect)),
103361
+ grading: omittedResponseCount > 0 ? PLATFORM_QTI_ADAPTER_CHILD_GRADING : QTI_CHILD_GRADING
103362
+ });
103363
+ }
103364
+ if (item.requests.length === 0) {
103365
+ completeItem();
103366
+ return [];
103367
+ }
103368
+ return item.requests.map((request, index2) => async () => {
103369
+ startedAt ??= Date.now();
103370
+ try {
103371
+ const response = responses[item.itemIdentifier]?.[request.identifier];
103372
+ if (response === undefined) {
103373
+ syntheticIncorrectInteractionCount += 1;
103374
+ interactionResults[index2] = { score: 0, isCorrect: false };
103375
+ } else {
103376
+ qtiRemoteInteractionCount += 1;
103377
+ interactionResults[index2] = await processResponse(item.itemIdentifier, request);
103378
+ }
103379
+ completedInteractions += 1;
103380
+ if (completedInteractions === item.requests.length && !failed) {
103381
+ completeItem();
103382
+ }
103383
+ } catch (error88) {
103384
+ if (!failed) {
103385
+ failed = true;
103386
+ fallbackEvent({
103387
+ "app.assessment.fallback_outcome": "failed",
103388
+ "exception.type": errorType(error88),
103389
+ "app.error.message": errorMessage(error88)
103390
+ });
103391
+ }
103392
+ throw error88;
103393
+ }
103394
+ });
103395
+ });
103396
+ await runWithConcurrency(fallbackTasks, SCORING_CONCURRENCY, (task) => task());
103397
+ }
103398
+ return scoringItems.map((item) => {
103399
+ const result = scored.get(item.itemIdentifier);
103400
+ if (!result) {
103401
+ throw new Error(`Assessment item ${item.itemIdentifier} was not scored`);
103402
+ }
103403
+ return result;
103404
+ });
103405
+ }
103406
+ var QTI_CHILD_GRADING, PLATFORM_QTI_ADAPTER_CHILD_GRADING, PLATFORM_CHILD_GRADING, SCORING_CONCURRENCY = 4;
103407
+ var init_timeback_assessment_scoring_util = __esm(() => {
103408
+ init_assessment_runtime2();
103409
+ init_assessment_artifact_envelope_util();
103410
+ init_timeback_assessment_runtime_util();
103411
+ QTI_CHILD_GRADING = {
103412
+ source: "timeback-qti",
103413
+ graderVersion: "timeback-qti-process-response-v1"
103414
+ };
103415
+ PLATFORM_QTI_ADAPTER_CHILD_GRADING = {
103416
+ source: "platform-qti-adapter",
103417
+ graderVersion: "platform-qti-adapter-v1",
103418
+ qtiGraderVersion: "timeback-qti-process-response-v1"
103419
+ };
103420
+ PLATFORM_CHILD_GRADING = {
103421
+ source: "platform-artifact",
103422
+ artifactVersion: `assessment-artifact-v${ASSESSMENT_ARTIFACT_SCHEMA_VERSION}`,
103423
+ graderVersion: `platform-grader-v${ASSESSMENT_GRADER_PROTOCOL_VERSION}`
103424
+ };
103425
+ });
103426
+
102345
103427
  // ../api-core/src/utils/timeback-qti-hydration.util.ts
102346
103428
  function qtiItemHref(client2, itemIdentifier) {
102347
103429
  return `${client2.getQtiBaseUrl()}/assessment-items/${itemIdentifier}`;
@@ -102419,6 +103501,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
102419
103501
  init_spans();
102420
103502
  init_assessment_runtime2();
102421
103503
  init_constants3();
103504
+ init_qti();
102422
103505
  init_types2();
102423
103506
  init_uuid();
102424
103507
  init_errors2();
@@ -102427,13 +103510,18 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
102427
103510
  init_timeback_assessment_artifacts_util();
102428
103511
  init_timeback_assessment_preparation_util();
102429
103512
  init_timeback_assessment_publication_util();
103513
+ init_timeback_assessment_reconciliation_util();
103514
+ init_timeback_assessment_replay_util();
103515
+ init_timeback_assessment_routing_util();
102430
103516
  init_timeback_assessment_rules_util();
102431
103517
  init_timeback_assessment_runtime_util();
103518
+ init_timeback_assessment_scoring_util();
102432
103519
  init_timeback_qti_hydration_util();
102433
103520
  await __promiseAll([
102434
103521
  init_dist5(),
102435
103522
  init_errors8(),
102436
- init_assessment_artifact_loader_util()
103523
+ init_assessment_artifact_loader_util(),
103524
+ init_timeback_assessment_child_result_util()
102437
103525
  ]);
102438
103526
  AssessmentPreparationPrerequisiteError = class AssessmentPreparationPrerequisiteError extends Error {
102439
103527
  original;
@@ -102814,12 +103902,19 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
102814
103902
  activityId: params.input.activityId,
102815
103903
  source
102816
103904
  }));
102817
- await this.provisionPreparationPrerequisites(() => runWithConcurrency(assessment.items, TimebackAssessmentRuntimeService.SCORING_CONCURRENCY, (item) => this.ensureReviewQuestionLineItem({
102818
- parentLineItemId,
102819
- integration: context2.integration,
102820
- bank: catalog.bank,
102821
- item
102822
- })));
103905
+ await this.provisionPreparationPrerequisites(() => runWithConcurrency(assessment.items, TimebackAssessmentRuntimeService.SCORING_CONCURRENCY, (item) => {
103906
+ const indexed = catalog.bank.items.find((candidate) => candidate.itemIdentifier === item.identifier);
103907
+ if (!indexed) {
103908
+ throw new Error(`Review-bank index is missing item ${item.identifier}`);
103909
+ }
103910
+ return this.ensureReviewQuestionLineItem({
103911
+ parentLineItemId,
103912
+ integration: context2.integration,
103913
+ bankRevision: catalog.bank.bankRevision,
103914
+ standards: [...indexed.standards],
103915
+ item
103916
+ });
103917
+ }));
102823
103918
  const attempt = await this.provisionPreparationPrerequisites(() => this.claimAttemptId({
102824
103919
  gameId: params.gameId,
102825
103920
  studentId: params.studentId,
@@ -103008,6 +104103,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
103008
104103
  testName: assessment.title,
103009
104104
  routingRevision: routing.routingRevision,
103010
104105
  ledger: [],
104106
+ routingSnapshots: [],
103011
104107
  state: routing.state
103012
104108
  }
103013
104109
  };
@@ -103167,15 +104263,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
103167
104263
  grade: context2.integration.grade
103168
104264
  })
103169
104265
  });
103170
- await this.ensureReviewChildResults({
103171
- result,
103172
- metadata: metadata2,
103173
- integration: context2.integration,
103174
- source,
103175
- assessment,
103176
- existingChildResults: refreshedListing.reviewChildren,
103177
- lookupMissingResults: false
103178
- });
103179
104266
  await this.persistPendingSupersessionsBestEffort(db2, pendingSupersessions);
103180
104267
  this.recordReviewPreparationPhase("provisioning", provisioningStartedAt);
103181
104268
  this.recordReviewPreparationPhase("total", preparationStartedAt, {
@@ -103310,13 +104397,39 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
103310
104397
  let preparationAttempts = 1;
103311
104398
  while (true) {
103312
104399
  const committed = await this.deps.assessmentRuntimeLock(this.deps.db, [assessmentRuntimeAttemptLockKey(params.attemptId)], async (db2) => {
103313
- const attempt = await this.authorizeAttempt(params, {
104400
+ const { attempt, enrollment } = await this.resolveAttemptAuthorization(params, {
103314
104401
  db: db2,
103315
104402
  developerAccessValidated: true
103316
104403
  });
103317
104404
  if (this.isPlatformRoutedDiagnosticMetadata(attempt.metadata)) {
104405
+ this.requireActiveAttempt({ attempt, enrollment }, params.attemptId);
103318
104406
  throw AssessmentRuntimeError.from(assessmentFlowViolation("Platform-routed diagnostic submissions require routingNodeKey.", { flow: "platform-routed-item-submit" }));
103319
104407
  }
104408
+ const replay = resolveAssessmentItemReplay(attempt.metadata, input);
104409
+ if (replay.action === "conflict") {
104410
+ this.requireActiveAttempt({ attempt, enrollment }, params.attemptId);
104411
+ throw AssessmentRuntimeError.from(replay.failure);
104412
+ }
104413
+ if (replay.action === "replay") {
104414
+ const prior = replay.submission;
104415
+ const child2 = attempt.metadata.purpose === "review" ? await this.prepareReviewAssessmentChildResult({
104416
+ attempt,
104417
+ metadata: attempt.metadata,
104418
+ submission: prior
104419
+ }) : null;
104420
+ return {
104421
+ action: "complete",
104422
+ response: buildAssessmentItemReplayResult(attempt.result.sourcedId, attempt.metadata, prior),
104423
+ checkpoint: child2,
104424
+ projection: attempt.metadata.purpose === "review" ? {
104425
+ result: attempt.result,
104426
+ metadata: attempt.metadata,
104427
+ itemIdentifier: input.itemIdentifier,
104428
+ integration: attempt.integration
104429
+ } : null
104430
+ };
104431
+ }
104432
+ this.requireActiveAttempt({ attempt, enrollment }, params.attemptId);
103320
104433
  this.assertInProgress(attempt.result);
103321
104434
  if (!preview || preview.responseVersion !== attempt.metadata.responseVersion || preview.submissionId !== input.submissionId || preview.itemIdentifier !== input.itemIdentifier) {
103322
104435
  if (preparationAttempts >= TimebackAssessmentRuntimeService.ITEM_SUBMISSION_MAX_PREPARATION_ATTEMPTS) {
@@ -103335,6 +104448,11 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
103335
104448
  throw AssessmentRuntimeError.from(transition.failure);
103336
104449
  }
103337
104450
  if (transition.action === "replay") {
104451
+ const child2 = attempt.metadata.purpose === "review" ? await this.prepareReviewAssessmentChildResult({
104452
+ attempt,
104453
+ metadata: attempt.metadata,
104454
+ submission: transition.submission
104455
+ }) : null;
103338
104456
  return {
103339
104457
  action: "complete",
103340
104458
  response: {
@@ -103345,10 +104463,12 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
103345
104463
  itemSubmissions: attempt.metadata.itemSubmissions,
103346
104464
  submission: transition.submission
103347
104465
  },
104466
+ checkpoint: child2,
103348
104467
  projection: attempt.metadata.purpose === "review" ? {
103349
104468
  result: attempt.result,
103350
104469
  metadata: attempt.metadata,
103351
- itemIdentifier: input.itemIdentifier
104470
+ itemIdentifier: input.itemIdentifier,
104471
+ integration: attempt.integration
103352
104472
  } : null
103353
104473
  };
103354
104474
  }
@@ -103364,6 +104484,11 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
103364
104484
  itemSubmissions: completed.itemSubmissions,
103365
104485
  updatedAt: submittedAt
103366
104486
  };
104487
+ const child = metadata2.purpose === "review" ? await this.prepareReviewAssessmentChildResult({
104488
+ attempt,
104489
+ metadata: metadata2,
104490
+ submission: completed.submission
104491
+ }) : null;
103367
104492
  await this.putResultMetadata(attempt.result, metadata2);
103368
104493
  return {
103369
104494
  action: "complete",
@@ -103375,10 +104500,12 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
103375
104500
  itemSubmissions: completed.itemSubmissions,
103376
104501
  submission: completed.submission
103377
104502
  },
104503
+ checkpoint: child,
103378
104504
  projection: metadata2.purpose === "review" ? {
103379
104505
  result: attempt.result,
103380
104506
  metadata: metadata2,
103381
- itemIdentifier: input.itemIdentifier
104507
+ itemIdentifier: input.itemIdentifier,
104508
+ integration: attempt.integration
103382
104509
  } : null
103383
104510
  };
103384
104511
  });
@@ -103387,7 +104514,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
103387
104514
  preview = await this.scoreItemSubmission(params, input);
103388
104515
  } else {
103389
104516
  if (committed.projection) {
103390
- await this.projectReviewItemResponse(params, committed.projection);
104517
+ this.projectReviewItemResponse(params, committed.projection, committed.checkpoint);
103391
104518
  }
103392
104519
  return committed.response;
103393
104520
  }
@@ -103398,40 +104525,39 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
103398
104525
  let preparationAttempts = 1;
103399
104526
  while (true) {
103400
104527
  const committed = await this.deps.assessmentRuntimeLock(this.deps.db, [assessmentRuntimeAttemptLockKey(params.attemptId)], async (db2) => {
103401
- const attempt = await this.authorizeAttempt(params, {
103402
- db: db2,
103403
- developerAccessValidated: true
103404
- });
104528
+ const { attempt, enrollment } = await this.resolveAttemptAuthorization(params, { db: db2, developerAccessValidated: true });
103405
104529
  if (!this.isPlatformRoutedDiagnosticMetadata(attempt.metadata)) {
104530
+ this.requireActiveAttempt({ attempt, enrollment }, params.attemptId);
103406
104531
  throw AssessmentRuntimeError.from(assessmentFlowViolation("routingNodeKey is accepted only for a platform-routed diagnostic.", { routingNodeKey: input.routingNodeKey }));
103407
104532
  }
103408
104533
  const metadata2 = attempt.metadata;
103409
- const priorIndex = metadata2.itemSubmissions.findIndex((submission2) => submission2.submissionId === input.submissionId);
103410
- if (priorIndex !== -1) {
103411
- const prior = metadata2.itemSubmissions[priorIndex];
103412
- const ledgerEntry = metadata2.diagnostic.ledger[priorIndex];
103413
- const sameRequest = prior !== undefined && ledgerEntry !== undefined && prior.itemIdentifier === input.itemIdentifier && ledgerEntry.routingNodeKey === input.routingNodeKey && itemResponseUpdateMatches(metadata2.responses[input.itemIdentifier], input.responses);
103414
- if (!sameRequest || !prior || !ledgerEntry) {
103415
- throw AssessmentRuntimeError.from(assessmentFlowViolation("This diagnostic submission ID was already used for a different request.", {
103416
- submissionId: input.submissionId,
103417
- routingNodeKey: input.routingNodeKey,
103418
- itemIdentifier: input.itemIdentifier
103419
- }));
103420
- }
103421
- const loaded2 = await this.loadAttemptDiagnosticManifest(metadata2, db2);
103422
- const replayed = replayDiagnosticRouting(loaded2.manifest, metadata2.diagnostic.ledger.slice(0, priorIndex + 1));
103423
- if (!replayed.ok) {
103424
- throw AssessmentRuntimeError.from(assessmentFlowViolation("The committed diagnostic routing state cannot be replayed.", { failure: replayed.failure }));
104534
+ const replay = resolveDiagnosticItemReplay(metadata2, input);
104535
+ if (replay.action === "conflict") {
104536
+ this.requireActiveAttempt({ attempt, enrollment }, params.attemptId);
104537
+ throw AssessmentRuntimeError.from(replay.failure);
104538
+ }
104539
+ if (replay.action === "replay") {
104540
+ const { submission: prior, ledgerEntry, priorIndex } = replay;
104541
+ let routing = replay.routing;
104542
+ if (!routing) {
104543
+ const assessment = preview?.assessment ?? await this.loadAttemptAssessment(metadata2);
104544
+ const loaded2 = await this.loadAttemptDiagnosticRouting(metadata2, assessment, db2);
104545
+ const replayed = replayDiagnosticRouting(loaded2.manifest, metadata2.diagnostic.ledger.slice(0, priorIndex + 1));
104546
+ if (!replayed.ok) {
104547
+ throw AssessmentRuntimeError.from(assessmentFlowViolation("The committed diagnostic routing state cannot be replayed.", { failure: replayed.failure }));
104548
+ }
104549
+ routing = this.diagnosticRoutingSnapshot(metadata2, replayed.state);
103425
104550
  }
104551
+ const child2 = await this.prepareDiagnosticAssessmentChildResult({
104552
+ attempt,
104553
+ metadata: metadata2,
104554
+ submission: prior,
104555
+ transition: ledgerEntry
104556
+ });
103426
104557
  return {
103427
104558
  action: "complete",
103428
- response: {
103429
- attemptId: attempt.result.sourcedId,
103430
- responseVersion: prior.responseVersion,
103431
- status: "in_progress",
103432
- routing: this.diagnosticRoutingSnapshot(metadata2, replayed.state),
103433
- submission: this.diagnosticReceipt(prior, ledgerEntry)
103434
- },
104559
+ response: buildDiagnosticItemReplayResult(attempt.result.sourcedId, prior, ledgerEntry, routing),
104560
+ checkpoint: child2,
103435
104561
  projection: {
103436
104562
  result: attempt.result,
103437
104563
  metadata: metadata2,
@@ -103440,6 +104566,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
103440
104566
  }
103441
104567
  };
103442
104568
  }
104569
+ this.requireActiveAttempt({ attempt, enrollment }, params.attemptId);
103443
104570
  this.assertInProgress(attempt.result);
103444
104571
  if (!preview || preview.responseVersion !== metadata2.responseVersion || preview.submissionId !== input.submissionId || preview.routingNodeKey !== input.routingNodeKey || preview.itemIdentifier !== input.itemIdentifier) {
103445
104572
  if (preparationAttempts >= TimebackAssessmentRuntimeService.ITEM_SUBMISSION_MAX_PREPARATION_ATTEMPTS) {
@@ -103473,14 +104600,17 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
103473
104600
  }
103474
104601
  const submittedAt = new Date().toISOString();
103475
104602
  const responseVersion = metadata2.responseVersion + 1;
104603
+ const routingSnapshot = this.diagnosticRoutingSnapshot(metadata2, advanced.state);
104604
+ const routingSnapshots = metadata2.diagnostic.routingSnapshots ? [...metadata2.diagnostic.routingSnapshots, routingSnapshot] : undefined;
103476
104605
  const submission = {
103477
104606
  submissionId: input.submissionId,
103478
104607
  itemIdentifier: input.itemIdentifier,
103479
104608
  submittedAt,
103480
104609
  responseVersion,
103481
- answered: true,
104610
+ answered: Object.keys(responses[input.itemIdentifier] ?? {}).length > 0,
103482
104611
  score: scoring.score,
103483
- isCorrect: scoring.isCorrect
104612
+ isCorrect: scoring.isCorrect,
104613
+ grading: scoring.grading
103484
104614
  };
103485
104615
  const nextMetadata = {
103486
104616
  ...metadata2,
@@ -103491,9 +104621,16 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
103491
104621
  diagnostic: {
103492
104622
  ...metadata2.diagnostic,
103493
104623
  ledger: [...metadata2.diagnostic.ledger, advanced.ledgerEntry],
104624
+ ...routingSnapshots ? { routingSnapshots } : {},
103494
104625
  state: advanced.state
103495
104626
  }
103496
104627
  };
104628
+ const child = await this.prepareDiagnosticAssessmentChildResult({
104629
+ attempt,
104630
+ metadata: nextMetadata,
104631
+ submission,
104632
+ transition: advanced.ledgerEntry
104633
+ });
103497
104634
  await this.putResultMetadata(attempt.result, nextMetadata);
103498
104635
  return {
103499
104636
  action: "complete",
@@ -103501,9 +104638,10 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
103501
104638
  attemptId: attempt.result.sourcedId,
103502
104639
  responseVersion,
103503
104640
  status: "in_progress",
103504
- routing: this.diagnosticRoutingSnapshot(nextMetadata, advanced.state),
103505
- submission: this.diagnosticReceipt(submission, advanced.ledgerEntry)
104641
+ routing: routingSnapshot,
104642
+ submission: diagnosticReceipt(submission, advanced.ledgerEntry)
103506
104643
  },
104644
+ checkpoint: child,
103507
104645
  projection: {
103508
104646
  result: attempt.result,
103509
104647
  metadata: nextMetadata,
@@ -103516,21 +104654,11 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
103516
104654
  preparationAttempts += 1;
103517
104655
  preview = await this.scoreDiagnosticItemSubmission(params, input);
103518
104656
  } else {
103519
- await this.projectDiagnosticItemResponse(committed.projection, preview?.assessment);
104657
+ this.projectDiagnosticItemResponse(committed.projection, preview?.assessment, committed.checkpoint);
103520
104658
  return committed.response;
103521
104659
  }
103522
104660
  }
103523
104661
  }
103524
- diagnosticReceipt(submission, ledgerEntry) {
103525
- return {
103526
- submissionId: submission.submissionId,
103527
- routingNodeKey: ledgerEntry.routingNodeKey,
103528
- itemIdentifier: submission.itemIdentifier,
103529
- submittedAt: submission.submittedAt,
103530
- responseVersion: submission.responseVersion,
103531
- answered: submission.answered
103532
- };
103533
- }
103534
104662
  async prepareDiagnosticItemSubmission(params, input) {
103535
104663
  try {
103536
104664
  return await this.scoreDiagnosticItemSubmission(params, input);
@@ -103550,22 +104678,24 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
103550
104678
  }
103551
104679
  const prior = attempt.metadata.itemSubmissions.find((submission) => submission.submissionId === input.submissionId);
103552
104680
  if (prior || !this.isOpen(attempt.result)) {
103553
- return {
103554
- responseVersion: attempt.metadata.responseVersion,
103555
- submissionId: input.submissionId,
103556
- routingNodeKey: input.routingNodeKey,
103557
- itemIdentifier: input.itemIdentifier,
103558
- assessment: await this.loadAttemptAssessment(attempt.metadata),
103559
- scoring: null
103560
- };
104681
+ return null;
103561
104682
  }
103562
104683
  const assessment = await this.loadAttemptAssessment(attempt.metadata);
103563
- const loaded = await this.loadAttemptDiagnosticRouting(attempt.metadata, assessment);
104684
+ const artifactIdentity = assessmentArtifactIdentityFromAttempt(attempt.metadata);
104685
+ const scoringArtifact = await this.artifactLoader.loadMatchedScoring(assessment, artifactIdentity);
104686
+ const loaded = await this.loadAttemptDiagnosticRouting(attempt.metadata, assessment, this.deps.db, scoringArtifact);
103564
104687
  const expected = loaded.state.next;
103565
104688
  let scoring = null;
103566
104689
  if (loaded.state.status === "in-progress" && expected?.nodeKey === input.routingNodeKey && expected.itemIdentifier === input.itemIdentifier && attempt.metadata.responseVersion === input.expectedResponseVersion) {
103567
104690
  const responses = prepareDiagnosticAssessmentResponses(assessment, attempt.metadata.responses, input);
103568
- scoring = await this.scoreItem(assessment, responses, input.itemIdentifier, assessmentArtifactIdentityFromAttempt(attempt.metadata));
104691
+ scoring = await this.scoreItem({
104692
+ assessment,
104693
+ responses,
104694
+ itemIdentifier: input.itemIdentifier,
104695
+ artifactIdentity,
104696
+ purpose: attempt.metadata.purpose,
104697
+ preparedArtifact: scoringArtifact
104698
+ });
103569
104699
  }
103570
104700
  return {
103571
104701
  responseVersion: attempt.metadata.responseVersion,
@@ -103576,39 +104706,27 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
103576
104706
  scoring
103577
104707
  };
103578
104708
  }
103579
- async scoreItems(assessment, responses, artifactIdentity, itemIdentifiers) {
103580
- await this.artifactLoader.loadMatchedScoring(assessment, artifactIdentity);
103581
- const client2 = this.requireClient();
103582
- const scoringItems = assessmentScoringRequests(assessment, responses).filter((item) => !itemIdentifiers || itemIdentifiers.has(item.itemIdentifier));
103583
- const scoringTasks = scoringItems.flatMap((item, itemIndex) => item.requests.map((request) => ({
103584
- itemIndex,
103585
- itemIdentifier: item.itemIdentifier,
103586
- request
103587
- })));
103588
- const interactionResults = await runWithConcurrency(scoringTasks, TimebackAssessmentRuntimeService.SCORING_CONCURRENCY, async ({ itemIdentifier, request }) => client2.qtiApi.assessmentItems.processResponse(itemIdentifier, request));
103589
- const scoresByItem = scoringItems.map(() => []);
103590
- const verdictsByItem = scoringItems.map(() => []);
103591
- scoringTasks.forEach((task, taskIndex) => {
103592
- const processed = interactionResults[taskIndex];
103593
- scoresByItem[task.itemIndex].push(Number.isFinite(processed.score) ? processed.score : 0);
103594
- verdictsByItem[task.itemIndex].push(processed.isCorrect);
103595
- });
103596
- return scoringItems.map((item, itemIndex) => {
103597
- const earned = assessmentItemEarnedScore(scoresByItem[itemIndex], item.maxScore);
103598
- const possible = item.maxScore;
103599
- return {
103600
- itemIdentifier: item.itemIdentifier,
103601
- score: {
103602
- earned,
103603
- possible,
103604
- normalized: possible === 0 ? 0 : earned / possible
103605
- },
103606
- isCorrect: assessmentItemCorrectness(verdictsByItem[itemIndex])
103607
- };
104709
+ async scoreItems({
104710
+ preparedArtifact,
104711
+ ...input
104712
+ }) {
104713
+ const artifact = preparedArtifact === undefined ? await this.artifactLoader.loadMatchedScoring(input.assessment, input.artifactIdentity) : preparedArtifact;
104714
+ return scoreAssessmentItems({
104715
+ ...input,
104716
+ artifact,
104717
+ processResponse: (identifier, request) => this.requireClient().qtiApi.assessmentItems.processResponse(identifier, request),
104718
+ onEvent: addEvent
103608
104719
  });
103609
104720
  }
103610
- async scoreItem(assessment, responses, itemIdentifier, artifactIdentity) {
103611
- const [scoring] = await this.scoreItems(assessment, responses, artifactIdentity, new Set([itemIdentifier]));
104721
+ async scoreItem(input) {
104722
+ const [scoring] = await this.scoreItems({
104723
+ assessment: input.assessment,
104724
+ responses: input.responses,
104725
+ artifactIdentity: input.artifactIdentity,
104726
+ purpose: input.purpose,
104727
+ itemIdentifiers: new Set([input.itemIdentifier]),
104728
+ preparedArtifact: input.preparedArtifact
104729
+ });
103612
104730
  return scoring;
103613
104731
  }
103614
104732
  async prepareItemSubmission(params, input) {
@@ -103625,7 +104743,8 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
103625
104743
  }
103626
104744
  async scoreItemSubmission(params, input) {
103627
104745
  const attempt = await this.peekAttempt(params);
103628
- if (!this.isOpen(attempt.result)) {
104746
+ const prior = attempt.metadata.itemSubmissions.find((submission) => submission.submissionId === input.submissionId);
104747
+ if (prior || !this.isOpen(attempt.result)) {
103629
104748
  return null;
103630
104749
  }
103631
104750
  const assessment = await this.loadAttemptAssessment(attempt.metadata);
@@ -103636,7 +104755,13 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
103636
104755
  itemSubmissions: attempt.metadata.itemSubmissions,
103637
104756
  assessment
103638
104757
  }, input);
103639
- const scoring = transition.action === "commit" ? await this.scoreItem(assessment, transition.responses, input.itemIdentifier, assessmentArtifactIdentityFromAttempt(attempt.metadata)) : null;
104758
+ const scoring = transition.action === "commit" ? await this.scoreItem({
104759
+ assessment,
104760
+ responses: transition.responses,
104761
+ itemIdentifier: input.itemIdentifier,
104762
+ artifactIdentity: assessmentArtifactIdentityFromAttempt(attempt.metadata),
104763
+ purpose: attempt.metadata.purpose
104764
+ }) : null;
103640
104765
  return {
103641
104766
  responseVersion: attempt.metadata.responseVersion,
103642
104767
  submissionId: input.submissionId,
@@ -103648,8 +104773,12 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
103648
104773
  async scoreSubmission(attempt) {
103649
104774
  const assessment = await this.loadAttemptAssessment(attempt.metadata);
103650
104775
  validateAssessmentResponses(assessment, attempt.metadata.responses);
103651
- const administeredItems = attempt.metadata.purpose === "review" ? administeredItemIdentifiers(attempt.metadata.itemSubmissions) : undefined;
103652
- const itemResults = await this.scoreItems(assessment, attempt.metadata.responses, assessmentArtifactIdentityFromAttempt(attempt.metadata), administeredItems);
104776
+ const itemResults = attempt.metadata.purpose === "review" ? committedReviewItemResults(attempt.metadata, assessment) : await this.scoreItems({
104777
+ assessment,
104778
+ responses: attempt.metadata.responses,
104779
+ artifactIdentity: assessmentArtifactIdentityFromAttempt(attempt.metadata),
104780
+ purpose: attempt.metadata.purpose
104781
+ });
103653
104782
  return {
103654
104783
  responseVersion: attempt.metadata.responseVersion,
103655
104784
  assessment,
@@ -103658,7 +104787,10 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
103658
104787
  correctQuestions: countCorrectQuestions(itemResults.map((result) => result.isCorrect))
103659
104788
  };
103660
104789
  }
103661
- async peekAttempt({ studentId, attemptId }) {
104790
+ async peekAttempt({
104791
+ studentId,
104792
+ attemptId
104793
+ }) {
103662
104794
  const result = await this.requireClient().api.oneroster.assessmentResults.get(attemptId).catch((error88) => {
103663
104795
  if (isApiError(error88) && error88.statusCode === 404) {
103664
104796
  throw this.unauthorizedAttempt(attemptId);
@@ -103703,21 +104835,19 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
103703
104835
  await this.deps.validateDeveloperAccess(params.user, params.gameId);
103704
104836
  const preview = await this.prepareSubmission(params, input);
103705
104837
  const submission = await this.deps.assessmentRuntimeLock(this.deps.db, [assessmentRuntimeAttemptLockKey(params.attemptId)], async (db2) => {
103706
- const attempt = await this.authorizeAttempt(params, {
103707
- db: db2,
103708
- developerAccessValidated: true
103709
- });
104838
+ const { attempt: identity, enrollment } = await this.resolveAttemptAuthorization(params, { db: db2, developerAccessValidated: true });
103710
104839
  const submissionDisposition = classifyAssessmentSubmission({
103711
- inProgress: attempt.result.inProgress ?? "",
103712
- scoreStatus: attempt.result.scoreStatus,
103713
- submissionId: attempt.metadata.submissionId
104840
+ inProgress: identity.result.inProgress ?? "",
104841
+ scoreStatus: identity.result.scoreStatus,
104842
+ submissionId: identity.metadata.submissionId
103714
104843
  }, input.submissionId);
103715
104844
  if (submissionDisposition === "replay") {
103716
104845
  return {
103717
- response: this.submittedResult(attempt.result, attempt.metadata),
103718
- emission: this.replayCompletion(attempt, input.submissionId, params, game2)
104846
+ response: this.submittedResult(identity.result, identity.metadata),
104847
+ emission: this.replayCompletion(identity, input.submissionId, params, game2)
103719
104848
  };
103720
104849
  }
104850
+ const attempt = this.requireActiveAttempt({ attempt: identity, enrollment }, params.attemptId);
103721
104851
  if (submissionDisposition === "reject") {
103722
104852
  throw AssessmentRuntimeError.from(assessmentAlreadySubmitted(attempt.result.sourcedId));
103723
104853
  }
@@ -103745,12 +104875,9 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
103745
104875
  const { assessment, itemResults, score, correctQuestions } = scored;
103746
104876
  const client2 = this.requireClient();
103747
104877
  const timestamp6 = new Date().toISOString();
103748
- const reviewOutcomes = attempt.metadata.purpose === "review" ? await this.finalizeReviewChildResults({
103749
- result: attempt.result,
104878
+ const reviewOutcomes = attempt.metadata.purpose === "review" ? this.reviewItemOutcomes({
103750
104879
  metadata: attempt.metadata,
103751
- itemResults,
103752
- submissionId: input.submissionId,
103753
- timestamp: timestamp6
104880
+ itemResults
103754
104881
  }) : null;
103755
104882
  const finalization = buildAssessmentResultSubmission({
103756
104883
  result: attempt.result,
@@ -103813,9 +104940,11 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
103813
104940
  ...submission.emission.attempt,
103814
104941
  metadata: submission.emission.attempt.metadata
103815
104942
  } : null;
104943
+ const { result, metadata: metadata2 } = submission.emission.attempt;
103816
104944
  await Promise.allSettled([
103817
104945
  this.emitSessionBestEffort(submission.emission),
103818
- ...diagnosticCompletion ? [this.restoreDiagnosticChildResults(diagnosticCompletion)] : []
104946
+ ...diagnosticCompletion ? [this.restoreDiagnosticChildResults(diagnosticCompletion)] : [],
104947
+ ...metadata2.purpose === "review" ? [this.restoreAllCompletedReviewChildResponses(result, metadata2)] : []
103819
104948
  ]);
103820
104949
  }
103821
104950
  return submission.response;
@@ -103965,17 +105094,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
103965
105094
  async finalizeDiagnosticSubmission(input) {
103966
105095
  const { attempt } = input;
103967
105096
  const routing = await this.loadAttemptDiagnosticManifest(attempt.metadata, input.db);
103968
- const state = this.replayAttemptDiagnosticRouting(attempt.metadata, routing.manifest);
103969
- const tracks = diagnosticTrackResults(state);
103970
- if (!tracks) {
103971
- throw AssessmentRuntimeError.from(assessmentFlowViolation("Diagnostic routing must be ready to complete before final submit.", { routingStatus: state.status }));
103972
- }
103973
- if (attempt.metadata.itemSubmissions.length === 0) {
103974
- throw AssessmentRuntimeError.from(assessmentFlowViolation("A diagnostic cannot be finalized without an administered item."));
103975
- }
103976
- const possible = attempt.metadata.itemSubmissions.reduce((total, submission) => total + submission.score.possible, 0);
103977
- const score = aggregateAssessmentScore(attempt.metadata.itemSubmissions.map((submission) => submission.score.earned), possible);
103978
- const correctQuestions = countCorrectQuestions(attempt.metadata.itemSubmissions.map((submission) => submission.isCorrect));
105097
+ const { tracks, score, correctQuestions } = summarizeDiagnosticSubmission(attempt.metadata, routing.manifest);
103979
105098
  const timestamp6 = new Date().toISOString();
103980
105099
  const finalization = buildAssessmentResultSubmission({
103981
105100
  result: attempt.result,
@@ -104063,10 +105182,13 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
104063
105182
  }) : undefined;
104064
105183
  const diagnostic = definition?.diagnosticKey ? await this.initializeHostedDiagnostic(definition, loaded.assessment) : null;
104065
105184
  const reviewBank = purpose === "review" ? await buildReviewBankIndex(loaded.assessment, loaded.questions, { customOnly: true }) : null;
105185
+ const scoring = compileQtiScoringArtifact(loaded.questions);
105186
+ const fullyPlatformScored = scoring.items.every((item) => item.strategy === "platform");
104066
105187
  return {
104067
105188
  ...test,
104068
105189
  live: true,
104069
105190
  assessment: loaded.assessment,
105191
+ ...fullyPlatformScored ? { scoring } : {},
104070
105192
  ...diagnostic ? {
104071
105193
  diagnostic: {
104072
105194
  diagnosticKey: diagnostic.definition.diagnosticKey,
@@ -104411,21 +105533,31 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
104411
105533
  if (resumed.metadata.purpose !== "review") {
104412
105534
  throw new Error("A standards-review request selected a non-review attempt");
104413
105535
  }
104414
- if (this.isSettled(resumed.result)) {
104415
- return this.snapshotForResult(resumed.result, resumed.metadata, context2.integration);
104416
- }
104417
- const source = await this.loadPinnedReviewBankSource(resumed.metadata);
104418
- const assessment = projectReviewAssessment(source.assessment, source.bank, resumed.metadata.review.selections);
104419
- await this.ensureReviewChildResults({
104420
- result: resumed.result,
104421
- metadata: resumed.metadata,
104422
- integration: context2.integration,
104423
- source,
104424
- assessment,
104425
- existingChildResults,
104426
- lookupMissingResults: true
105536
+ const reviewMetadata = resumed.metadata;
105537
+ let assessment;
105538
+ const current = await reconcileReviewChildResults({
105539
+ initial: resumed,
105540
+ writeOpen: async () => {
105541
+ const source = await this.loadPinnedReviewBankSource(reviewMetadata);
105542
+ assessment = projectReviewAssessment(source.assessment, source.bank, reviewMetadata.review.selections);
105543
+ await this.ensureReviewChildResults({
105544
+ result: resumed.result,
105545
+ metadata: reviewMetadata,
105546
+ integration: context2.integration,
105547
+ source,
105548
+ assessment,
105549
+ existingChildResults,
105550
+ lookupMissingResults: true
105551
+ });
105552
+ },
105553
+ crossBarrier: () => crossAssessmentAttemptLockBarrier(this.deps.assessmentRuntimeLock, this.deps.db, resumed.result.sourcedId),
105554
+ readLatest: () => this.peekAttempt({
105555
+ studentId: resumed.result.student.sourcedId,
105556
+ attemptId: resumed.result.sourcedId
105557
+ }),
105558
+ restoreTerminal: (latest) => this.restoreAllCompletedReviewChildResponses(latest.result, latest.metadata)
104427
105559
  });
104428
- return this.snapshot(resumed.result, resumed.metadata, assessment, context2.integration);
105560
+ return assessmentChildProjectionIsTerminal(current.result) ? this.snapshotForResult(current.result, current.metadata, context2.integration) : this.snapshot(current.result, current.metadata, assessment, context2.integration);
104429
105561
  }
104430
105562
  recordReviewPreparationPhase(phase, startedAt, counts = {}) {
104431
105563
  addEvent("assessment.review_preparation_phase", {
@@ -104647,20 +105779,9 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
104647
105779
  }
104648
105780
  async initializeHostedDiagnostic(definition, assessment) {
104649
105781
  const initialized = await this.initializeHostedDiagnosticManifest(definition);
104650
- this.assertDiagnosticItemsAvailable(definition, initialized.manifest, assessment);
105782
+ assertDiagnosticItemsAvailable(definition.id, initialized.manifest, assessment);
104651
105783
  return initialized;
104652
105784
  }
104653
- assertDiagnosticItemsAvailable(definition, manifest, assessment) {
104654
- const itemIdentifiers = new Set(assessment.items.map((item) => item.identifier));
104655
- const unknownItem = manifest.nodes.find((node) => !itemIdentifiers.has(node.itemIdentifier));
104656
- if (unknownItem) {
104657
- throw new AssessmentRuntimeError("SELECTED_TEST_VERSION_UNAVAILABLE", `Diagnostic routing references unavailable item ${unknownItem.itemIdentifier}.`, {
104658
- definitionId: definition.id,
104659
- nodeKey: unknownItem.key,
104660
- itemIdentifier: unknownItem.itemIdentifier
104661
- });
104662
- }
104663
- }
104664
105785
  async initializeHostedDiagnosticManifest(definition) {
104665
105786
  const initialized = initializeDiagnosticRouting(definition.diagnosticRoutingManifest);
104666
105787
  if (!initialized.ok) {
@@ -104684,21 +105805,32 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
104684
105805
  }
104685
105806
  return initialized;
104686
105807
  }
104687
- replayAttemptDiagnosticRouting(metadata2, manifest) {
104688
- const replayed = replayDiagnosticRouting(manifest, metadata2.diagnostic.ledger);
104689
- if (!replayed.ok || canonicalJson2(replayed.state) !== canonicalJson2(metadata2.diagnostic.state)) {
104690
- throw AssessmentRuntimeError.from(assessmentFlowViolation("The persisted diagnostic routing state is corrupt.", {
104691
- definitionId: metadata2.diagnostic.definitionId,
104692
- ...replayed.ok ? {} : { failure: replayed.failure }
104693
- }));
104694
- }
104695
- return replayed.state;
104696
- }
104697
- async loadAttemptDiagnosticRouting(metadata2, assessment, db2 = this.deps.db) {
105808
+ async loadAttemptDiagnosticRouting(metadata2, assessment, db2 = this.deps.db, preparedArtifact) {
105809
+ const startedAt = Date.now();
105810
+ const artifactIdentity = assessmentArtifactIdentityFromAttempt(metadata2);
105811
+ const artifact = preparedArtifact === undefined ? await this.artifactLoader.loadMatchedScoring(assessment, artifactIdentity) : preparedArtifact;
105812
+ const compiled = await resolveCompiledDiagnosticRouting(metadata2, assessment, artifact);
105813
+ if (compiled) {
105814
+ addEvent("assessment.diagnostic_routing_load", {
105815
+ "app.assessment.diagnostic_definition_id": metadata2.diagnostic.definitionId,
105816
+ "app.assessment.routing_source": "platform-artifact",
105817
+ "app.assessment.routing_duration_ms": Date.now() - startedAt
105818
+ });
105819
+ return compiled;
105820
+ }
105821
+ addEvent("assessment.diagnostic_routing_fallback", {
105822
+ "app.assessment.diagnostic_definition_id": metadata2.diagnostic.definitionId,
105823
+ "app.assessment.fallback_reason": diagnosticRoutingFallbackReason(artifact, metadata2.diagnostic.routingRevision),
105824
+ "app.assessment.fallback_duration_ms": Date.now() - startedAt
105825
+ });
104698
105826
  const pinned = await this.loadAttemptDiagnosticManifest(metadata2, db2);
104699
- this.assertDiagnosticItemsAvailable(pinned.definition, pinned.manifest, assessment);
104700
- const state = this.replayAttemptDiagnosticRouting(metadata2, pinned.manifest);
104701
- return { ...pinned, state };
105827
+ assertDiagnosticItemsAvailable(pinned.definition.id, pinned.manifest, assessment);
105828
+ const state = replayAttemptDiagnosticRouting(metadata2, pinned.manifest);
105829
+ return {
105830
+ manifest: pinned.manifest,
105831
+ routingRevision: pinned.routingRevision,
105832
+ state
105833
+ };
104702
105834
  }
104703
105835
  async loadAssessmentSource(identifier, expectedRevision) {
104704
105836
  const client2 = this.requireClient();
@@ -104833,11 +105965,167 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
104833
105965
  }
104834
105966
  return lineItemId;
104835
105967
  }
105968
+ assessmentChildAttempt(attempt) {
105969
+ const { metadata: metadata2 } = attempt;
105970
+ const assessmentKey = metadata2.selectedTest.assessmentKey ?? assessmentKeyFromManagedQtiIdentifier(metadata2.selectedTest.identifier);
105971
+ if (!assessmentKey) {
105972
+ addEvent("assessment.child_result_legacy_attempt_skipped", {
105973
+ "app.assessment.attempt_id": attempt.result.sourcedId
105974
+ });
105975
+ return null;
105976
+ }
105977
+ return {
105978
+ attemptId: attempt.result.sourcedId,
105979
+ gameId: attempt.integration.gameId,
105980
+ studentId: attempt.result.student.sourcedId,
105981
+ activityId: metadata2.activityId,
105982
+ courseId: metadata2.courseId,
105983
+ integrationId: metadata2.integrationId,
105984
+ enrollmentId: metadata2.enrollmentId,
105985
+ selectedTest: {
105986
+ assessmentKey,
105987
+ identifier: metadata2.selectedTest.identifier,
105988
+ contentRevision: metadata2.selectedTest.contentRevision
105989
+ }
105990
+ };
105991
+ }
105992
+ reviewChildResultContext(metadata2, submission) {
105993
+ const selection = metadata2.review.selections.find((candidate) => candidate.itemIdentifier === submission.itemIdentifier);
105994
+ if (!selection) {
105995
+ throw new Error(`Committed review item ${submission.itemIdentifier} is not in its pinned selection`);
105996
+ }
105997
+ return {
105998
+ kind: "review",
105999
+ bankRevision: metadata2.review.bankRevision,
106000
+ standard: selection.standard
106001
+ };
106002
+ }
106003
+ prepareReviewAssessmentChildResult(input) {
106004
+ return this.prepareAssessmentChildResult({
106005
+ attempt: input.attempt,
106006
+ metadata: input.metadata,
106007
+ submission: input.submission,
106008
+ context: () => this.reviewChildResultContext(input.metadata, input.submission)
106009
+ });
106010
+ }
106011
+ prepareDiagnosticAssessmentChildResult(input) {
106012
+ return this.prepareAssessmentChildResult({
106013
+ attempt: input.attempt,
106014
+ metadata: input.metadata,
106015
+ submission: input.submission,
106016
+ context: () => ({
106017
+ kind: "platform-routed-diagnostic",
106018
+ definitionId: input.metadata.diagnostic.definitionId,
106019
+ diagnosticKey: input.metadata.diagnostic.diagnosticKey,
106020
+ routingRevision: input.metadata.diagnostic.routingRevision,
106021
+ transition: input.transition
106022
+ })
106023
+ });
106024
+ }
106025
+ async prepareAssessmentChildResult(input) {
106026
+ try {
106027
+ const attemptContext = this.assessmentChildAttempt({
106028
+ ...input.attempt,
106029
+ metadata: input.metadata
106030
+ });
106031
+ if (!attemptContext) {
106032
+ return null;
106033
+ }
106034
+ const parentLineItemId = input.attempt.result.assessmentLineItem.sourcedId;
106035
+ const { itemIdentifier } = input.submission;
106036
+ const context2 = input.context();
106037
+ const [command, childLineItemId] = await Promise.all([
106038
+ buildSubmissionChildResultCommand({
106039
+ attempt: attemptContext,
106040
+ submission: input.submission,
106041
+ responses: input.metadata.responses[itemIdentifier],
106042
+ grading: input.submission.grading ?? QTI_CHILD_GRADING,
106043
+ context: context2
106044
+ }),
106045
+ context2.kind === "review" ? reviewQuestionLineItemId(parentLineItemId, itemIdentifier) : diagnosticQuestionLineItemId(parentLineItemId, itemIdentifier)
106046
+ ]);
106047
+ return { command, childLineItemId };
106048
+ } catch (error88) {
106049
+ addEvent("assessment.child_result_prepare_failed", {
106050
+ "app.assessment.attempt_id": input.attempt.result.sourcedId,
106051
+ "app.assessment.submission_id": input.submission.submissionId,
106052
+ "app.assessment.qti_item_identifier": input.submission.itemIdentifier,
106053
+ "exception.type": errorType(error88),
106054
+ "app.error.message": errorMessage(error88)
106055
+ });
106056
+ return null;
106057
+ }
106058
+ }
106059
+ async projectPreparedAssessmentChildResult(prepared) {
106060
+ if (!prepared) {
106061
+ return;
106062
+ }
106063
+ const startedAt = Date.now();
106064
+ const identity = {
106065
+ "app.assessment.attempt_id": prepared.command.payload.attempt.attemptId,
106066
+ "app.assessment.submission_id": prepared.command.payload.administration.submissionId
106067
+ };
106068
+ try {
106069
+ const disposition = await persistAssessmentChildResult({
106070
+ assessmentResults: this.requireClient().api.oneroster.assessmentResults,
106071
+ command: prepared.command,
106072
+ childLineItemId: prepared.childLineItemId
106073
+ });
106074
+ if (disposition.outcome === "conflict") {
106075
+ addEvent("assessment.child_result_conflict", {
106076
+ ...identity,
106077
+ "app.assessment.child_result_id": disposition.resultId,
106078
+ "app.assessment.child_result_conflict_reason": disposition.reason
106079
+ });
106080
+ return;
106081
+ }
106082
+ addEvent("assessment.child_result_persisted", {
106083
+ ...identity,
106084
+ "app.assessment.child_result_id": disposition.resultId,
106085
+ "app.assessment.child_result_outcome": disposition.outcome,
106086
+ "app.assessment.duration_ms": Date.now() - startedAt
106087
+ });
106088
+ } catch (error88) {
106089
+ addEvent("assessment.child_result_persist_failed", {
106090
+ ...identity,
106091
+ "app.assessment.duration_ms": Date.now() - startedAt,
106092
+ "exception.type": errorType(error88),
106093
+ "app.error.message": errorMessage(error88)
106094
+ });
106095
+ }
106096
+ }
106097
+ assertAssessmentChildLineItem(lineItemId, stored, expected) {
106098
+ if (assessmentChildLineItemMatches(stored, expected)) {
106099
+ return;
106100
+ }
106101
+ addEvent("assessment.question_line_item_conflict", {
106102
+ "app.assessment.line_item_id": lineItemId
106103
+ });
106104
+ throw AssessmentRuntimeError.from(assessmentFlowViolation("A deterministic assessment question line item is occupied by incompatible data.", { lineItemId }));
106105
+ }
104836
106106
  async ensureDiagnosticQuestionLineItem(input) {
104837
106107
  const client2 = this.requireClient();
104838
106108
  const lineItemId = await diagnosticQuestionLineItemId(input.parentLineItemId, input.item.identifier);
106109
+ const expected = {
106110
+ status: ONEROSTER_STATUS2.active,
106111
+ parentAssessmentLineItem: { sourcedId: input.parentLineItemId },
106112
+ course: { sourcedId: input.integration.courseId },
106113
+ resultValueMin: 0,
106114
+ resultValueMax: input.item.maxScore,
106115
+ metadata: {
106116
+ playcademyDiagnosticQuestionDefinition: {
106117
+ version: 1,
106118
+ definitionId: input.metadata.diagnostic.definitionId,
106119
+ diagnosticKey: input.metadata.diagnostic.diagnosticKey,
106120
+ routingRevision: input.metadata.diagnostic.routingRevision,
106121
+ contentRevision: input.metadata.selectedTest.contentRevision,
106122
+ itemIdentifier: input.item.identifier
106123
+ }
106124
+ }
106125
+ };
104839
106126
  try {
104840
- await client2.api.oneroster.assessmentLineItems.get(lineItemId);
106127
+ const stored = await client2.api.oneroster.assessmentLineItems.get(lineItemId);
106128
+ this.assertAssessmentChildLineItem(lineItemId, stored, expected);
104841
106129
  return lineItemId;
104842
106130
  } catch (error88) {
104843
106131
  if (!isApiError(error88) || error88.statusCode !== 404) {
@@ -104847,35 +106135,23 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
104847
106135
  try {
104848
106136
  await client2.api.oneroster.assessmentLineItems.create({
104849
106137
  sourcedId: lineItemId,
104850
- status: ONEROSTER_STATUS2.active,
104851
106138
  title: input.item.title,
104852
106139
  description: "adaptive diagnostic question",
104853
- parentAssessmentLineItem: { sourcedId: input.parentLineItemId },
104854
- course: { sourcedId: input.integration.courseId },
104855
- resultValueMin: 0,
104856
- resultValueMax: input.item.maxScore,
104857
- metadata: {
104858
- playcademyDiagnosticQuestionDefinition: {
104859
- version: 1,
104860
- definitionId: input.metadata.diagnostic.definitionId,
104861
- diagnosticKey: input.metadata.diagnostic.diagnosticKey,
104862
- routingRevision: input.metadata.diagnostic.routingRevision,
104863
- contentRevision: input.metadata.selectedTest.contentRevision,
104864
- itemIdentifier: input.item.identifier
104865
- }
104866
- }
106140
+ ...expected
104867
106141
  });
104868
106142
  } catch (error88) {
104869
106143
  if (!isApiError(error88) || error88.statusCode !== 409) {
104870
106144
  throw error88;
104871
106145
  }
106146
+ const stored = await client2.api.oneroster.assessmentLineItems.get(lineItemId);
106147
+ this.assertAssessmentChildLineItem(lineItemId, stored, expected);
104872
106148
  addEvent("assessment.diagnostic_question_line_item_create_conflict", {
104873
106149
  "app.assessment.line_item_id": lineItemId
104874
106150
  });
104875
106151
  }
104876
106152
  return lineItemId;
104877
106153
  }
104878
- async projectDiagnosticItemResponse(projection, assessment) {
106154
+ async projectDiagnosticItemResponse(projection, assessment, checkpoint = null) {
104879
106155
  try {
104880
106156
  const loadedAssessment = assessment ?? await this.loadAttemptAssessment(projection.metadata);
104881
106157
  const item = loadedAssessment.items.find((candidate) => candidate.identifier === projection.ledgerEntry.itemIdentifier);
@@ -104888,6 +106164,9 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
104888
106164
  metadata: projection.metadata,
104889
106165
  item
104890
106166
  });
106167
+ if (checkpoint && checkpoint.childLineItemId !== childLineItemId) {
106168
+ throw new Error("Prepared diagnostic checkpoint resolved a different line item");
106169
+ }
104891
106170
  const childResultId = await diagnosticQuestionResultId(projection.result.sourcedId, childLineItemId);
104892
106171
  const finalized = buildFinalizedDiagnosticChildResult({
104893
106172
  childLineItemId,
@@ -104896,7 +106175,10 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
104896
106175
  metadata: projection.metadata,
104897
106176
  ledgerEntry: projection.ledgerEntry
104898
106177
  });
104899
- await this.requireClient().api.oneroster.assessmentResults.upsert(childResultId, finalized.resultUpdate);
106178
+ await Promise.all([
106179
+ this.projectPreparedAssessmentChildResult(checkpoint),
106180
+ this.requireClient().api.oneroster.assessmentResults.upsert(childResultId, finalized.resultUpdate)
106181
+ ]);
104900
106182
  } catch (error88) {
104901
106183
  addEvent("assessment.diagnostic_child_projection_failed", {
104902
106184
  "app.assessment.attempt_id": projection.result.sourcedId,
@@ -104957,41 +106239,43 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
104957
106239
  async ensureReviewQuestionLineItem(input) {
104958
106240
  const client2 = this.requireClient();
104959
106241
  const lineItemId = await reviewQuestionLineItemId(input.parentLineItemId, input.item.identifier);
106242
+ const expected = {
106243
+ status: ONEROSTER_STATUS2.active,
106244
+ parentAssessmentLineItem: { sourcedId: input.parentLineItemId },
106245
+ course: { sourcedId: input.integration.courseId },
106246
+ resultValueMin: 0,
106247
+ resultValueMax: input.item.maxScore,
106248
+ metadata: {
106249
+ playcademyReviewQuestionDefinition: {
106250
+ version: 1,
106251
+ bankRevision: input.bankRevision,
106252
+ itemIdentifier: input.item.identifier,
106253
+ standards: input.standards
106254
+ }
106255
+ }
106256
+ };
104960
106257
  try {
104961
- await client2.api.oneroster.assessmentLineItems.get(lineItemId);
106258
+ const stored = await client2.api.oneroster.assessmentLineItems.get(lineItemId);
106259
+ this.assertAssessmentChildLineItem(lineItemId, stored, expected);
104962
106260
  return lineItemId;
104963
106261
  } catch (error88) {
104964
106262
  if (!isApiError(error88) || error88.statusCode !== 404) {
104965
106263
  throw error88;
104966
106264
  }
104967
106265
  }
104968
- const indexed = input.bank.items.find((candidate) => candidate.itemIdentifier === input.item.identifier);
104969
- if (!indexed) {
104970
- throw new Error(`Review-bank index is missing item ${input.item.identifier}`);
104971
- }
104972
106266
  try {
104973
106267
  await client2.api.oneroster.assessmentLineItems.create({
104974
106268
  sourcedId: lineItemId,
104975
- status: ONEROSTER_STATUS2.active,
104976
106269
  title: input.item.title,
104977
106270
  description: "standards review question",
104978
- parentAssessmentLineItem: { sourcedId: input.parentLineItemId },
104979
- course: { sourcedId: input.integration.courseId },
104980
- resultValueMin: 0,
104981
- resultValueMax: input.item.maxScore,
104982
- metadata: {
104983
- playcademyReviewQuestionDefinition: {
104984
- version: 1,
104985
- bankRevision: input.bank.bankRevision,
104986
- itemIdentifier: input.item.identifier,
104987
- standards: indexed.standards
104988
- }
104989
- }
106271
+ ...expected
104990
106272
  });
104991
106273
  } catch (error88) {
104992
106274
  if (!isApiError(error88) || error88.statusCode !== 409) {
104993
106275
  throw error88;
104994
106276
  }
106277
+ const stored = await client2.api.oneroster.assessmentLineItems.get(lineItemId);
106278
+ this.assertAssessmentChildLineItem(lineItemId, stored, expected);
104995
106279
  addEvent("assessment.review_question_line_item_create_conflict", {
104996
106280
  "app.assessment.line_item_id": lineItemId
104997
106281
  });
@@ -105012,6 +106296,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
105012
106296
  }
105013
106297
  async ensureReviewChildResults(input) {
105014
106298
  const itemByIdentifier = new Map(input.assessment.items.map((item) => [item.identifier, item]));
106299
+ const bankItemByIdentifier = new Map(input.source.bank.items.map((item) => [item.itemIdentifier, item]));
105015
106300
  const physicalItems = new Set(input.metadata.review.selections.map((selection) => selection.itemIdentifier));
105016
106301
  if (physicalItems.size !== input.metadata.review.selections.length) {
105017
106302
  throw new Error("MVP review selection must use each physical item at most once");
@@ -105019,19 +106304,20 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
105019
106304
  const administeredItems = administeredItemIdentifiers(input.metadata.itemSubmissions);
105020
106305
  await runWithConcurrency(input.metadata.review.selections, TimebackAssessmentRuntimeService.SCORING_CONCURRENCY, async (selection) => {
105021
106306
  const item = itemByIdentifier.get(selection.itemIdentifier);
105022
- if (!item) {
105023
- throw new Error(`Review assessment is missing selected item ${selection.itemIdentifier}`);
106307
+ const indexed = bankItemByIdentifier.get(selection.itemIdentifier);
106308
+ if (!item || !indexed) {
106309
+ throw new Error(`Review assessment or bank is missing selected item ${selection.itemIdentifier}`);
105024
106310
  }
106311
+ const childLineItemId = await this.ensureReviewQuestionLineItem({
106312
+ parentLineItemId: input.result.assessmentLineItem.sourcedId,
106313
+ integration: input.integration,
106314
+ bankRevision: input.metadata.review.bankRevision,
106315
+ standards: [...indexed.standards],
106316
+ item
106317
+ });
105025
106318
  if (!administeredItems.has(selection.itemIdentifier)) {
105026
- await this.ensureReviewQuestionLineItem({
105027
- parentLineItemId: input.result.assessmentLineItem.sourcedId,
105028
- integration: input.integration,
105029
- bank: input.source.bank,
105030
- item
105031
- });
105032
106319
  return;
105033
106320
  }
105034
- const childLineItemId = await reviewQuestionLineItemId(input.result.assessmentLineItem.sourcedId, item.identifier);
105035
106321
  const childResultId = await reviewQuestionResultId(input.result.sourcedId, childLineItemId);
105036
106322
  const streamedChildResult = input.existingChildResults.get(childResultId);
105037
106323
  let childResult = streamedChildResult;
@@ -105054,12 +106340,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
105054
106340
  if (resolution.action === "reuse") {
105055
106341
  return;
105056
106342
  }
105057
- await this.ensureReviewQuestionLineItem({
105058
- parentLineItemId: input.result.assessmentLineItem.sourcedId,
105059
- integration: input.integration,
105060
- bank: input.source.bank,
105061
- item
105062
- });
105063
106343
  if (resolution.action === "restore") {
105064
106344
  addEvent("assessment.review_child_result_restore", {
105065
106345
  "app.assessment.review_child_restore_reason": resolution.reason,
@@ -105077,24 +106357,43 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
105077
106357
  });
105078
106358
  });
105079
106359
  }
105080
- async putReviewChildResponse(result, metadata2, itemIdentifier) {
106360
+ async putReviewChildResponse(projection, checkpoint) {
106361
+ const { result, metadata: metadata2, itemIdentifier, integration } = projection;
105081
106362
  const selection = metadata2.review.selections.find((candidate) => candidate.itemIdentifier === itemIdentifier);
105082
106363
  if (!selection) {
105083
106364
  return;
105084
106365
  }
105085
106366
  try {
105086
- const childLineItemId = await reviewQuestionLineItemId(result.assessmentLineItem.sourcedId, selection.itemIdentifier);
106367
+ const source = await this.loadPinnedReviewBankSource(metadata2);
106368
+ const item = source.assessment.items.find((candidate) => candidate.identifier === selection.itemIdentifier);
106369
+ const indexed = source.bank.items.find((candidate) => candidate.itemIdentifier === selection.itemIdentifier);
106370
+ if (!item || !indexed) {
106371
+ throw new Error(`Review assessment or bank is missing selected item ${selection.itemIdentifier}`);
106372
+ }
106373
+ const childLineItemId = await this.ensureReviewQuestionLineItem({
106374
+ parentLineItemId: result.assessmentLineItem.sourcedId,
106375
+ integration,
106376
+ bankRevision: metadata2.review.bankRevision,
106377
+ standards: [...indexed.standards],
106378
+ item
106379
+ });
106380
+ if (checkpoint && checkpoint.childLineItemId !== childLineItemId) {
106381
+ throw new Error("Prepared review checkpoint resolved a different line item");
106382
+ }
105087
106383
  const childResultId = await reviewQuestionResultId(result.sourcedId, childLineItemId);
105088
106384
  const childMetadata = buildReviewItemResultMetadata({
105089
106385
  metadata: metadata2,
105090
106386
  selection,
105091
106387
  parentAttemptId: result.sourcedId
105092
106388
  });
105093
- await this.requireClient().api.oneroster.assessmentResults.upsert(childResultId, buildOpenReviewChildResult({
105094
- childLineItemId,
105095
- student: result.student,
105096
- metadata: childMetadata
105097
- }));
106389
+ await Promise.all([
106390
+ this.projectPreparedAssessmentChildResult(checkpoint),
106391
+ this.requireClient().api.oneroster.assessmentResults.upsert(childResultId, buildOpenReviewChildResult({
106392
+ childLineItemId,
106393
+ student: result.student,
106394
+ metadata: childMetadata
106395
+ }))
106396
+ ]);
105098
106397
  } catch (error88) {
105099
106398
  addEvent("assessment.review_child_response_projection_failed", {
105100
106399
  "app.assessment.attempt_id": result.sourcedId,
@@ -105104,14 +106403,26 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
105104
106403
  });
105105
106404
  }
105106
106405
  }
105107
- async projectReviewItemResponse(params, projection) {
105108
- await this.putReviewChildResponse(projection.result, projection.metadata, projection.itemIdentifier);
106406
+ async projectReviewItemResponse(params, projection, checkpoint) {
106407
+ const terminal = assessmentChildProjectionIsTerminal(projection.result);
105109
106408
  try {
105110
- await crossAssessmentAttemptLockBarrier(this.deps.assessmentRuntimeLock, this.deps.db, params.attemptId);
105111
- const latest = await this.peekAttempt(params);
105112
- if (this.isSettled(latest.result) && latest.metadata.purpose === "review") {
105113
- await this.restoreCompletedReviewChildResponses(latest.result, latest.metadata, new Set([projection.itemIdentifier]));
105114
- }
106409
+ await reconcileReviewChildResults({
106410
+ initial: projection,
106411
+ writeOpen: () => this.putReviewChildResponse(projection, checkpoint),
106412
+ crossBarrier: () => crossAssessmentAttemptLockBarrier(this.deps.assessmentRuntimeLock, this.deps.db, params.attemptId),
106413
+ readLatest: () => this.peekAttempt(params),
106414
+ restoreTerminal: async (latest) => {
106415
+ const repair = this.restoreCompletedReviewChildResponses(latest.result, latest.metadata, new Set([projection.itemIdentifier]));
106416
+ if (terminal) {
106417
+ await Promise.allSettled([
106418
+ this.projectPreparedAssessmentChildResult(checkpoint),
106419
+ repair
106420
+ ]);
106421
+ } else {
106422
+ await repair;
106423
+ }
106424
+ }
106425
+ });
105115
106426
  } catch (error88) {
105116
106427
  addEvent("assessment.review_child_projection_barrier_failed", {
105117
106428
  "app.assessment.attempt_id": params.attemptId,
@@ -105121,6 +106432,9 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
105121
106432
  });
105122
106433
  }
105123
106434
  }
106435
+ restoreAllCompletedReviewChildResponses(result, metadata2) {
106436
+ return this.restoreCompletedReviewChildResponses(result, metadata2, new Set(metadata2.itemSubmissions.map((item) => item.itemIdentifier)));
106437
+ }
105124
106438
  async restoreCompletedReviewChildResponses(result, metadata2, changedItemIdentifiers) {
105125
106439
  const outcomes = metadata2.completion?.itemOutcomes;
105126
106440
  const submissionId = metadata2.submissionId;
@@ -105164,28 +106478,22 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
105164
106478
  }
105165
106479
  });
105166
106480
  }
105167
- async finalizeReviewChildResults(input) {
106481
+ reviewItemOutcomes(input) {
105168
106482
  const resultByItem = new Map(input.itemResults.map((itemResult) => [itemResult.itemIdentifier, itemResult]));
105169
106483
  const administeredItems = administeredItemIdentifiers(input.metadata.itemSubmissions);
105170
- return runWithConcurrency(input.metadata.review.selections.filter((selection) => administeredItems.has(selection.itemIdentifier)), TimebackAssessmentRuntimeService.SCORING_CONCURRENCY, async (selection) => {
106484
+ return input.metadata.review.selections.filter((selection) => administeredItems.has(selection.itemIdentifier)).map((selection) => {
105171
106485
  const itemResult = resultByItem.get(selection.itemIdentifier);
105172
- if (!itemResult) {
106486
+ const submission = itemAdministration(input.metadata.itemSubmissions, selection.itemIdentifier);
106487
+ if (!itemResult || !submission) {
105173
106488
  throw new Error(`Scoring omitted selected review item ${selection.itemIdentifier}`);
105174
106489
  }
105175
- const childLineItemId = await reviewQuestionLineItemId(input.result.assessmentLineItem.sourcedId, selection.itemIdentifier);
105176
- const childResultId = await reviewQuestionResultId(input.result.sourcedId, childLineItemId);
105177
- const finalized = buildFinalizedReviewChildResult({
105178
- childLineItemId,
105179
- student: input.result.student,
105180
- parentAttemptId: input.result.sourcedId,
105181
- metadata: input.metadata,
105182
- selection,
105183
- itemResult,
105184
- submissionId: input.submissionId,
105185
- timestamp: input.timestamp
105186
- });
105187
- await this.requireClient().api.oneroster.assessmentResults.upsert(childResultId, finalized.resultUpdate);
105188
- return finalized.outcome;
106490
+ return {
106491
+ itemIdentifier: selection.itemIdentifier,
106492
+ standard: selection.standard,
106493
+ answered: submission.answered,
106494
+ score: itemResult.score,
106495
+ isCorrect: itemResult.isCorrect
106496
+ };
105189
106497
  });
105190
106498
  }
105191
106499
  async resolveAttemptAuthorization({ gameId, studentId, attemptId, user }, options = {}) {
@@ -105217,11 +106525,12 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
105217
106525
  return { attempt: { result, metadata: metadata2, integration }, enrollment };
105218
106526
  }
105219
106527
  async authorizeAttempt(params, options = {}) {
105220
- const { attempt, enrollment } = await this.resolveAttemptAuthorization(params, options);
105221
- if (!enrollment || attempt.integration.status === "deactivated") {
105222
- throw this.unauthorizedAttempt(params.attemptId);
105223
- }
105224
- return { ...attempt, enrollment };
106528
+ const authorization = await this.resolveAttemptAuthorization(params, options);
106529
+ return this.requireActiveAttempt(authorization, params.attemptId);
106530
+ }
106531
+ requireActiveAttempt(authorization, attemptId) {
106532
+ const enrollment = requireAssessmentMutationAccess(attemptId, authorization.attempt.integration.status, authorization.enrollment);
106533
+ return { ...authorization.attempt, enrollment };
105225
106534
  }
105226
106535
  async authorizeAttemptRead(params) {
105227
106536
  const { attempt, enrollment } = await this.resolveAttemptAuthorization(params);
@@ -107099,7 +108408,7 @@ function getTotalXpFromTimebackConfig(config5) {
107099
108408
  return courseMetadata.metrics.totalXp;
107100
108409
  }
107101
108410
  const resourceMetadata = config5.resource.metadata;
107102
- if (isRecord5(resourceMetadata) && typeof resourceMetadata.xp === "number") {
108411
+ if (isRecord4(resourceMetadata) && typeof resourceMetadata.xp === "number") {
107103
108412
  return resourceMetadata.xp;
107104
108413
  }
107105
108414
  return null;
@@ -108122,8 +109431,8 @@ var init_timeback_service = __esm(async () => {
108122
109431
  });
108123
109432
  }
108124
109433
  static patchCourseMetadata(metadata2, totalXp, options) {
108125
- const nextMetadata = isRecord5(metadata2) ? { ...metadata2 } : {};
108126
- const currentMetrics = isRecord5(nextMetadata.metrics) ? nextMetadata.metrics : {};
109434
+ const nextMetadata = isRecord4(metadata2) ? { ...metadata2 } : {};
109435
+ const currentMetrics = isRecord4(nextMetadata.metrics) ? nextMetadata.metrics : {};
108127
109436
  const metrics2 = { ...currentMetrics };
108128
109437
  if (totalXp === null) {
108129
109438
  delete metrics2.totalXp;
@@ -108148,7 +109457,7 @@ var init_timeback_service = __esm(async () => {
108148
109457
  if (goals === null) {
108149
109458
  delete nextMetadata.goals;
108150
109459
  } else {
108151
- const currentGoals = isRecord5(nextMetadata.goals) ? nextMetadata.goals : {};
109460
+ const currentGoals = isRecord4(nextMetadata.goals) ? nextMetadata.goals : {};
108152
109461
  const nextGoals = { ...currentGoals };
108153
109462
  for (const [key, value] of Object.entries(goals)) {
108154
109463
  if (value === null) {
@@ -108167,7 +109476,7 @@ var init_timeback_service = __esm(async () => {
108167
109476
  if (options?.publishStatus !== undefined) {
108168
109477
  if (options.publishStatus === null) {
108169
109478
  delete nextMetadata.publishStatus;
108170
- const alphaLearn = isRecord5(nextMetadata.AlphaLearn) ? { ...nextMetadata.AlphaLearn } : {};
109479
+ const alphaLearn = isRecord4(nextMetadata.AlphaLearn) ? { ...nextMetadata.AlphaLearn } : {};
108171
109480
  delete alphaLearn.publishStatus;
108172
109481
  if (Object.keys(alphaLearn).length > 0) {
108173
109482
  nextMetadata.AlphaLearn = alphaLearn;
@@ -108176,7 +109485,7 @@ var init_timeback_service = __esm(async () => {
108176
109485
  }
108177
109486
  } else {
108178
109487
  nextMetadata.publishStatus = options.publishStatus;
108179
- const alphaLearn = isRecord5(nextMetadata.AlphaLearn) ? { ...nextMetadata.AlphaLearn } : {};
109488
+ const alphaLearn = isRecord4(nextMetadata.AlphaLearn) ? { ...nextMetadata.AlphaLearn } : {};
108180
109489
  alphaLearn.publishStatus = options.publishStatus === "published" ? "active" : options.publishStatus;
108181
109490
  nextMetadata.AlphaLearn = alphaLearn;
108182
109491
  }
@@ -108194,9 +109503,9 @@ var init_timeback_service = __esm(async () => {
108194
109503
  return Object.keys(nextMetadata).length > 0 ? nextMetadata : undefined;
108195
109504
  }
108196
109505
  static patchResourceMetadata(metadata2, options) {
108197
- const nextMetadata = isRecord5(metadata2) ? { ...metadata2 } : {};
108198
- const playcademyMetadata = isRecord5(nextMetadata.playcademy) ? { ...nextMetadata.playcademy } : {};
108199
- const masteryMetadata = isRecord5(playcademyMetadata.mastery) ? { ...playcademyMetadata.mastery } : {};
109506
+ const nextMetadata = isRecord4(metadata2) ? { ...metadata2 } : {};
109507
+ const playcademyMetadata = isRecord4(nextMetadata.playcademy) ? { ...nextMetadata.playcademy } : {};
109508
+ const masteryMetadata = isRecord4(playcademyMetadata.mastery) ? { ...playcademyMetadata.mastery } : {};
108200
109509
  nextMetadata.subject = options.subject;
108201
109510
  nextMetadata.grades = [options.grade];
108202
109511
  if (options.totalXp === null) {