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