@playcademy/sandbox 0.8.1-beta.6 → 0.9.0
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.
- package/dist/cli.js +251 -392
- package/dist/server.js +251 -392
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -1127,7 +1127,7 @@ var package_default;
|
|
|
1127
1127
|
var init_package = __esm(() => {
|
|
1128
1128
|
package_default = {
|
|
1129
1129
|
name: "@playcademy/sandbox",
|
|
1130
|
-
version: "0.
|
|
1130
|
+
version: "0.9.0",
|
|
1131
1131
|
description: "Local development server for Playcademy game development",
|
|
1132
1132
|
type: "module",
|
|
1133
1133
|
exports: {
|
|
@@ -10747,12 +10747,10 @@ function selectAssessmentCourseCandidate(candidates, subjectSpecified) {
|
|
|
10747
10747
|
if (candidates.length === 0) {
|
|
10748
10748
|
return { outcome: "none" };
|
|
10749
10749
|
}
|
|
10750
|
-
const
|
|
10751
|
-
if (
|
|
10750
|
+
const eligible = candidates.filter((candidate) => candidate.hasLiveTests);
|
|
10751
|
+
if (eligible.length === 0) {
|
|
10752
10752
|
return { outcome: "no_eligible_tests" };
|
|
10753
10753
|
}
|
|
10754
|
-
const withOpenAttempt = serviceable.filter((candidate) => candidate.hasOpenAttempt);
|
|
10755
|
-
const eligible = withOpenAttempt.length > 0 ? withOpenAttempt : serviceable;
|
|
10756
10754
|
const subjects = new Set(eligible.map((candidate) => candidate.subject)).size;
|
|
10757
10755
|
if (!subjectSpecified && subjects > 1) {
|
|
10758
10756
|
return { outcome: "ambiguous", subjects };
|
|
@@ -10764,12 +10762,9 @@ function assessmentAttemptTimestamp(value) {
|
|
|
10764
10762
|
const parsed = Date.parse(value);
|
|
10765
10763
|
return Number.isNaN(parsed) ? 0 : parsed;
|
|
10766
10764
|
}
|
|
10767
|
-
function newestAttempt(left, right, field) {
|
|
10768
|
-
const dateDifference = assessmentAttemptTimestamp(right[field]) - assessmentAttemptTimestamp(left[field]);
|
|
10769
|
-
return dateDifference || left.attemptId.localeCompare(right.attemptId);
|
|
10770
|
-
}
|
|
10771
10765
|
function compareNewestCompletedAssessmentAttempt(left, right) {
|
|
10772
|
-
|
|
10766
|
+
const dateDifference = assessmentAttemptTimestamp(right.scoreDate) - assessmentAttemptTimestamp(left.scoreDate);
|
|
10767
|
+
return dateDifference || left.attemptId.localeCompare(right.attemptId);
|
|
10773
10768
|
}
|
|
10774
10769
|
function compareAssessmentCatalogOrder(left, right) {
|
|
10775
10770
|
if (left.assessmentKey !== null && right.assessmentKey !== null) {
|
|
@@ -10792,31 +10787,22 @@ function orderRuntimeAssessmentTests(tests) {
|
|
|
10792
10787
|
return [...tests].toSorted(compareAssessmentCatalogOrder);
|
|
10793
10788
|
}
|
|
10794
10789
|
function selectRuntimeAssessment(liveTests, attempts) {
|
|
10795
|
-
const resumable = attempts.filter((attempt) => attempt.resumable !== false && (isAssessmentAttemptOpen(attempt) || isAssessmentAttemptAwaitingAward(attempt))).toSorted((left, right) => newestAttempt(left, right, "updatedAt"));
|
|
10796
|
-
if (resumable.length > 0) {
|
|
10797
|
-
return {
|
|
10798
|
-
kind: "resume",
|
|
10799
|
-
attempt: resumable[0],
|
|
10800
|
-
additionalResumableAttemptIds: resumable.slice(1).map((candidate) => candidate.attemptId)
|
|
10801
|
-
};
|
|
10802
|
-
}
|
|
10803
10790
|
const orderedTests = orderRuntimeAssessmentTests(liveTests);
|
|
10804
10791
|
if (orderedTests.length === 0) {
|
|
10805
10792
|
return null;
|
|
10806
10793
|
}
|
|
10807
|
-
const
|
|
10808
|
-
const exposedAssessmentKeys = new Set(
|
|
10794
|
+
const history = attempts.filter((attempt) => isAssessmentAttemptOpen(attempt) || isAssessmentAttemptAwaitingAward(attempt) || isAssessmentAttemptCompleted(attempt) || isAssessmentAttemptSuperseded(attempt));
|
|
10795
|
+
const exposedAssessmentKeys = new Set(history.map((attempt) => attempt.selectedAssessmentKey));
|
|
10809
10796
|
const unattempted = orderedTests.find((test) => !exposedAssessmentKeys.has(test.assessmentKey));
|
|
10810
10797
|
if (unattempted) {
|
|
10811
|
-
return {
|
|
10798
|
+
return { test: unattempted, reason: "unattempted" };
|
|
10812
10799
|
}
|
|
10813
|
-
const
|
|
10814
|
-
const latestTestIndex =
|
|
10800
|
+
const latest = history.toSorted(compareNewestCompletedAssessmentAttempt)[0];
|
|
10801
|
+
const latestTestIndex = latest ? orderedTests.findIndex((test) => test.assessmentKey === latest.selectedAssessmentKey) : -1;
|
|
10815
10802
|
if (latestTestIndex === -1) {
|
|
10816
|
-
return {
|
|
10803
|
+
return { test: orderedTests[0], reason: "first_live" };
|
|
10817
10804
|
}
|
|
10818
10805
|
return {
|
|
10819
|
-
kind: "start",
|
|
10820
10806
|
test: orderedTests[(latestTestIndex + 1) % orderedTests.length],
|
|
10821
10807
|
reason: "round_robin"
|
|
10822
10808
|
};
|
|
@@ -100351,13 +100337,6 @@ function buildAssessmentAwardRecord(input, mastery) {
|
|
|
100351
100337
|
...mastery.warnings ? { warnings: mastery.warnings } : {}
|
|
100352
100338
|
};
|
|
100353
100339
|
}
|
|
100354
|
-
function stageAssessmentAttemptSupersession(attempt) {
|
|
100355
|
-
Object.assign(attempt.result, ASSESSMENT_ATTEMPT_SUPERSEDED);
|
|
100356
|
-
Object.assign(attempt.selection, ASSESSMENT_ATTEMPT_SUPERSEDED);
|
|
100357
|
-
}
|
|
100358
|
-
function shouldConfirmAssessmentVersionUnavailable(details) {
|
|
100359
|
-
return typeof details === "object" && details !== null && "expectedRevision" in details && typeof details.expectedRevision === "string" && !("currentRevision" in details);
|
|
100360
|
-
}
|
|
100361
100340
|
function buildAssessmentAttemptSupersessionUpdate(result) {
|
|
100362
100341
|
if (result.status !== "active" || !isAssessmentAttemptOpen({
|
|
100363
100342
|
inProgress: result.inProgress ?? "",
|
|
@@ -100375,17 +100354,6 @@ function buildAssessmentAttemptSupersessionUpdate(result) {
|
|
|
100375
100354
|
metadata: result.metadata ?? {}
|
|
100376
100355
|
};
|
|
100377
100356
|
}
|
|
100378
|
-
async function resolveAvailableAssessmentAttempt(initialDecision, select3, resume) {
|
|
100379
|
-
let decision = initialDecision;
|
|
100380
|
-
while (decision?.kind === "resume") {
|
|
100381
|
-
const snapshot = await resume(decision);
|
|
100382
|
-
if (snapshot !== null) {
|
|
100383
|
-
return { kind: "resumed", snapshot };
|
|
100384
|
-
}
|
|
100385
|
-
decision = select3();
|
|
100386
|
-
}
|
|
100387
|
-
return { kind: "continue", decision };
|
|
100388
|
-
}
|
|
100389
100357
|
function diagnosticQuestionLineItemId(parentLineItemId, itemIdentifier) {
|
|
100390
100358
|
return deterministicUUID(["playcademy:diagnostic-question-line-item:v1", parentLineItemId, itemIdentifier].join("\x00"));
|
|
100391
100359
|
}
|
|
@@ -100460,6 +100428,20 @@ function assessmentAttemptId(input) {
|
|
|
100460
100428
|
String(input.attemptNumber)
|
|
100461
100429
|
].join("\x00"));
|
|
100462
100430
|
}
|
|
100431
|
+
function buildAssessmentAttemptResultWhere(scope) {
|
|
100432
|
+
return {
|
|
100433
|
+
"student.sourcedId": scope.studentId,
|
|
100434
|
+
status: ONEROSTER_STATUS2.active,
|
|
100435
|
+
[`metadata.${PLAYCADEMY_ASSESSMENT_RESULT_METADATA_KEY}.activityId`]: scope.activityId
|
|
100436
|
+
};
|
|
100437
|
+
}
|
|
100438
|
+
function buildAssessmentChildResultWhere(scope) {
|
|
100439
|
+
return {
|
|
100440
|
+
"student.sourcedId": scope.studentId,
|
|
100441
|
+
status: ONEROSTER_STATUS2.active,
|
|
100442
|
+
[`metadata.${PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY}.payload.attempt.activityId`]: scope.activityId
|
|
100443
|
+
};
|
|
100444
|
+
}
|
|
100463
100445
|
async function findLatestAssessmentResult(results, match) {
|
|
100464
100446
|
let latest = null;
|
|
100465
100447
|
let latestTimestamp = 0;
|
|
@@ -100899,6 +100881,7 @@ var PLAYABLE_SHAPE_SET;
|
|
|
100899
100881
|
var init_timeback_assessment_runtime_util = __esm(() => {
|
|
100900
100882
|
init_src();
|
|
100901
100883
|
init_assessment_runtime2();
|
|
100884
|
+
init_constants3();
|
|
100902
100885
|
init_qti();
|
|
100903
100886
|
init_types2();
|
|
100904
100887
|
init_utils6();
|
|
@@ -102686,21 +102669,6 @@ function assessmentPreparationFlightKey(input) {
|
|
|
102686
102669
|
async function assessmentPreparationReceiptMatches(plan, request) {
|
|
102687
102670
|
return plan.gameId === request.gameId && plan.studentId === request.studentId && plan.activityId === request.input.activityId && plan.purpose === request.input.purpose && plan.requestHash === await assessmentPreparationRequestHash(request.input);
|
|
102688
102671
|
}
|
|
102689
|
-
function resolveAssessmentPreparationSelection(decision, candidateCourseCount) {
|
|
102690
|
-
if (decision?.kind === "resume") {
|
|
102691
|
-
return {
|
|
102692
|
-
status: "blocked",
|
|
102693
|
-
result: { status: "canonical_required", reason: "resumable_attempt" }
|
|
102694
|
-
};
|
|
102695
|
-
}
|
|
102696
|
-
if (candidateCourseCount !== 1) {
|
|
102697
|
-
return {
|
|
102698
|
-
status: "blocked",
|
|
102699
|
-
result: { status: "unprepared", reason: "prerequisite_unavailable" }
|
|
102700
|
-
};
|
|
102701
|
-
}
|
|
102702
|
-
return decision ? { status: "ready", decision } : { status: "unavailable" };
|
|
102703
|
-
}
|
|
102704
102672
|
function preparedAssessmentIntegration(plan, candidates) {
|
|
102705
102673
|
if (candidates.rows.length !== 1) {
|
|
102706
102674
|
return null;
|
|
@@ -103446,15 +103414,16 @@ function assessmentReviewTranscriptExposures(input) {
|
|
|
103446
103414
|
for (const { result, metadata: metadata2 } of input.attempts) {
|
|
103447
103415
|
const state = { ...result, inProgress: result.inProgress ?? "" };
|
|
103448
103416
|
const settled = isAssessmentAttemptAwaitingAward(state) || isAssessmentAttemptCompleted(state);
|
|
103449
|
-
|
|
103417
|
+
const useTranscript = !settled && metadata2.transcript !== undefined;
|
|
103418
|
+
if (metadata2.purpose === "review" && (useTranscript || settled)) {
|
|
103450
103419
|
authoritative.add(result.sourcedId);
|
|
103451
103420
|
if (metadata2.review.bankRevision === input.bankRevision) {
|
|
103452
|
-
const accepted =
|
|
103453
|
-
itemIdentifier: submission.itemIdentifier,
|
|
103454
|
-
deliveredAt: submission.submittedAt
|
|
103455
|
-
})) : metadata2.transcript.administrations.map((entry) => ({
|
|
103421
|
+
const accepted = useTranscript ? metadata2.transcript.administrations.map((entry) => ({
|
|
103456
103422
|
itemIdentifier: entry.itemIdentifier,
|
|
103457
103423
|
deliveredAt: metadata2.updatedAt
|
|
103424
|
+
})) : metadata2.itemSubmissions.map((submission) => ({
|
|
103425
|
+
itemIdentifier: submission.itemIdentifier,
|
|
103426
|
+
deliveredAt: submission.submittedAt
|
|
103458
103427
|
}));
|
|
103459
103428
|
exposures.push(...accepted);
|
|
103460
103429
|
}
|
|
@@ -103588,7 +103557,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
103588
103557
|
static PREPARATION_IN_FLIGHT_LIMIT = 32;
|
|
103589
103558
|
static ASSESSMENT_CACHE_TTL_MS = 60000;
|
|
103590
103559
|
static EXPORT_CONCURRENCY = 4;
|
|
103591
|
-
static ASSESSMENT_VERSION_CONFIRMATION_DELAY_MS = 25;
|
|
103592
103560
|
static SCORING_CONCURRENCY = 4;
|
|
103593
103561
|
static STALE_AWARD_AGE_MS = 15 * 60 * 1000;
|
|
103594
103562
|
deps;
|
|
@@ -103678,8 +103646,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
103678
103646
|
return this.startReview({ gameId, studentId, input, user });
|
|
103679
103647
|
}
|
|
103680
103648
|
const masteryStandard = input.purpose === "mastery" ? normalizeMasteryStandard(input.standard) : undefined;
|
|
103681
|
-
const requestFingerprint = this.startRequestFingerprint(input);
|
|
103682
|
-
const pendingSupersessions = new Set;
|
|
103683
103649
|
const candidates = await this.candidateIntegrations(gameId, studentId, input);
|
|
103684
103650
|
const lockKeys = candidates.rows.map((row) => assessmentRuntimeStartLockKey({
|
|
103685
103651
|
studentId,
|
|
@@ -103688,7 +103654,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
103688
103654
|
courseId: row.courseId
|
|
103689
103655
|
}));
|
|
103690
103656
|
return this.deps.assessmentRuntimeLock(this.deps.db, lockKeys, async (db2) => {
|
|
103691
|
-
const context2 = await this.selectIntegration(candidates,
|
|
103657
|
+
const context2 = await this.selectIntegration(candidates, input);
|
|
103692
103658
|
const attemptScope = {
|
|
103693
103659
|
studentId,
|
|
103694
103660
|
activityId: input.activityId,
|
|
@@ -103701,39 +103667,28 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
103701
103667
|
this.liveTests(context2.integration.id, input.purpose, db2, masteryStandard),
|
|
103702
103668
|
this.listAttempts(attemptScope)
|
|
103703
103669
|
]);
|
|
103704
|
-
const
|
|
103705
|
-
|
|
103706
|
-
|
|
103707
|
-
|
|
103708
|
-
|
|
103709
|
-
}
|
|
103710
|
-
|
|
103711
|
-
|
|
103712
|
-
|
|
103713
|
-
|
|
103714
|
-
|
|
103715
|
-
|
|
103716
|
-
|
|
103717
|
-
|
|
103718
|
-
|
|
103719
|
-
|
|
103720
|
-
if (assessment.identifier !== decision.test.qtiTestIdentifier) {
|
|
103721
|
-
assessment = await this.loadAssessment(decision.test.qtiTestIdentifier);
|
|
103722
|
-
}
|
|
103723
|
-
const classId = await this.resolveClassId(context2.integration.courseId, context2.enrollment, studentId);
|
|
103670
|
+
const pending = await this.pendingAwardSnapshot(listing, context2);
|
|
103671
|
+
if (pending) {
|
|
103672
|
+
return pending;
|
|
103673
|
+
}
|
|
103674
|
+
const decision = this.selectFreshStart(tests, listing, input.purpose);
|
|
103675
|
+
const [assessment, classId, { attemptNumber, attemptId }] = await Promise.all([
|
|
103676
|
+
this.loadAssessment(decision.test.qtiTestIdentifier),
|
|
103677
|
+
this.resolveClassId(context2.integration.courseId, context2.enrollment, studentId),
|
|
103678
|
+
this.claimAttemptId({
|
|
103679
|
+
gameId,
|
|
103680
|
+
studentId,
|
|
103681
|
+
courseId: context2.integration.courseId,
|
|
103682
|
+
activityId: input.activityId,
|
|
103683
|
+
purpose: input.purpose
|
|
103684
|
+
}, nextAssessmentAttemptNumber(listing.attempts.values()))
|
|
103685
|
+
]);
|
|
103724
103686
|
const lineItemId = await this.ensureLineItem({
|
|
103725
103687
|
classId,
|
|
103726
103688
|
integration: context2.integration,
|
|
103727
103689
|
purpose: input.purpose,
|
|
103728
103690
|
assessment
|
|
103729
103691
|
});
|
|
103730
|
-
const { attemptNumber, attemptId } = await this.claimAttemptId({
|
|
103731
|
-
gameId,
|
|
103732
|
-
studentId,
|
|
103733
|
-
courseId: context2.integration.courseId,
|
|
103734
|
-
activityId: input.activityId,
|
|
103735
|
-
purpose: input.purpose
|
|
103736
|
-
}, Math.max(0, ...[...refreshed.values()].map((entry) => entry.metadata.attemptNumber)) + 1);
|
|
103737
103692
|
const timestamp6 = new Date().toISOString();
|
|
103738
103693
|
const metadataBase = {
|
|
103739
103694
|
version: PLAYCADEMY_ASSESSMENT_RESULT_METADATA_VERSION,
|
|
@@ -103758,7 +103713,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
103758
103713
|
...metadataBase,
|
|
103759
103714
|
purpose: input.purpose,
|
|
103760
103715
|
mastery: {
|
|
103761
|
-
requestFingerprint,
|
|
103716
|
+
requestFingerprint: masteryRequestFingerprint(input),
|
|
103762
103717
|
standard: masteryStandard
|
|
103763
103718
|
}
|
|
103764
103719
|
} : { ...metadataBase, purpose: input.purpose };
|
|
@@ -103776,17 +103731,24 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
103776
103731
|
grade: context2.integration.grade
|
|
103777
103732
|
})
|
|
103778
103733
|
});
|
|
103779
|
-
await
|
|
103780
|
-
|
|
103734
|
+
const [snapshot] = await Promise.all([
|
|
103735
|
+
this.snapshot(result, metadata2, assessment, context2.integration),
|
|
103736
|
+
this.supersedeReplacedAttempts(db2, listing.attempts, context2.enrollment.id)
|
|
103737
|
+
]);
|
|
103738
|
+
return snapshot;
|
|
103781
103739
|
});
|
|
103782
103740
|
}
|
|
103783
103741
|
async prepareNewAttempt(params, requestHash) {
|
|
103784
103742
|
const startedAt = Date.now();
|
|
103785
103743
|
const [candidates, results] = await Promise.all([
|
|
103786
103744
|
this.candidateIntegrations(params.gameId, params.studentId, params.input),
|
|
103787
|
-
this.
|
|
103745
|
+
this.activityAssessmentResults({
|
|
103746
|
+
studentId: params.studentId,
|
|
103747
|
+
activityId: params.input.activityId,
|
|
103748
|
+
purpose: params.input.purpose
|
|
103749
|
+
})
|
|
103788
103750
|
]);
|
|
103789
|
-
const context2 = await this.selectIntegration(candidates, params.
|
|
103751
|
+
const context2 = await this.selectIntegration(candidates, params.input);
|
|
103790
103752
|
const attemptScope = {
|
|
103791
103753
|
studentId: params.studentId,
|
|
103792
103754
|
activityId: params.input.activityId,
|
|
@@ -103797,40 +103759,30 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
103797
103759
|
};
|
|
103798
103760
|
const tests = await this.liveTests(context2.integration.id, params.input.purpose, this.deps.db, params.input.purpose === "mastery" ? params.input.standard : undefined, params.input.purpose === "diagnostic" ? params.input.diagnosticKey : undefined);
|
|
103799
103761
|
const listing = await this.attemptListing(results, attemptScope);
|
|
103800
|
-
|
|
103801
|
-
|
|
103802
|
-
|
|
103803
|
-
const selection = resolveAssessmentPreparationSelection(decision, candidates.rows.length);
|
|
103804
|
-
if (selection.status === "blocked") {
|
|
103805
|
-
this.recordPreparation(selection.result.status, params.input.purpose, startedAt, {
|
|
103806
|
-
reason: selection.result.status === "canonical_required" ? selection.result.reason : "multiple_candidate_courses"
|
|
103762
|
+
if (this.pendingAwardAttempt(listing, context2.enrollment.id)) {
|
|
103763
|
+
this.recordPreparation("unprepared", params.input.purpose, startedAt, {
|
|
103764
|
+
reason: "pending_award"
|
|
103807
103765
|
});
|
|
103808
|
-
return
|
|
103766
|
+
return { status: "unprepared", reason: "prerequisite_unavailable" };
|
|
103809
103767
|
}
|
|
103810
|
-
if (
|
|
103811
|
-
|
|
103768
|
+
if (candidates.rows.length !== 1) {
|
|
103769
|
+
this.recordPreparation("unprepared", params.input.purpose, startedAt, {
|
|
103770
|
+
reason: "multiple_candidate_courses"
|
|
103771
|
+
});
|
|
103772
|
+
return { status: "unprepared", reason: "prerequisite_unavailable" };
|
|
103812
103773
|
}
|
|
103813
103774
|
const preparation = {
|
|
103814
103775
|
context: context2,
|
|
103815
103776
|
tests,
|
|
103816
103777
|
listing,
|
|
103817
|
-
decision:
|
|
103778
|
+
decision: this.selectFreshStart(tests, listing, params.input.purpose)
|
|
103818
103779
|
};
|
|
103819
103780
|
let prepared;
|
|
103820
103781
|
try {
|
|
103821
103782
|
prepared = await this.buildPreparationPlan(params, requestHash, preparation);
|
|
103822
|
-
|
|
103823
|
-
if (!assessment) {
|
|
103783
|
+
if (!await this.loadPreparedPlanAssessment(prepared.plan)) {
|
|
103824
103784
|
throw new Error("Prepared assessment artifact is unavailable");
|
|
103825
103785
|
}
|
|
103826
|
-
await this.provisionPreparationPrerequisites(() => this.ensureTranscriptLineItems({
|
|
103827
|
-
version: 1,
|
|
103828
|
-
gameId: prepared.plan.gameId,
|
|
103829
|
-
studentId: prepared.plan.studentId,
|
|
103830
|
-
attemptId: prepared.plan.attemptId,
|
|
103831
|
-
parentLineItemId: prepared.plan.parentLineItemId,
|
|
103832
|
-
metadata: buildPreparedAssessmentMetadata(prepared.plan, new Date().toISOString(), assessment.title)
|
|
103833
|
-
}, assessment));
|
|
103834
103786
|
} catch (error88) {
|
|
103835
103787
|
if (error88 instanceof ValidationError || error88 instanceof AssessmentRuntimeError) {
|
|
103836
103788
|
throw error88;
|
|
@@ -103919,6 +103871,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
103919
103871
|
courseName: context2.enrollment.course.title,
|
|
103920
103872
|
...attempt,
|
|
103921
103873
|
parentLineItemId,
|
|
103874
|
+
supersedes: [...this.replacedAttemptIds(listing.attempts, context2.enrollment.id)],
|
|
103922
103875
|
selectedTest: {
|
|
103923
103876
|
assessmentKey: decision.test.assessmentKey,
|
|
103924
103877
|
identifier: assessment.identifier,
|
|
@@ -103970,26 +103923,12 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
103970
103923
|
throw AssessmentRuntimeError.from(assessmentReviewBankShortage(selected.fulfillment));
|
|
103971
103924
|
}
|
|
103972
103925
|
const artifact = await this.loadOrCompileReviewBankArtifact(catalog);
|
|
103973
|
-
const assessment = projectReviewAssessment(artifact.assessment, catalog.bank, selected.selections);
|
|
103974
103926
|
const source = { assessment: artifact.assessment, bank: catalog.bank };
|
|
103975
103927
|
const parentLineItemId = await this.provisionPreparationPrerequisites(() => this.ensureReviewBankLineItem({
|
|
103976
103928
|
integration: context2.integration,
|
|
103977
103929
|
activityId: params.input.activityId,
|
|
103978
103930
|
source
|
|
103979
103931
|
}));
|
|
103980
|
-
await this.provisionPreparationPrerequisites(() => runWithConcurrency(assessment.items, TimebackAssessmentRuntimeService.SCORING_CONCURRENCY, (item) => {
|
|
103981
|
-
const indexed = catalog.bank.items.find((candidate) => candidate.itemIdentifier === item.identifier);
|
|
103982
|
-
if (!indexed) {
|
|
103983
|
-
throw new Error(`Review-bank index is missing item ${item.identifier}`);
|
|
103984
|
-
}
|
|
103985
|
-
return this.ensureReviewQuestionLineItem({
|
|
103986
|
-
parentLineItemId,
|
|
103987
|
-
integration: context2.integration,
|
|
103988
|
-
bankRevision: catalog.bank.bankRevision,
|
|
103989
|
-
standards: [...indexed.standards],
|
|
103990
|
-
item
|
|
103991
|
-
});
|
|
103992
|
-
}));
|
|
103993
103932
|
const attempt = await this.provisionPreparationPrerequisites(() => this.claimAttemptId({
|
|
103994
103933
|
gameId: params.gameId,
|
|
103995
103934
|
studentId: params.studentId,
|
|
@@ -104011,6 +103950,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
104011
103950
|
courseName: context2.enrollment.course.title,
|
|
104012
103951
|
...attempt,
|
|
104013
103952
|
parentLineItemId,
|
|
103953
|
+
supersedes: [...this.replacedAttemptIds(listing.attempts, context2.enrollment.id)],
|
|
104014
103954
|
selectedTest: {
|
|
104015
103955
|
assessmentKey: decision.test.assessmentKey,
|
|
104016
103956
|
identifier: artifact.assessment.identifier,
|
|
@@ -104059,10 +103999,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
104059
103999
|
if (!integration || !await this.preparedPlanTestIsLive(verified, db2)) {
|
|
104060
104000
|
return null;
|
|
104061
104001
|
}
|
|
104062
|
-
|
|
104063
|
-
return null;
|
|
104064
|
-
}
|
|
104065
|
-
return this.createPreparedAttempt(verified, assessment, integration);
|
|
104002
|
+
return this.claimPreparedAttempt(db2, verified, assessment, integration);
|
|
104066
104003
|
});
|
|
104067
104004
|
if (!outcome) {
|
|
104068
104005
|
this.recordPreparedStart("fallback", params.input.purpose, startedAt, {
|
|
@@ -104087,8 +104024,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
104087
104024
|
studentId,
|
|
104088
104025
|
input
|
|
104089
104026
|
}) {
|
|
104090
|
-
const requestFingerprint = this.startRequestFingerprint(input);
|
|
104091
|
-
const pendingSupersessions = new Set;
|
|
104092
104027
|
const candidates = await this.candidateIntegrations(gameId, studentId, input);
|
|
104093
104028
|
const lockKeys = candidates.rows.map((row) => assessmentRuntimeStartLockKey({
|
|
104094
104029
|
studentId,
|
|
@@ -104097,7 +104032,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
104097
104032
|
courseId: row.courseId
|
|
104098
104033
|
}));
|
|
104099
104034
|
return this.deps.assessmentRuntimeLock(this.deps.db, lockKeys, async (db2) => {
|
|
104100
|
-
const context2 = await this.selectIntegration(candidates,
|
|
104035
|
+
const context2 = await this.selectIntegration(candidates, input);
|
|
104101
104036
|
const attemptScope = {
|
|
104102
104037
|
studentId,
|
|
104103
104038
|
activityId: input.activityId,
|
|
@@ -104110,32 +104045,25 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
104110
104045
|
this.liveTests(context2.integration.id, input.purpose, db2, undefined, input.diagnosticKey),
|
|
104111
104046
|
this.listAttempts(attemptScope)
|
|
104112
104047
|
]);
|
|
104113
|
-
const
|
|
104114
|
-
if (
|
|
104115
|
-
|
|
104116
|
-
return initialResolution.snapshot;
|
|
104048
|
+
const pending = await this.pendingAwardSnapshot(listing, context2);
|
|
104049
|
+
if (pending) {
|
|
104050
|
+
return pending;
|
|
104117
104051
|
}
|
|
104118
|
-
|
|
104052
|
+
const decision = this.selectFreshStart(tests, listing, input.purpose);
|
|
104119
104053
|
let definition = await this.requireDiagnosticDefinition(decision.test.id, context2.integration.id, db2, true);
|
|
104120
|
-
|
|
104121
|
-
|
|
104122
|
-
|
|
104123
|
-
|
|
104124
|
-
|
|
104125
|
-
|
|
104126
|
-
|
|
104127
|
-
|
|
104128
|
-
|
|
104129
|
-
|
|
104130
|
-
|
|
104131
|
-
|
|
104132
|
-
|
|
104133
|
-
routing = await this.initializeHostedDiagnostic(definition, assessment);
|
|
104134
|
-
} else {
|
|
104135
|
-
definition = await this.requireDiagnosticDefinition(definition.id, context2.integration.id, db2, true);
|
|
104136
|
-
routing = await this.initializeHostedDiagnostic(definition, assessment);
|
|
104137
|
-
}
|
|
104138
|
-
const classId = await this.resolveClassId(context2.integration.courseId, context2.enrollment, studentId);
|
|
104054
|
+
const [assessment, classId, { attemptNumber, attemptId }] = await Promise.all([
|
|
104055
|
+
this.loadAssessment(definition.qtiTestIdentifier),
|
|
104056
|
+
this.resolveClassId(context2.integration.courseId, context2.enrollment, studentId),
|
|
104057
|
+
this.claimAttemptId({
|
|
104058
|
+
gameId,
|
|
104059
|
+
studentId,
|
|
104060
|
+
courseId: context2.integration.courseId,
|
|
104061
|
+
activityId: input.activityId,
|
|
104062
|
+
purpose: input.purpose
|
|
104063
|
+
}, nextAssessmentAttemptNumber(listing.attempts.values()))
|
|
104064
|
+
]);
|
|
104065
|
+
definition = await this.requireDiagnosticDefinition(definition.id, context2.integration.id, db2, true);
|
|
104066
|
+
const routing = await this.initializeHostedDiagnostic(definition, assessment);
|
|
104139
104067
|
const lineItemId = await this.ensureLineItem({
|
|
104140
104068
|
classId,
|
|
104141
104069
|
integration: context2.integration,
|
|
@@ -104147,13 +104075,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
104147
104075
|
routingRevision: routing.routingRevision
|
|
104148
104076
|
}
|
|
104149
104077
|
});
|
|
104150
|
-
const { attemptNumber, attemptId } = await this.claimAttemptId({
|
|
104151
|
-
gameId,
|
|
104152
|
-
studentId,
|
|
104153
|
-
courseId: context2.integration.courseId,
|
|
104154
|
-
activityId: input.activityId,
|
|
104155
|
-
purpose: input.purpose
|
|
104156
|
-
}, Math.max(0, ...[...refreshed.values()].map((entry) => entry.metadata.attemptNumber)) + 1);
|
|
104157
104078
|
const timestamp6 = new Date().toISOString();
|
|
104158
104079
|
const metadata2 = {
|
|
104159
104080
|
version: PLAYCADEMY_ASSESSMENT_RESULT_METADATA_VERSION,
|
|
@@ -104197,10 +104118,13 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
104197
104118
|
grade: context2.integration.grade
|
|
104198
104119
|
})
|
|
104199
104120
|
});
|
|
104200
|
-
await
|
|
104201
|
-
|
|
104202
|
-
|
|
104203
|
-
|
|
104121
|
+
const [snapshot] = await Promise.all([
|
|
104122
|
+
this.diagnosticSnapshot(result, metadata2, assessment, context2.integration, {
|
|
104123
|
+
state: routing.state
|
|
104124
|
+
}),
|
|
104125
|
+
this.supersedeReplacedAttempts(db2, listing.attempts, context2.enrollment.id)
|
|
104126
|
+
]);
|
|
104127
|
+
return snapshot;
|
|
104204
104128
|
});
|
|
104205
104129
|
}
|
|
104206
104130
|
async startReview({
|
|
@@ -104210,7 +104134,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
104210
104134
|
}) {
|
|
104211
104135
|
const preparationStartedAt = Date.now();
|
|
104212
104136
|
const requestFingerprint = reviewRequestFingerprint(input, DEFAULT_REVIEW_SELECTION_POLICY);
|
|
104213
|
-
const pendingSupersessions = new Set;
|
|
104214
104137
|
const normalizedRequest = normalizeReviewRequest(input);
|
|
104215
104138
|
const candidates = await this.candidateIntegrations(gameId, studentId, input);
|
|
104216
104139
|
const lockKeys = candidates.rows.map((row) => assessmentRuntimeStartLockKey({
|
|
@@ -104220,7 +104143,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
104220
104143
|
courseId: row.courseId
|
|
104221
104144
|
}));
|
|
104222
104145
|
return this.deps.assessmentRuntimeLock(this.deps.db, lockKeys, async (db2) => {
|
|
104223
|
-
const context2 = await this.selectIntegration(candidates,
|
|
104146
|
+
const context2 = await this.selectIntegration(candidates, input);
|
|
104224
104147
|
const attemptScope = {
|
|
104225
104148
|
studentId,
|
|
104226
104149
|
activityId: input.activityId,
|
|
@@ -104233,42 +104156,22 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
104233
104156
|
this.liveTests(context2.integration.id, input.purpose, db2),
|
|
104234
104157
|
this.listAttempts(attemptScope)
|
|
104235
104158
|
]);
|
|
104236
|
-
const
|
|
104237
|
-
|
|
104238
|
-
|
|
104239
|
-
await this.persistPendingSupersessionsBestEffort(db2, pendingSupersessions);
|
|
104240
|
-
return initialResolution.snapshot;
|
|
104241
|
-
}
|
|
104242
|
-
let decision = initialResolution.decision;
|
|
104243
|
-
if (tests.length === 0 || !decision) {
|
|
104244
|
-
throw AssessmentRuntimeError.from(assessmentNoEligibleTests(input.purpose));
|
|
104159
|
+
const pending = await this.pendingAwardSnapshot(listing, context2);
|
|
104160
|
+
if (pending) {
|
|
104161
|
+
return pending;
|
|
104245
104162
|
}
|
|
104163
|
+
const decision = this.selectFreshStart(tests, listing, input.purpose);
|
|
104246
104164
|
if (tests.length > 1) {
|
|
104247
104165
|
throw new ValidationError("Standards review requires exactly one live review-bank assessment.");
|
|
104248
104166
|
}
|
|
104249
104167
|
const manifestStartedAt = Date.now();
|
|
104250
|
-
|
|
104168
|
+
const catalog = await this.loadReviewBankCatalog(decision.test.qtiTestIdentifier);
|
|
104251
104169
|
this.recordReviewPreparationPhase("manifest_load", manifestStartedAt, {
|
|
104252
104170
|
candidateCount: catalog.bank.items.length
|
|
104253
104171
|
});
|
|
104254
|
-
const refreshedListing = await this.listAttempts(attemptScope);
|
|
104255
|
-
const refreshed = refreshedListing.attempts;
|
|
104256
|
-
const refreshedResolution = await this.resumeAvailableAttempt(tests, refreshed, requestFingerprint, pendingSupersessions, (resumeDecision) => this.resumeReviewSnapshot(resumeDecision, refreshed, context2));
|
|
104257
|
-
if (refreshedResolution.kind === "resumed") {
|
|
104258
|
-
await this.persistPendingSupersessionsBestEffort(db2, pendingSupersessions);
|
|
104259
|
-
return refreshedResolution.snapshot;
|
|
104260
|
-
}
|
|
104261
|
-
const refreshedDecision = refreshedResolution.decision;
|
|
104262
|
-
if (!refreshedDecision || refreshedDecision.kind !== "start") {
|
|
104263
|
-
throw AssessmentRuntimeError.from(assessmentNoEligibleTests(input.purpose));
|
|
104264
|
-
}
|
|
104265
|
-
decision = refreshedDecision;
|
|
104266
|
-
if (catalog.test.identifier !== decision.test.qtiTestIdentifier) {
|
|
104267
|
-
catalog = await this.loadReviewBankCatalog(decision.test.qtiTestIdentifier);
|
|
104268
|
-
}
|
|
104269
104172
|
const exposures = assessmentReviewTranscriptExposures({
|
|
104270
|
-
attempts:
|
|
104271
|
-
children:
|
|
104173
|
+
attempts: listing.attempts.values(),
|
|
104174
|
+
children: listing.reviewChildren.values(),
|
|
104272
104175
|
bankRevision: catalog.bank.bankRevision
|
|
104273
104176
|
});
|
|
104274
104177
|
const selected = selectReviewItems({
|
|
@@ -104289,18 +104192,20 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
104289
104192
|
});
|
|
104290
104193
|
const assessment = projectReviewAssessment(source.assessment, source.bank, selected.selections);
|
|
104291
104194
|
const provisioningStartedAt = Date.now();
|
|
104292
|
-
const parentLineItemId = await
|
|
104293
|
-
|
|
104294
|
-
|
|
104295
|
-
|
|
104296
|
-
|
|
104297
|
-
|
|
104298
|
-
|
|
104299
|
-
|
|
104300
|
-
|
|
104301
|
-
|
|
104302
|
-
|
|
104303
|
-
|
|
104195
|
+
const [parentLineItemId, { attemptNumber, attemptId }] = await Promise.all([
|
|
104196
|
+
this.ensureReviewBankLineItem({
|
|
104197
|
+
integration: context2.integration,
|
|
104198
|
+
activityId: input.activityId,
|
|
104199
|
+
source
|
|
104200
|
+
}),
|
|
104201
|
+
this.claimAttemptId({
|
|
104202
|
+
gameId,
|
|
104203
|
+
studentId,
|
|
104204
|
+
courseId: context2.integration.courseId,
|
|
104205
|
+
activityId: input.activityId,
|
|
104206
|
+
purpose: input.purpose
|
|
104207
|
+
}, nextAssessmentAttemptNumber(listing.attempts.values()))
|
|
104208
|
+
]);
|
|
104304
104209
|
const timestamp6 = new Date().toISOString();
|
|
104305
104210
|
const metadata2 = {
|
|
104306
104211
|
version: PLAYCADEMY_ASSESSMENT_RESULT_METADATA_VERSION,
|
|
@@ -104344,13 +104249,16 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
104344
104249
|
grade: context2.integration.grade
|
|
104345
104250
|
})
|
|
104346
104251
|
});
|
|
104347
|
-
await
|
|
104252
|
+
const [snapshot] = await Promise.all([
|
|
104253
|
+
this.snapshot(result, metadata2, assessment, context2.integration),
|
|
104254
|
+
this.supersedeReplacedAttempts(db2, listing.attempts, context2.enrollment.id)
|
|
104255
|
+
]);
|
|
104348
104256
|
this.recordReviewPreparationPhase("provisioning", provisioningStartedAt);
|
|
104349
104257
|
this.recordReviewPreparationPhase("total", preparationStartedAt, {
|
|
104350
104258
|
candidateCount: catalog.bank.items.length,
|
|
104351
104259
|
selectedCount: selected.selections.length
|
|
104352
104260
|
});
|
|
104353
|
-
return
|
|
104261
|
+
return snapshot;
|
|
104354
104262
|
});
|
|
104355
104263
|
}
|
|
104356
104264
|
async get(params) {
|
|
@@ -104868,28 +104776,11 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
104868
104776
|
liveTestIntegrationIds: integrationIdsWithLiveTests
|
|
104869
104777
|
};
|
|
104870
104778
|
}
|
|
104871
|
-
async selectIntegration(candidates,
|
|
104872
|
-
const {
|
|
104873
|
-
rows: candidateRows,
|
|
104874
|
-
enrollmentByCourse,
|
|
104875
|
-
liveTestIntegrationIds: integrationIdsWithLiveTests
|
|
104876
|
-
} = candidates;
|
|
104877
|
-
const needsOpenAnnotation = candidateRows.length > 1 || candidateRows.some((row) => !integrationIdsWithLiveTests.has(row.id));
|
|
104878
|
-
const openAttemptScope = {
|
|
104879
|
-
studentId,
|
|
104880
|
-
activityId: input.activityId,
|
|
104881
|
-
purpose: input.purpose,
|
|
104882
|
-
enrollmentIdByCourse: new Map(candidateRows.map((row) => [row.courseId, enrollmentByCourse.get(row.courseId).id])),
|
|
104883
|
-
requestFingerprint: this.startRequestFingerprint(input)
|
|
104884
|
-
};
|
|
104885
|
-
let openAttemptCourseIds = new Set;
|
|
104886
|
-
if (needsOpenAnnotation) {
|
|
104887
|
-
openAttemptCourseIds = results ? this.openAttemptCourseIdsFromResults(results, openAttemptScope) : await this.openAttemptCourseIds(openAttemptScope);
|
|
104888
|
-
}
|
|
104779
|
+
async selectIntegration(candidates, input) {
|
|
104780
|
+
const { rows: candidateRows, enrollmentByCourse, liveTestIntegrationIds } = candidates;
|
|
104889
104781
|
const annotated = candidateRows.map((row) => ({
|
|
104890
104782
|
...row,
|
|
104891
|
-
hasLiveTests:
|
|
104892
|
-
hasOpenAttempt: openAttemptCourseIds.has(row.courseId)
|
|
104783
|
+
hasLiveTests: liveTestIntegrationIds.has(row.id)
|
|
104893
104784
|
}));
|
|
104894
104785
|
const selection = selectAssessmentCourseCandidate(annotated, Boolean(input.subject));
|
|
104895
104786
|
if (selection.outcome === "none") {
|
|
@@ -104918,15 +104809,19 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
104918
104809
|
standard: assessmentStandardForRow(row)
|
|
104919
104810
|
})).filter((test) => !matchesMasteryStandard || matchesMasteryStandard(test.standard));
|
|
104920
104811
|
}
|
|
104921
|
-
async
|
|
104922
|
-
|
|
104812
|
+
async activityAssessmentResults(scope) {
|
|
104813
|
+
const [parents, children] = await Promise.all([
|
|
104814
|
+
this.collectAssessmentResults(buildAssessmentAttemptResultWhere(scope)),
|
|
104815
|
+
scope.purpose === "review" ? this.activityChildResults(scope) : []
|
|
104816
|
+
]);
|
|
104817
|
+
return [...parents, ...children];
|
|
104818
|
+
}
|
|
104819
|
+
activityChildResults(scope) {
|
|
104820
|
+
return this.collectAssessmentResults(buildAssessmentChildResultWhere(scope));
|
|
104923
104821
|
}
|
|
104924
|
-
async
|
|
104822
|
+
async collectAssessmentResults(where) {
|
|
104925
104823
|
const stream = this.requireClient().api.oneroster.assessmentResults.stream({
|
|
104926
|
-
where
|
|
104927
|
-
"student.sourcedId": studentId,
|
|
104928
|
-
status: ONEROSTER_STATUS2.active
|
|
104929
|
-
}
|
|
104824
|
+
where
|
|
104930
104825
|
});
|
|
104931
104826
|
const results = [];
|
|
104932
104827
|
for await (const result of stream) {
|
|
@@ -104934,18 +104829,8 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
104934
104829
|
}
|
|
104935
104830
|
return results;
|
|
104936
104831
|
}
|
|
104937
|
-
openAttemptCourseIdsFromResults(results, scope) {
|
|
104938
|
-
const courseIds = new Set;
|
|
104939
|
-
for (const result of results) {
|
|
104940
|
-
const metadata2 = playcademyAssessmentResultMetadata(result.metadata);
|
|
104941
|
-
if (metadata2 && metadata2.activityId === scope.activityId && metadata2.purpose === scope.purpose && this.attemptMatchesRequestFingerprint(metadata2, scope.requestFingerprint) && result.student.sourcedId === scope.studentId && metadata2.enrollmentId === scope.enrollmentIdByCourse.get(metadata2.courseId) && (this.isOpen(result) || this.isAwaitingAward(result))) {
|
|
104942
|
-
courseIds.add(metadata2.courseId);
|
|
104943
|
-
}
|
|
104944
|
-
}
|
|
104945
|
-
return courseIds;
|
|
104946
|
-
}
|
|
104947
104832
|
async listAttempts(scope) {
|
|
104948
|
-
return this.attemptListing(await this.
|
|
104833
|
+
return this.attemptListing(await this.activityAssessmentResults(scope), scope);
|
|
104949
104834
|
}
|
|
104950
104835
|
async attemptListing(results, scope) {
|
|
104951
104836
|
const attempts = new Map;
|
|
@@ -104974,7 +104859,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
104974
104859
|
}
|
|
104975
104860
|
}
|
|
104976
104861
|
}
|
|
104977
|
-
if (metadata2 &&
|
|
104862
|
+
if (metadata2 && this.attemptInScope(result, metadata2, scope)) {
|
|
104978
104863
|
const selectedAssessmentKey = metadata2.selectedTest.assessmentKey ?? assessmentKeyFromManagedQtiIdentifier(metadata2.selectedTest.identifier);
|
|
104979
104864
|
if (selectedAssessmentKey) {
|
|
104980
104865
|
attempts.set(result.sourcedId, {
|
|
@@ -104983,12 +104868,9 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
104983
104868
|
selection: {
|
|
104984
104869
|
attemptId: result.sourcedId,
|
|
104985
104870
|
selectedAssessmentKey,
|
|
104986
|
-
selectedTestIdentifier: metadata2.selectedTest.identifier,
|
|
104987
104871
|
inProgress: result.inProgress ?? "",
|
|
104988
104872
|
scoreStatus: result.scoreStatus,
|
|
104989
|
-
scoreDate: result.scoreDate
|
|
104990
|
-
updatedAt: result.dateLastModified ?? metadata2.updatedAt,
|
|
104991
|
-
resumable: metadata2.enrollmentId === scope.enrollmentId
|
|
104873
|
+
scoreDate: result.scoreDate
|
|
104992
104874
|
}
|
|
104993
104875
|
});
|
|
104994
104876
|
}
|
|
@@ -104996,114 +104878,61 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
104996
104878
|
}
|
|
104997
104879
|
return { attempts, reviewChildren };
|
|
104998
104880
|
}
|
|
104999
|
-
|
|
105000
|
-
|
|
105001
|
-
return attempts;
|
|
105002
|
-
}
|
|
105003
|
-
return new Map([...attempts].filter(([, attempt]) => this.attemptMatchesRequestFingerprint(attempt.metadata, requestFingerprint)));
|
|
105004
|
-
}
|
|
105005
|
-
startRequestFingerprint(input) {
|
|
105006
|
-
if (input.purpose === "review") {
|
|
105007
|
-
return reviewRequestFingerprint(input, DEFAULT_REVIEW_SELECTION_POLICY);
|
|
105008
|
-
}
|
|
105009
|
-
if (input.purpose === "mastery") {
|
|
105010
|
-
return masteryRequestFingerprint(input);
|
|
105011
|
-
}
|
|
105012
|
-
return input.purpose === "diagnostic" ? input.diagnosticKey : undefined;
|
|
105013
|
-
}
|
|
105014
|
-
attemptMatchesRequestFingerprint(metadata2, requestFingerprint) {
|
|
105015
|
-
if (metadata2.purpose === "review") {
|
|
105016
|
-
return metadata2.review.requestFingerprint === requestFingerprint;
|
|
105017
|
-
}
|
|
105018
|
-
if (metadata2.purpose === "mastery") {
|
|
105019
|
-
return metadata2.mastery.requestFingerprint === requestFingerprint;
|
|
105020
|
-
}
|
|
105021
|
-
if (metadata2.purpose === "diagnostic" && requestFingerprint !== undefined) {
|
|
105022
|
-
return this.isPlatformRoutedDiagnosticMetadata(metadata2) && metadata2.diagnostic.diagnosticKey === requestFingerprint;
|
|
105023
|
-
}
|
|
105024
|
-
return true;
|
|
105025
|
-
}
|
|
105026
|
-
requireDecision(decision, purpose) {
|
|
104881
|
+
selectFreshStart(tests, listing, purpose) {
|
|
104882
|
+
const decision = selectRuntimeAssessment(tests, [...listing.attempts.values()].map((entry) => entry.selection));
|
|
105027
104883
|
if (!decision) {
|
|
105028
104884
|
throw AssessmentRuntimeError.from(assessmentNoEligibleTests(purpose));
|
|
105029
104885
|
}
|
|
105030
104886
|
return decision;
|
|
105031
104887
|
}
|
|
105032
|
-
|
|
105033
|
-
|
|
105034
|
-
|
|
105035
|
-
"app.assessment.attempt_id": decision.attempt.attemptId,
|
|
105036
|
-
"app.assessment.additional_attempt_ids": decision.additionalResumableAttemptIds.join(",")
|
|
105037
|
-
});
|
|
105038
|
-
}
|
|
105039
|
-
const resumed = attempts.get(decision.attempt.attemptId);
|
|
105040
|
-
return this.snapshotForResult(resumed.result, resumed.metadata, integration);
|
|
105041
|
-
}
|
|
105042
|
-
async resumeDiagnosticSnapshot(decision, attempts, integration, _db = this.deps.db) {
|
|
105043
|
-
const resumed = attempts.get(decision.attempt.attemptId);
|
|
105044
|
-
return this.snapshotForResult(resumed.result, resumed.metadata, integration);
|
|
104888
|
+
pendingAwardAttempt(listing, enrollmentId) {
|
|
104889
|
+
const pending = [...listing.attempts.values()].filter((attempt) => attempt.metadata.enrollmentId === enrollmentId && this.isAwaitingAward(attempt.result));
|
|
104890
|
+
return pending.toSorted((left, right) => compareNewestCompletedAssessmentAttempt(left.selection, right.selection))[0] ?? null;
|
|
105045
104891
|
}
|
|
105046
|
-
async
|
|
105047
|
-
const
|
|
105048
|
-
|
|
104892
|
+
async pendingAwardSnapshot(listing, context2) {
|
|
104893
|
+
const pending = this.pendingAwardAttempt(listing, context2.enrollment.id);
|
|
104894
|
+
if (!pending) {
|
|
104895
|
+
return null;
|
|
104896
|
+
}
|
|
104897
|
+
addEvent("assessment.pending_award_returned", {
|
|
104898
|
+
"app.assessment.attempt_id": pending.result.sourcedId
|
|
104899
|
+
});
|
|
104900
|
+
return this.snapshotForResult(pending.result, pending.metadata, context2.integration);
|
|
105049
104901
|
}
|
|
105050
|
-
|
|
105051
|
-
|
|
105052
|
-
|
|
105053
|
-
|
|
105054
|
-
|
|
105055
|
-
throw error88;
|
|
105056
|
-
}
|
|
105057
|
-
if (shouldConfirmAssessmentVersionUnavailable(error88.details)) {
|
|
105058
|
-
await sleep(TimebackAssessmentRuntimeService.ASSESSMENT_VERSION_CONFIRMATION_DELAY_MS);
|
|
105059
|
-
try {
|
|
105060
|
-
return await resume();
|
|
105061
|
-
} catch (confirmationError) {
|
|
105062
|
-
if (!(confirmationError instanceof AssessmentRuntimeError) || confirmationError.runtimeCode !== "SELECTED_TEST_VERSION_UNAVAILABLE") {
|
|
105063
|
-
throw confirmationError;
|
|
105064
|
-
}
|
|
105065
|
-
}
|
|
104902
|
+
replacedAttemptIds(attempts, enrollmentId) {
|
|
104903
|
+
const replaced = new Set;
|
|
104904
|
+
for (const attempt of attempts.values()) {
|
|
104905
|
+
if (attempt.metadata.enrollmentId === enrollmentId && this.isOpen(attempt.result)) {
|
|
104906
|
+
replaced.add(attempt.result.sourcedId);
|
|
105066
104907
|
}
|
|
105067
|
-
const staged = attempts.get(decision.attempt.attemptId);
|
|
105068
|
-
pendingSupersessions.add(staged.result.sourcedId);
|
|
105069
|
-
stageAssessmentAttemptSupersession(staged);
|
|
105070
|
-
addEvent("assessment.attempt_supersession_staged", {
|
|
105071
|
-
"app.assessment.attempt_id": decision.attempt.attemptId,
|
|
105072
|
-
"app.assessment.reason": "selected_test_version_unavailable"
|
|
105073
|
-
});
|
|
105074
|
-
return null;
|
|
105075
104908
|
}
|
|
104909
|
+
return replaced;
|
|
104910
|
+
}
|
|
104911
|
+
supersedeReplacedAttempts(db2, attempts, enrollmentId) {
|
|
104912
|
+
return this.supersedeAttemptsBestEffort(db2, this.replacedAttemptIds(attempts, enrollmentId));
|
|
105076
104913
|
}
|
|
105077
|
-
async
|
|
104914
|
+
async supersedeAttempts(db2, attemptIds) {
|
|
105078
104915
|
for (const attemptId of [...attemptIds].toSorted()) {
|
|
105079
104916
|
await this.deps.assessmentRuntimeLock(db2, [assessmentRuntimeAttemptLockKey(attemptId)], async () => {
|
|
105080
|
-
|
|
105081
|
-
|
|
105082
|
-
result = await this.requireClient().api.oneroster.assessmentResults.get(attemptId);
|
|
105083
|
-
} catch (error88) {
|
|
105084
|
-
if (isApiError(error88) && error88.statusCode === 404) {
|
|
105085
|
-
return;
|
|
105086
|
-
}
|
|
105087
|
-
throw error88;
|
|
105088
|
-
}
|
|
105089
|
-
const update2 = buildAssessmentAttemptSupersessionUpdate(result);
|
|
104917
|
+
const result = await this.findAttemptResult(attemptId);
|
|
104918
|
+
const update2 = result && buildAssessmentAttemptSupersessionUpdate(result);
|
|
105090
104919
|
if (!update2) {
|
|
105091
104920
|
return;
|
|
105092
104921
|
}
|
|
105093
104922
|
await this.requireClient().api.oneroster.assessmentResults.upsert(attemptId, update2);
|
|
105094
104923
|
addEvent("assessment.attempt_superseded", {
|
|
105095
104924
|
"app.assessment.attempt_id": attemptId,
|
|
105096
|
-
"app.assessment.reason": "
|
|
104925
|
+
"app.assessment.reason": "replaced_by_fresh_start"
|
|
105097
104926
|
});
|
|
105098
104927
|
});
|
|
105099
104928
|
}
|
|
105100
104929
|
}
|
|
105101
|
-
async
|
|
104930
|
+
async supersedeAttemptsBestEffort(db2, attemptIds) {
|
|
105102
104931
|
if (attemptIds.size === 0) {
|
|
105103
104932
|
return;
|
|
105104
104933
|
}
|
|
105105
104934
|
try {
|
|
105106
|
-
await this.
|
|
104935
|
+
await this.supersedeAttempts(db2, attemptIds);
|
|
105107
104936
|
} catch (error88) {
|
|
105108
104937
|
addEvent("assessment.attempt_supersession_persist_failed", {
|
|
105109
104938
|
"app.assessment.attempt_ids": [...attemptIds].toSorted().join(","),
|
|
@@ -105112,10 +104941,6 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
105112
104941
|
});
|
|
105113
104942
|
}
|
|
105114
104943
|
}
|
|
105115
|
-
async resumeReviewSnapshot(decision, attempts, context2) {
|
|
105116
|
-
const resumed = attempts.get(decision.attempt.attemptId);
|
|
105117
|
-
return this.snapshotForResult(resumed.result, resumed.metadata, context2.integration);
|
|
105118
|
-
}
|
|
105119
104944
|
recordReviewPreparationPhase(phase, startedAt, counts = {}) {
|
|
105120
104945
|
addEvent("assessment.review_preparation_phase", {
|
|
105121
104946
|
"app.assessment.review_phase": phase,
|
|
@@ -105466,12 +105291,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
105466
105291
|
}
|
|
105467
105292
|
});
|
|
105468
105293
|
} catch (error88) {
|
|
105469
|
-
|
|
105470
|
-
throw error88;
|
|
105471
|
-
}
|
|
105472
|
-
addEvent("assessment.line_item_create_conflict", {
|
|
105473
|
-
"app.assessment.line_item_id": lineItemId
|
|
105474
|
-
});
|
|
105294
|
+
await this.confirmLineItemAfterRefusedCreate(lineItemId, error88, "assessment.line_item_create_conflict", () => client2.api.oneroster.assessmentLineItems.get(lineItemId));
|
|
105475
105295
|
}
|
|
105476
105296
|
return lineItemId;
|
|
105477
105297
|
}
|
|
@@ -105513,15 +105333,26 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
105513
105333
|
}
|
|
105514
105334
|
});
|
|
105515
105335
|
} catch (error88) {
|
|
105516
|
-
|
|
105517
|
-
throw error88;
|
|
105518
|
-
}
|
|
105519
|
-
addEvent("assessment.review_bank_line_item_create_conflict", {
|
|
105520
|
-
"app.assessment.line_item_id": lineItemId
|
|
105521
|
-
});
|
|
105336
|
+
await this.confirmLineItemAfterRefusedCreate(lineItemId, error88, "assessment.review_bank_line_item_create_conflict", () => client2.api.oneroster.assessmentLineItems.get(lineItemId));
|
|
105522
105337
|
}
|
|
105523
105338
|
return lineItemId;
|
|
105524
105339
|
}
|
|
105340
|
+
async confirmLineItemAfterRefusedCreate(lineItemId, createError, event, read) {
|
|
105341
|
+
let stored;
|
|
105342
|
+
try {
|
|
105343
|
+
stored = await read();
|
|
105344
|
+
} catch (readError) {
|
|
105345
|
+
if (isApiError(readError) && readError.statusCode === 404) {
|
|
105346
|
+
throw createError;
|
|
105347
|
+
}
|
|
105348
|
+
throw readError;
|
|
105349
|
+
}
|
|
105350
|
+
addEvent(event, {
|
|
105351
|
+
"app.assessment.line_item_id": lineItemId,
|
|
105352
|
+
...isApiError(createError) ? { "app.timeback.status": createError.statusCode } : {}
|
|
105353
|
+
});
|
|
105354
|
+
return stored;
|
|
105355
|
+
}
|
|
105525
105356
|
tokenContext(attempt) {
|
|
105526
105357
|
return {
|
|
105527
105358
|
version: 1,
|
|
@@ -105575,10 +105406,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
105575
105406
|
try {
|
|
105576
105407
|
await store.create({ sourcedId: id, title: item.title, ...expected });
|
|
105577
105408
|
} catch (error88) {
|
|
105578
|
-
|
|
105579
|
-
throw error88;
|
|
105580
|
-
}
|
|
105581
|
-
this.assertAssessmentChildLineItem(id, await store.get(id), expected);
|
|
105409
|
+
this.assertAssessmentChildLineItem(id, await this.confirmLineItemAfterRefusedCreate(id, error88, "assessment.question_line_item_create_conflict", () => store.get(id)), expected);
|
|
105582
105410
|
}
|
|
105583
105411
|
}
|
|
105584
105412
|
this.childLineItemCache.set(id, true);
|
|
@@ -105644,14 +105472,8 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
105644
105472
|
...expected
|
|
105645
105473
|
});
|
|
105646
105474
|
} catch (error88) {
|
|
105647
|
-
|
|
105648
|
-
throw error88;
|
|
105649
|
-
}
|
|
105650
|
-
const stored = await client2.api.oneroster.assessmentLineItems.get(lineItemId);
|
|
105475
|
+
const stored = await this.confirmLineItemAfterRefusedCreate(lineItemId, error88, "assessment.diagnostic_question_line_item_create_conflict", () => client2.api.oneroster.assessmentLineItems.get(lineItemId));
|
|
105651
105476
|
this.assertAssessmentChildLineItem(lineItemId, stored, expected);
|
|
105652
|
-
addEvent("assessment.diagnostic_question_line_item_create_conflict", {
|
|
105653
|
-
"app.assessment.line_item_id": lineItemId
|
|
105654
|
-
});
|
|
105655
105477
|
}
|
|
105656
105478
|
return lineItemId;
|
|
105657
105479
|
}
|
|
@@ -105690,14 +105512,8 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
105690
105512
|
...expected
|
|
105691
105513
|
});
|
|
105692
105514
|
} catch (error88) {
|
|
105693
|
-
|
|
105694
|
-
throw error88;
|
|
105695
|
-
}
|
|
105696
|
-
const stored = await client2.api.oneroster.assessmentLineItems.get(lineItemId);
|
|
105515
|
+
const stored = await this.confirmLineItemAfterRefusedCreate(lineItemId, error88, "assessment.review_question_line_item_create_conflict", () => client2.api.oneroster.assessmentLineItems.get(lineItemId));
|
|
105697
105516
|
this.assertAssessmentChildLineItem(lineItemId, stored, expected);
|
|
105698
|
-
addEvent("assessment.review_question_line_item_create_conflict", {
|
|
105699
|
-
"app.assessment.line_item_id": lineItemId
|
|
105700
|
-
});
|
|
105701
105517
|
}
|
|
105702
105518
|
return lineItemId;
|
|
105703
105519
|
}
|
|
@@ -105779,7 +105595,10 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
105779
105595
|
const graded = await resumeAssessmentTranscript({
|
|
105780
105596
|
suppliedTranscript,
|
|
105781
105597
|
checkpointTranscript: metadata2.transcript,
|
|
105782
|
-
recover: async () => recoverAssessmentTranscript(context2, await this.
|
|
105598
|
+
recover: async () => recoverAssessmentTranscript(context2, await this.activityChildResults({
|
|
105599
|
+
studentId: context2.studentId,
|
|
105600
|
+
activityId: context2.metadata.activityId
|
|
105601
|
+
})),
|
|
105783
105602
|
grade: (transcript) => this.gradeTranscript(context2, transcript)
|
|
105784
105603
|
});
|
|
105785
105604
|
const session2 = await this.assessmentSession(context2, graded.transcript);
|
|
@@ -105938,7 +105757,22 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
105938
105757
|
}
|
|
105939
105758
|
return projectReviewAssessment(playable, preparedReviewBankIndex(plan), plan.review.selections);
|
|
105940
105759
|
}
|
|
105941
|
-
async
|
|
105760
|
+
async claimPreparedAttempt(db2, plan, assessment, integration) {
|
|
105761
|
+
const occupant = await this.findAttemptResult(plan.attemptId);
|
|
105762
|
+
if (occupant) {
|
|
105763
|
+
const metadata3 = playcademyAssessmentResultMetadata(occupant.metadata);
|
|
105764
|
+
if (occupant.status !== ONEROSTER_STATUS2.active || !metadata3 || !this.attemptInScope(occupant, metadata3, plan) || metadata3.enrollmentId !== plan.enrollmentId || !this.isOpen(occupant) || !this.attemptMatchesPlan(metadata3, plan)) {
|
|
105765
|
+
return null;
|
|
105766
|
+
}
|
|
105767
|
+
addEvent("assessment.prepared_attempt_recovered", {
|
|
105768
|
+
"app.assessment.attempt_id": plan.attemptId
|
|
105769
|
+
});
|
|
105770
|
+
const [snapshot2] = await Promise.all([
|
|
105771
|
+
this.snapshotForResult(occupant, metadata3, integration),
|
|
105772
|
+
this.supersedeAttemptsBestEffort(db2, new Set(plan.supersedes))
|
|
105773
|
+
]);
|
|
105774
|
+
return snapshot2;
|
|
105775
|
+
}
|
|
105942
105776
|
const timestamp6 = new Date().toISOString();
|
|
105943
105777
|
const metadata2 = buildPreparedAssessmentMetadata(plan, timestamp6, assessment.title);
|
|
105944
105778
|
const result = await this.requireClient().api.oneroster.assessmentResults.upsert(plan.attemptId, {
|
|
@@ -105955,9 +105789,42 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
105955
105789
|
grade: integration.grade
|
|
105956
105790
|
})
|
|
105957
105791
|
});
|
|
105958
|
-
|
|
105959
|
-
|
|
105960
|
-
|
|
105792
|
+
const [snapshot] = await Promise.all([
|
|
105793
|
+
this.isPlatformRoutedDiagnosticMetadata(metadata2) ? this.diagnosticSnapshot(result, metadata2, assessment, integration, {
|
|
105794
|
+
state: metadata2.diagnostic.state
|
|
105795
|
+
}) : this.snapshot(result, metadata2, assessment, integration),
|
|
105796
|
+
this.supersedeAttemptsBestEffort(db2, new Set(plan.supersedes))
|
|
105797
|
+
]);
|
|
105798
|
+
return snapshot;
|
|
105799
|
+
}
|
|
105800
|
+
attemptMatchesPlan(metadata2, plan) {
|
|
105801
|
+
const planned = buildPreparedAssessmentMetadata(plan, metadata2.startedAt, "");
|
|
105802
|
+
if (metadata2.selectedTest.assessmentKey !== planned.selectedTest.assessmentKey || metadata2.selectedTest.identifier !== planned.selectedTest.identifier || metadata2.selectedTest.contentRevision !== planned.selectedTest.contentRevision) {
|
|
105803
|
+
return false;
|
|
105804
|
+
}
|
|
105805
|
+
if (metadata2.purpose === "review" && planned.purpose === "review") {
|
|
105806
|
+
return metadata2.review.requestFingerprint === planned.review.requestFingerprint && metadata2.review.bankRevision === planned.review.bankRevision && metadata2.review.selections.length === planned.review.selections.length && metadata2.review.selections.every((selection, index2) => selection.itemIdentifier === planned.review.selections[index2].itemIdentifier);
|
|
105807
|
+
}
|
|
105808
|
+
if (metadata2.purpose === "mastery" && planned.purpose === "mastery") {
|
|
105809
|
+
return metadata2.mastery.requestFingerprint === planned.mastery.requestFingerprint;
|
|
105810
|
+
}
|
|
105811
|
+
if (metadata2.purpose === "diagnostic" && plan.purpose === "diagnostic") {
|
|
105812
|
+
return this.isPlatformRoutedDiagnosticMetadata(metadata2) && metadata2.diagnostic.definitionId === plan.diagnostic.definitionId && metadata2.diagnostic.routingRevision === plan.diagnostic.routingRevision;
|
|
105813
|
+
}
|
|
105814
|
+
return metadata2.purpose === planned.purpose;
|
|
105815
|
+
}
|
|
105816
|
+
async findAttemptResult(attemptId) {
|
|
105817
|
+
try {
|
|
105818
|
+
return await this.requireClient().api.oneroster.assessmentResults.get(attemptId);
|
|
105819
|
+
} catch (error88) {
|
|
105820
|
+
if (isApiError(error88) && error88.statusCode === 404) {
|
|
105821
|
+
return null;
|
|
105822
|
+
}
|
|
105823
|
+
throw error88;
|
|
105824
|
+
}
|
|
105825
|
+
}
|
|
105826
|
+
attemptInScope(result, metadata2, scope) {
|
|
105827
|
+
return metadata2.activityId === scope.activityId && metadata2.purpose === scope.purpose && metadata2.courseId === scope.courseId && metadata2.integrationId === scope.integrationId && result.student.sourcedId === scope.studentId;
|
|
105961
105828
|
}
|
|
105962
105829
|
async claimAttemptId(scope, from) {
|
|
105963
105830
|
for (let attemptNumber = from;attemptNumber < from + 20; attemptNumber += 1) {
|
|
@@ -105973,15 +105840,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
105973
105840
|
throw new Error(`Assessment attempt numbers ${from} through ${from + 19} are all occupied by records this code cannot read; refusing to overwrite them.`);
|
|
105974
105841
|
}
|
|
105975
105842
|
async attemptIdOccupied(attemptId) {
|
|
105976
|
-
|
|
105977
|
-
await this.requireClient().api.oneroster.assessmentResults.get(attemptId);
|
|
105978
|
-
} catch (error88) {
|
|
105979
|
-
if (isApiError(error88) && error88.statusCode === 404) {
|
|
105980
|
-
return false;
|
|
105981
|
-
}
|
|
105982
|
-
throw error88;
|
|
105983
|
-
}
|
|
105984
|
-
return true;
|
|
105843
|
+
return await this.findAttemptResult(attemptId) !== null;
|
|
105985
105844
|
}
|
|
105986
105845
|
replayCompletion(attempt, submissionId, params, game2) {
|
|
105987
105846
|
const { score, completion } = attempt.metadata;
|