@playcademy/sandbox 0.10.0 → 0.10.1-beta.2

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 +86 -31
  2. package/dist/server.js +86 -31
  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.10.0",
1131
+ version: "0.10.1-beta.2",
1132
1132
  description: "Local development server for Playcademy game development",
1133
1133
  type: "module",
1134
1134
  exports: {
@@ -9562,6 +9562,21 @@ function assessmentReviewBankShortage(fulfillment) {
9562
9562
  function assessmentFlowForPurpose(purpose) {
9563
9563
  return purpose === "review" ? "item-submit" : "attempt-submit";
9564
9564
  }
9565
+ function assessmentItemOutcomes(submissions, standardForItem) {
9566
+ return submissions.map((submission) => {
9567
+ const standard = standardForItem(submission.itemIdentifier);
9568
+ if (!standard) {
9569
+ throw new Error(`Missing outcome standard for item ${submission.itemIdentifier}`);
9570
+ }
9571
+ return {
9572
+ itemIdentifier: submission.itemIdentifier,
9573
+ standard: { ...standard },
9574
+ answered: submission.answered,
9575
+ score: { ...submission.score },
9576
+ isCorrect: submission.isCorrect
9577
+ };
9578
+ });
9579
+ }
9565
9580
  function isObject(value) {
9566
9581
  return typeof value === "object" && value !== null;
9567
9582
  }
@@ -10730,7 +10745,7 @@ function completeAssessmentTranscript(input) {
10730
10745
  }
10731
10746
  if (input.terminal) {
10732
10747
  const count = itemSubmissions.length;
10733
- const conventionalComplete = assessmentFlowForPurpose(input.purpose) === "item-submit" ? count > 0 : count === input.itemCount;
10748
+ const conventionalComplete = assessmentFlowForPurpose(input.purpose) === "item-submit" ? count > 0 : count === input.itemCount && (input.purpose !== "mastery" || count > 0);
10734
10749
  const complete = state ? state.status === "ready-to-complete" && count > 0 : conventionalComplete;
10735
10750
  if (!complete) {
10736
10751
  throw new AssessmentTranscriptError("A complete administered transcript is required for submission", count);
@@ -10738,12 +10753,6 @@ function completeAssessmentTranscript(input) {
10738
10753
  }
10739
10754
  return { itemSubmissions, ledger, state };
10740
10755
  }
10741
- function administeredItemIdentifiers(itemSubmissions) {
10742
- return new Set(itemSubmissions.map((submission) => submission.itemIdentifier));
10743
- }
10744
- function itemAdministration(itemSubmissions, itemIdentifier) {
10745
- return itemSubmissions.find((submission) => submission.itemIdentifier === itemIdentifier);
10746
- }
10747
10756
  function selectAssessmentCourseCandidate(candidates, subjectSpecified) {
10748
10757
  if (candidates.length === 0) {
10749
10758
  return { outcome: "none" };
@@ -10993,6 +11002,32 @@ function scorePlatformAssessmentItem(item, responses) {
10993
11002
  isCorrect: verdicts.length > 0 && verdicts.every(Boolean)
10994
11003
  };
10995
11004
  }
11005
+ function reportReviewRequiredPairUnavailable(review, administeredItemIdentifiers2, report) {
11006
+ if (!review.requireBothQuestions) {
11007
+ return;
11008
+ }
11009
+ const administered = new Set(administeredItemIdentifiers2);
11010
+ const failures = [];
11011
+ for (const standard of review.standards) {
11012
+ const key = assessmentStandardRefKey(standard);
11013
+ const selected = new Set(review.selections.filter((selection) => assessmentStandardRefKey(selection.standard) === key).map((selection) => selection.itemIdentifier));
11014
+ const administeredCount = [...selected].filter((identifier) => administered.has(identifier)).length;
11015
+ if (selected.size < 2 || administeredCount === 1) {
11016
+ failures.push({
11017
+ standard,
11018
+ reason: selected.size < 2 ? "selection_shortage" : "not_administered",
11019
+ requiredCount: 2,
11020
+ selectedCount: selected.size,
11021
+ administeredCount
11022
+ });
11023
+ }
11024
+ }
11025
+ if (failures.length > 0) {
11026
+ try {
11027
+ report({ code: "REVIEW_REQUIRED_PAIR_UNAVAILABLE", failures });
11028
+ } catch {}
11029
+ }
11030
+ }
10996
11031
  function reviewStandardFieldsWithinLimits(input) {
10997
11032
  return input.framework.trim().length <= TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.standardFieldLength && input.identifier.trim().length <= TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.standardFieldLength;
10998
11033
  }
@@ -11045,7 +11080,11 @@ function normalizeReviewRequest(request) {
11045
11080
  if (standards.length === 0) {
11046
11081
  throw new Error("At least one review standard is required");
11047
11082
  }
11048
- return { standards, candidateItemsPerStandard };
11083
+ return {
11084
+ standards,
11085
+ candidateItemsPerStandard,
11086
+ ...request.requireBothQuestions ? { requireBothQuestions: true } : {}
11087
+ };
11049
11088
  }
11050
11089
  function reviewRequestFingerprint(request, policy) {
11051
11090
  const normalized = normalizeReviewRequest(request);
@@ -11055,6 +11094,7 @@ function reviewRequestFingerprint(request, policy) {
11055
11094
  version: policy.version
11056
11095
  },
11057
11096
  candidateItemsPerStandard: normalized.candidateItemsPerStandard,
11097
+ ...normalized.requireBothQuestions ? { requireBothQuestions: true } : {},
11058
11098
  standardKeys: normalized.standards.map(assessmentStandardRefKey)
11059
11099
  });
11060
11100
  }
@@ -11570,7 +11610,7 @@ function isReviewFulfillment(value) {
11570
11610
  const shortagesValid = value.shortages.every((shortage) => isRecord(shortage) && isAssessmentStandardRef(shortage.standard) && Number.isInteger(shortage.requested) && Number.isInteger(shortage.selected) && Number.isInteger(shortage.eligible));
11571
11611
  return shortagesValid && (value.status === "complete" && value.shortages.length === 0 || value.status === "partial" && value.shortages.length > 0);
11572
11612
  }
11573
- function isReviewItemOutcome(value) {
11613
+ function isAssessmentItemOutcome(value) {
11574
11614
  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");
11575
11615
  }
11576
11616
  function isAssessmentItemSubmission(value) {
@@ -11637,7 +11677,7 @@ function isRoutedDiagnosticAttemptMetadata(value, itemSubmissions, responseVersi
11637
11677
  });
11638
11678
  }
11639
11679
  function isReviewAttemptMetadata(value) {
11640
- return isRecord(value) && typeof value.requestFingerprint === "string" && typeof value.bankRevision === "string" && Array.isArray(value.standards) && value.standards.every(isAssessmentStandardRef) && Number.isInteger(value.candidateItemsPerStandard) && Array.isArray(value.selections) && value.selections.every((selection) => isRecord(selection) && isAssessmentStandardRef(selection.standard) && typeof selection.itemIdentifier === "string") && isReviewFulfillment(value.fulfillment);
11680
+ return isRecord(value) && typeof value.requestFingerprint === "string" && typeof value.bankRevision === "string" && Array.isArray(value.standards) && value.standards.every(isAssessmentStandardRef) && Number.isInteger(value.candidateItemsPerStandard) && (value.requireBothQuestions === undefined || typeof value.requireBothQuestions === "boolean") && Array.isArray(value.selections) && value.selections.every((selection) => isRecord(selection) && isAssessmentStandardRef(selection.standard) && typeof selection.itemIdentifier === "string") && isReviewFulfillment(value.fulfillment);
11641
11681
  }
11642
11682
  function isOptionalInteger(value) {
11643
11683
  return value === undefined || Number.isInteger(value);
@@ -11661,7 +11701,7 @@ function isCompletionMetadata(value, purpose, routedDiagnostic) {
11661
11701
  if (!isRecord(value) || typeof value.testName !== "string" || value.courseName !== undefined && typeof value.courseName !== "string" || !Number.isInteger(value.correctQuestions) || value.session !== undefined && !isAssessmentSessionTiming(value.session) || value.totalQuestions !== undefined && (!Number.isInteger(value.totalQuestions) || Number(value.totalQuestions) < 0)) {
11662
11702
  return false;
11663
11703
  }
11664
- const itemOutcomesValid = value.itemOutcomes === undefined || purpose === "review" && Array.isArray(value.itemOutcomes) && value.itemOutcomes.every(isReviewItemOutcome);
11704
+ const itemOutcomesValid = value.itemOutcomes === undefined || purpose === "review" && Array.isArray(value.itemOutcomes) && value.itemOutcomes.every(isAssessmentItemOutcome);
11665
11705
  const diagnosticTracksValid = value.diagnosticTracks === undefined || routedDiagnostic && Array.isArray(value.diagnosticTracks) && value.diagnosticTracks.every(isDiagnosticTrackResult);
11666
11706
  return itemOutcomesValid && diagnosticTracksValid && (!routedDiagnostic || value.itemOutcomes === undefined && Array.isArray(value.diagnosticTracks) && value.diagnosticTracks.length > 0);
11667
11707
  }
@@ -12335,6 +12375,7 @@ var init_assessment_runtime2 = __esm(() => {
12335
12375
  }),
12336
12376
  StartAssessmentBaseSchema.extend({
12337
12377
  purpose: exports_external.literal("review"),
12378
+ requireBothQuestions: exports_external.boolean().optional(),
12338
12379
  standards: exports_external.array(AssessmentStandardRefSchema2).min(1).max(TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.standards),
12339
12380
  candidateItemsPerStandard: exports_external.number().int().positive().max(TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.candidateItemsPerStandard).optional()
12340
12381
  }),
@@ -100305,6 +100346,7 @@ function buildConventionalAssessmentSnapshot(input) {
100305
100346
  purpose: metadata2.purpose,
100306
100347
  standards: [...metadata2.review.standards],
100307
100348
  candidateItemsPerStandard: metadata2.review.candidateItemsPerStandard,
100349
+ ...metadata2.review.requireBothQuestions ? { requireBothQuestions: true } : {},
100308
100350
  selections: [...metadata2.review.selections],
100309
100351
  fulfillment: metadata2.review.fulfillment
100310
100352
  };
@@ -100388,6 +100430,17 @@ function buildAssessmentSubmitResult(input) {
100388
100430
  const itemOutcomes = input.metadata.completion?.itemOutcomes;
100389
100431
  return itemOutcomes ? { ...base, purpose: input.metadata.purpose, itemOutcomes } : null;
100390
100432
  }
100433
+ if (input.metadata.purpose === "mastery") {
100434
+ const { itemSubmissions, mastery, completion } = input.metadata;
100435
+ if (itemSubmissions.length === 0 || completion?.totalQuestions !== undefined && itemSubmissions.length !== completion.totalQuestions) {
100436
+ return null;
100437
+ }
100438
+ return {
100439
+ ...base,
100440
+ purpose: "mastery",
100441
+ itemOutcomes: assessmentItemOutcomes(itemSubmissions, () => mastery.standard)
100442
+ };
100443
+ }
100391
100444
  return { ...base, purpose: input.metadata.purpose };
100392
100445
  }
100393
100446
  function assessmentAwardFromResult(result, metadata2) {
@@ -102765,11 +102818,13 @@ function buildPreparedAssessmentMetadata(plan, timestamp6, testName) {
102765
102818
  review: {
102766
102819
  requestFingerprint: reviewRequestFingerprint({
102767
102820
  standards: plan.review.standards,
102768
- candidateItemsPerStandard: plan.review.candidateItemsPerStandard
102821
+ candidateItemsPerStandard: plan.review.candidateItemsPerStandard,
102822
+ requireBothQuestions: plan.review.requireBothQuestions
102769
102823
  }, DEFAULT_REVIEW_SELECTION_POLICY),
102770
102824
  bankRevision: plan.review.bankRevision,
102771
102825
  standards: plan.review.standards,
102772
102826
  candidateItemsPerStandard: plan.review.candidateItemsPerStandard,
102827
+ ...plan.review.requireBothQuestions ? { requireBothQuestions: true } : {},
102773
102828
  selections: plan.review.selections,
102774
102829
  fulfillment: plan.review.fulfillment
102775
102830
  }
@@ -103394,22 +103449,11 @@ async function resumeAssessmentTranscript(input) {
103394
103449
  }
103395
103450
  }
103396
103451
  function assessmentReviewTranscriptOutcomes(input) {
103397
- const resultByItem = new Map(input.itemResults.map((item) => [item.itemIdentifier, item]));
103398
- const administered = administeredItemIdentifiers(input.metadata.itemSubmissions);
103399
- return input.metadata.review.selections.filter((selection) => administered.has(selection.itemIdentifier)).map((selection) => {
103400
- const result = resultByItem.get(selection.itemIdentifier);
103401
- const submission = itemAdministration(input.metadata.itemSubmissions, selection.itemIdentifier);
103402
- if (!result || !submission) {
103403
- throw new Error(`Scoring omitted selected review item ${selection.itemIdentifier}`);
103404
- }
103405
- return {
103406
- itemIdentifier: selection.itemIdentifier,
103407
- standard: selection.standard,
103408
- answered: submission.answered,
103409
- score: result.score,
103410
- isCorrect: result.isCorrect
103411
- };
103412
- });
103452
+ const standards = new Map(input.metadata.review.selections.map((selection) => [
103453
+ selection.itemIdentifier,
103454
+ selection.standard
103455
+ ]));
103456
+ return assessmentItemOutcomes(input.metadata.itemSubmissions, (identifier) => standards.get(identifier));
103413
103457
  }
103414
103458
  function assessmentReviewTranscriptExposures(input) {
103415
103459
  const exposures = [];
@@ -103963,6 +104007,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
103963
104007
  bankRevision: catalog.bank.bankRevision,
103964
104008
  standards: [...normalized.standards],
103965
104009
  candidateItemsPerStandard: normalized.candidateItemsPerStandard,
104010
+ ...normalized.requireBothQuestions ? { requireBothQuestions: true } : {},
103966
104011
  selections: [...selected.selections],
103967
104012
  fulfillment: selected.fulfillment
103968
104013
  }
@@ -104234,6 +104279,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
104234
104279
  bankRevision: source.bank.bankRevision,
104235
104280
  standards: [...normalizedRequest.standards],
104236
104281
  candidateItemsPerStandard: normalizedRequest.candidateItemsPerStandard,
104282
+ ...normalizedRequest.requireBothQuestions ? { requireBothQuestions: true } : {},
104237
104283
  selections: [...selected.selections],
104238
104284
  fulfillment: selected.fulfillment
104239
104285
  }
@@ -104461,8 +104507,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
104461
104507
  }, transcript, true, db2);
104462
104508
  const diagnostic = this.isPlatformRoutedDiagnosticMetadata(graded.metadata) ? summarizeDiagnosticSubmission(graded.metadata, graded.manifest) : null;
104463
104509
  const itemOutcomes = graded.metadata.purpose === "review" ? assessmentReviewTranscriptOutcomes({
104464
- metadata: graded.metadata,
104465
- itemResults: graded.itemResults
104510
+ metadata: graded.metadata
104466
104511
  }) : undefined;
104467
104512
  const courseName = attempt.metadata.courseName ?? "Game Course";
104468
104513
  const timestamp6 = new Date().toISOString();
@@ -104510,6 +104555,16 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
104510
104555
  };
104511
104556
  });
104512
104557
  if (accepted.repair) {
104558
+ const { metadata: metadata2 } = accepted.repair.graded;
104559
+ if (metadata2.purpose === "review") {
104560
+ reportReviewRequiredPairUnavailable(metadata2.review, metadata2.itemSubmissions.map((item) => item.itemIdentifier), (error88) => addEvent("assessment.review_required_pair_unavailable", {
104561
+ "app.assessment.attempt_id": params.attemptId,
104562
+ "app.assessment.submission_id": input.submissionId,
104563
+ "app.error.code": error88.code,
104564
+ "app.assessment.review.required_pair_failure_count": error88.failures.length,
104565
+ "app.assessment.review.required_pair_failures": error88.failures.map((failure) => JSON.stringify(failure))
104566
+ }));
104567
+ }
104513
104568
  this.reconcileTranscriptChildren(accepted.repair.context, accepted.repair.graded).catch((error88) => this.recordTranscriptFailure("completion_repair", params.attemptId, error88));
104514
104569
  }
104515
104570
  if (accepted.emission) {
package/dist/server.js CHANGED
@@ -1127,7 +1127,7 @@ var package_default;
1127
1127
  var init_package = __esm(() => {
1128
1128
  package_default = {
1129
1129
  name: "@playcademy/sandbox",
1130
- version: "0.10.0",
1130
+ version: "0.10.1-beta.2",
1131
1131
  description: "Local development server for Playcademy game development",
1132
1132
  type: "module",
1133
1133
  exports: {
@@ -9561,6 +9561,21 @@ function assessmentReviewBankShortage(fulfillment) {
9561
9561
  function assessmentFlowForPurpose(purpose) {
9562
9562
  return purpose === "review" ? "item-submit" : "attempt-submit";
9563
9563
  }
9564
+ function assessmentItemOutcomes(submissions, standardForItem) {
9565
+ return submissions.map((submission) => {
9566
+ const standard = standardForItem(submission.itemIdentifier);
9567
+ if (!standard) {
9568
+ throw new Error(`Missing outcome standard for item ${submission.itemIdentifier}`);
9569
+ }
9570
+ return {
9571
+ itemIdentifier: submission.itemIdentifier,
9572
+ standard: { ...standard },
9573
+ answered: submission.answered,
9574
+ score: { ...submission.score },
9575
+ isCorrect: submission.isCorrect
9576
+ };
9577
+ });
9578
+ }
9564
9579
  function isObject(value) {
9565
9580
  return typeof value === "object" && value !== null;
9566
9581
  }
@@ -10729,7 +10744,7 @@ function completeAssessmentTranscript(input) {
10729
10744
  }
10730
10745
  if (input.terminal) {
10731
10746
  const count = itemSubmissions.length;
10732
- const conventionalComplete = assessmentFlowForPurpose(input.purpose) === "item-submit" ? count > 0 : count === input.itemCount;
10747
+ const conventionalComplete = assessmentFlowForPurpose(input.purpose) === "item-submit" ? count > 0 : count === input.itemCount && (input.purpose !== "mastery" || count > 0);
10733
10748
  const complete = state ? state.status === "ready-to-complete" && count > 0 : conventionalComplete;
10734
10749
  if (!complete) {
10735
10750
  throw new AssessmentTranscriptError("A complete administered transcript is required for submission", count);
@@ -10737,12 +10752,6 @@ function completeAssessmentTranscript(input) {
10737
10752
  }
10738
10753
  return { itemSubmissions, ledger, state };
10739
10754
  }
10740
- function administeredItemIdentifiers(itemSubmissions) {
10741
- return new Set(itemSubmissions.map((submission) => submission.itemIdentifier));
10742
- }
10743
- function itemAdministration(itemSubmissions, itemIdentifier) {
10744
- return itemSubmissions.find((submission) => submission.itemIdentifier === itemIdentifier);
10745
- }
10746
10755
  function selectAssessmentCourseCandidate(candidates, subjectSpecified) {
10747
10756
  if (candidates.length === 0) {
10748
10757
  return { outcome: "none" };
@@ -10992,6 +11001,32 @@ function scorePlatformAssessmentItem(item, responses) {
10992
11001
  isCorrect: verdicts.length > 0 && verdicts.every(Boolean)
10993
11002
  };
10994
11003
  }
11004
+ function reportReviewRequiredPairUnavailable(review, administeredItemIdentifiers2, report) {
11005
+ if (!review.requireBothQuestions) {
11006
+ return;
11007
+ }
11008
+ const administered = new Set(administeredItemIdentifiers2);
11009
+ const failures = [];
11010
+ for (const standard of review.standards) {
11011
+ const key = assessmentStandardRefKey(standard);
11012
+ const selected = new Set(review.selections.filter((selection) => assessmentStandardRefKey(selection.standard) === key).map((selection) => selection.itemIdentifier));
11013
+ const administeredCount = [...selected].filter((identifier) => administered.has(identifier)).length;
11014
+ if (selected.size < 2 || administeredCount === 1) {
11015
+ failures.push({
11016
+ standard,
11017
+ reason: selected.size < 2 ? "selection_shortage" : "not_administered",
11018
+ requiredCount: 2,
11019
+ selectedCount: selected.size,
11020
+ administeredCount
11021
+ });
11022
+ }
11023
+ }
11024
+ if (failures.length > 0) {
11025
+ try {
11026
+ report({ code: "REVIEW_REQUIRED_PAIR_UNAVAILABLE", failures });
11027
+ } catch {}
11028
+ }
11029
+ }
10995
11030
  function reviewStandardFieldsWithinLimits(input) {
10996
11031
  return input.framework.trim().length <= TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.standardFieldLength && input.identifier.trim().length <= TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.standardFieldLength;
10997
11032
  }
@@ -11044,7 +11079,11 @@ function normalizeReviewRequest(request) {
11044
11079
  if (standards.length === 0) {
11045
11080
  throw new Error("At least one review standard is required");
11046
11081
  }
11047
- return { standards, candidateItemsPerStandard };
11082
+ return {
11083
+ standards,
11084
+ candidateItemsPerStandard,
11085
+ ...request.requireBothQuestions ? { requireBothQuestions: true } : {}
11086
+ };
11048
11087
  }
11049
11088
  function reviewRequestFingerprint(request, policy) {
11050
11089
  const normalized = normalizeReviewRequest(request);
@@ -11054,6 +11093,7 @@ function reviewRequestFingerprint(request, policy) {
11054
11093
  version: policy.version
11055
11094
  },
11056
11095
  candidateItemsPerStandard: normalized.candidateItemsPerStandard,
11096
+ ...normalized.requireBothQuestions ? { requireBothQuestions: true } : {},
11057
11097
  standardKeys: normalized.standards.map(assessmentStandardRefKey)
11058
11098
  });
11059
11099
  }
@@ -11569,7 +11609,7 @@ function isReviewFulfillment(value) {
11569
11609
  const shortagesValid = value.shortages.every((shortage) => isRecord(shortage) && isAssessmentStandardRef(shortage.standard) && Number.isInteger(shortage.requested) && Number.isInteger(shortage.selected) && Number.isInteger(shortage.eligible));
11570
11610
  return shortagesValid && (value.status === "complete" && value.shortages.length === 0 || value.status === "partial" && value.shortages.length > 0);
11571
11611
  }
11572
- function isReviewItemOutcome(value) {
11612
+ function isAssessmentItemOutcome(value) {
11573
11613
  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");
11574
11614
  }
11575
11615
  function isAssessmentItemSubmission(value) {
@@ -11636,7 +11676,7 @@ function isRoutedDiagnosticAttemptMetadata(value, itemSubmissions, responseVersi
11636
11676
  });
11637
11677
  }
11638
11678
  function isReviewAttemptMetadata(value) {
11639
- return isRecord(value) && typeof value.requestFingerprint === "string" && typeof value.bankRevision === "string" && Array.isArray(value.standards) && value.standards.every(isAssessmentStandardRef) && Number.isInteger(value.candidateItemsPerStandard) && Array.isArray(value.selections) && value.selections.every((selection) => isRecord(selection) && isAssessmentStandardRef(selection.standard) && typeof selection.itemIdentifier === "string") && isReviewFulfillment(value.fulfillment);
11679
+ return isRecord(value) && typeof value.requestFingerprint === "string" && typeof value.bankRevision === "string" && Array.isArray(value.standards) && value.standards.every(isAssessmentStandardRef) && Number.isInteger(value.candidateItemsPerStandard) && (value.requireBothQuestions === undefined || typeof value.requireBothQuestions === "boolean") && Array.isArray(value.selections) && value.selections.every((selection) => isRecord(selection) && isAssessmentStandardRef(selection.standard) && typeof selection.itemIdentifier === "string") && isReviewFulfillment(value.fulfillment);
11640
11680
  }
11641
11681
  function isOptionalInteger(value) {
11642
11682
  return value === undefined || Number.isInteger(value);
@@ -11660,7 +11700,7 @@ function isCompletionMetadata(value, purpose, routedDiagnostic) {
11660
11700
  if (!isRecord(value) || typeof value.testName !== "string" || value.courseName !== undefined && typeof value.courseName !== "string" || !Number.isInteger(value.correctQuestions) || value.session !== undefined && !isAssessmentSessionTiming(value.session) || value.totalQuestions !== undefined && (!Number.isInteger(value.totalQuestions) || Number(value.totalQuestions) < 0)) {
11661
11701
  return false;
11662
11702
  }
11663
- const itemOutcomesValid = value.itemOutcomes === undefined || purpose === "review" && Array.isArray(value.itemOutcomes) && value.itemOutcomes.every(isReviewItemOutcome);
11703
+ const itemOutcomesValid = value.itemOutcomes === undefined || purpose === "review" && Array.isArray(value.itemOutcomes) && value.itemOutcomes.every(isAssessmentItemOutcome);
11664
11704
  const diagnosticTracksValid = value.diagnosticTracks === undefined || routedDiagnostic && Array.isArray(value.diagnosticTracks) && value.diagnosticTracks.every(isDiagnosticTrackResult);
11665
11705
  return itemOutcomesValid && diagnosticTracksValid && (!routedDiagnostic || value.itemOutcomes === undefined && Array.isArray(value.diagnosticTracks) && value.diagnosticTracks.length > 0);
11666
11706
  }
@@ -12334,6 +12374,7 @@ var init_assessment_runtime2 = __esm(() => {
12334
12374
  }),
12335
12375
  StartAssessmentBaseSchema.extend({
12336
12376
  purpose: exports_external.literal("review"),
12377
+ requireBothQuestions: exports_external.boolean().optional(),
12337
12378
  standards: exports_external.array(AssessmentStandardRefSchema2).min(1).max(TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.standards),
12338
12379
  candidateItemsPerStandard: exports_external.number().int().positive().max(TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.candidateItemsPerStandard).optional()
12339
12380
  }),
@@ -100304,6 +100345,7 @@ function buildConventionalAssessmentSnapshot(input) {
100304
100345
  purpose: metadata2.purpose,
100305
100346
  standards: [...metadata2.review.standards],
100306
100347
  candidateItemsPerStandard: metadata2.review.candidateItemsPerStandard,
100348
+ ...metadata2.review.requireBothQuestions ? { requireBothQuestions: true } : {},
100307
100349
  selections: [...metadata2.review.selections],
100308
100350
  fulfillment: metadata2.review.fulfillment
100309
100351
  };
@@ -100387,6 +100429,17 @@ function buildAssessmentSubmitResult(input) {
100387
100429
  const itemOutcomes = input.metadata.completion?.itemOutcomes;
100388
100430
  return itemOutcomes ? { ...base, purpose: input.metadata.purpose, itemOutcomes } : null;
100389
100431
  }
100432
+ if (input.metadata.purpose === "mastery") {
100433
+ const { itemSubmissions, mastery, completion } = input.metadata;
100434
+ if (itemSubmissions.length === 0 || completion?.totalQuestions !== undefined && itemSubmissions.length !== completion.totalQuestions) {
100435
+ return null;
100436
+ }
100437
+ return {
100438
+ ...base,
100439
+ purpose: "mastery",
100440
+ itemOutcomes: assessmentItemOutcomes(itemSubmissions, () => mastery.standard)
100441
+ };
100442
+ }
100390
100443
  return { ...base, purpose: input.metadata.purpose };
100391
100444
  }
100392
100445
  function assessmentAwardFromResult(result, metadata2) {
@@ -102764,11 +102817,13 @@ function buildPreparedAssessmentMetadata(plan, timestamp6, testName) {
102764
102817
  review: {
102765
102818
  requestFingerprint: reviewRequestFingerprint({
102766
102819
  standards: plan.review.standards,
102767
- candidateItemsPerStandard: plan.review.candidateItemsPerStandard
102820
+ candidateItemsPerStandard: plan.review.candidateItemsPerStandard,
102821
+ requireBothQuestions: plan.review.requireBothQuestions
102768
102822
  }, DEFAULT_REVIEW_SELECTION_POLICY),
102769
102823
  bankRevision: plan.review.bankRevision,
102770
102824
  standards: plan.review.standards,
102771
102825
  candidateItemsPerStandard: plan.review.candidateItemsPerStandard,
102826
+ ...plan.review.requireBothQuestions ? { requireBothQuestions: true } : {},
102772
102827
  selections: plan.review.selections,
102773
102828
  fulfillment: plan.review.fulfillment
102774
102829
  }
@@ -103393,22 +103448,11 @@ async function resumeAssessmentTranscript(input) {
103393
103448
  }
103394
103449
  }
103395
103450
  function assessmentReviewTranscriptOutcomes(input) {
103396
- const resultByItem = new Map(input.itemResults.map((item) => [item.itemIdentifier, item]));
103397
- const administered = administeredItemIdentifiers(input.metadata.itemSubmissions);
103398
- return input.metadata.review.selections.filter((selection) => administered.has(selection.itemIdentifier)).map((selection) => {
103399
- const result = resultByItem.get(selection.itemIdentifier);
103400
- const submission = itemAdministration(input.metadata.itemSubmissions, selection.itemIdentifier);
103401
- if (!result || !submission) {
103402
- throw new Error(`Scoring omitted selected review item ${selection.itemIdentifier}`);
103403
- }
103404
- return {
103405
- itemIdentifier: selection.itemIdentifier,
103406
- standard: selection.standard,
103407
- answered: submission.answered,
103408
- score: result.score,
103409
- isCorrect: result.isCorrect
103410
- };
103411
- });
103451
+ const standards = new Map(input.metadata.review.selections.map((selection) => [
103452
+ selection.itemIdentifier,
103453
+ selection.standard
103454
+ ]));
103455
+ return assessmentItemOutcomes(input.metadata.itemSubmissions, (identifier) => standards.get(identifier));
103412
103456
  }
103413
103457
  function assessmentReviewTranscriptExposures(input) {
103414
103458
  const exposures = [];
@@ -103962,6 +104006,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
103962
104006
  bankRevision: catalog.bank.bankRevision,
103963
104007
  standards: [...normalized.standards],
103964
104008
  candidateItemsPerStandard: normalized.candidateItemsPerStandard,
104009
+ ...normalized.requireBothQuestions ? { requireBothQuestions: true } : {},
103965
104010
  selections: [...selected.selections],
103966
104011
  fulfillment: selected.fulfillment
103967
104012
  }
@@ -104233,6 +104278,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
104233
104278
  bankRevision: source.bank.bankRevision,
104234
104279
  standards: [...normalizedRequest.standards],
104235
104280
  candidateItemsPerStandard: normalizedRequest.candidateItemsPerStandard,
104281
+ ...normalizedRequest.requireBothQuestions ? { requireBothQuestions: true } : {},
104236
104282
  selections: [...selected.selections],
104237
104283
  fulfillment: selected.fulfillment
104238
104284
  }
@@ -104460,8 +104506,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
104460
104506
  }, transcript, true, db2);
104461
104507
  const diagnostic = this.isPlatformRoutedDiagnosticMetadata(graded.metadata) ? summarizeDiagnosticSubmission(graded.metadata, graded.manifest) : null;
104462
104508
  const itemOutcomes = graded.metadata.purpose === "review" ? assessmentReviewTranscriptOutcomes({
104463
- metadata: graded.metadata,
104464
- itemResults: graded.itemResults
104509
+ metadata: graded.metadata
104465
104510
  }) : undefined;
104466
104511
  const courseName = attempt.metadata.courseName ?? "Game Course";
104467
104512
  const timestamp6 = new Date().toISOString();
@@ -104509,6 +104554,16 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
104509
104554
  };
104510
104555
  });
104511
104556
  if (accepted.repair) {
104557
+ const { metadata: metadata2 } = accepted.repair.graded;
104558
+ if (metadata2.purpose === "review") {
104559
+ reportReviewRequiredPairUnavailable(metadata2.review, metadata2.itemSubmissions.map((item) => item.itemIdentifier), (error88) => addEvent("assessment.review_required_pair_unavailable", {
104560
+ "app.assessment.attempt_id": params.attemptId,
104561
+ "app.assessment.submission_id": input.submissionId,
104562
+ "app.error.code": error88.code,
104563
+ "app.assessment.review.required_pair_failure_count": error88.failures.length,
104564
+ "app.assessment.review.required_pair_failures": error88.failures.map((failure) => JSON.stringify(failure))
104565
+ }));
104566
+ }
104512
104567
  this.reconcileTranscriptChildren(accepted.repair.context, accepted.repair.graded).catch((error88) => this.recordTranscriptFailure("completion_repair", params.attemptId, error88));
104513
104568
  }
104514
104569
  if (accepted.emission) {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcademy/sandbox",
3
- "version": "0.10.0",
3
+ "version": "0.10.1-beta.2",
4
4
  "description": "Local development server for Playcademy game development",
5
5
  "type": "module",
6
6
  "exports": {