@playcademy/sandbox 0.7.1-beta.22 → 0.7.1-beta.23
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 +563 -184
- package/dist/server.js +563 -184
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -1122,7 +1122,7 @@ var package_default;
|
|
|
1122
1122
|
var init_package = __esm(() => {
|
|
1123
1123
|
package_default = {
|
|
1124
1124
|
name: "@playcademy/sandbox",
|
|
1125
|
-
version: "0.7.1-beta.
|
|
1125
|
+
version: "0.7.1-beta.23",
|
|
1126
1126
|
description: "Local development server for Playcademy game development",
|
|
1127
1127
|
type: "module",
|
|
1128
1128
|
exports: {
|
|
@@ -9455,6 +9455,9 @@ function isAssessmentAttemptOpen(attempt) {
|
|
|
9455
9455
|
function isAssessmentAttemptCompleted(attempt) {
|
|
9456
9456
|
return attempt.inProgress === ASSESSMENT_ATTEMPT_COMPLETED.inProgress && attempt.scoreStatus === ASSESSMENT_ATTEMPT_COMPLETED.scoreStatus;
|
|
9457
9457
|
}
|
|
9458
|
+
function isAssessmentAttemptSuperseded(attempt) {
|
|
9459
|
+
return attempt.inProgress === ASSESSMENT_ATTEMPT_SUPERSEDED.inProgress && attempt.scoreStatus === ASSESSMENT_ATTEMPT_SUPERSEDED.scoreStatus;
|
|
9460
|
+
}
|
|
9458
9461
|
function classifyAssessmentSubmission(attempt, submissionId) {
|
|
9459
9462
|
if (isAssessmentAttemptCompleted(attempt)) {
|
|
9460
9463
|
return attempt.submissionId === submissionId ? "replay" : "reject";
|
|
@@ -9512,6 +9515,15 @@ function assessmentReviewBankShortage(fulfillment) {
|
|
|
9512
9515
|
function assessmentFlowForPurpose(purpose) {
|
|
9513
9516
|
return purpose === "review" ? "item-submit" : "attempt-submit";
|
|
9514
9517
|
}
|
|
9518
|
+
function compareCodeUnits(left, right) {
|
|
9519
|
+
if (left < right) {
|
|
9520
|
+
return -1;
|
|
9521
|
+
}
|
|
9522
|
+
if (left > right) {
|
|
9523
|
+
return 1;
|
|
9524
|
+
}
|
|
9525
|
+
return 0;
|
|
9526
|
+
}
|
|
9515
9527
|
function canonicalJson(value) {
|
|
9516
9528
|
if (value === null || typeof value !== "object") {
|
|
9517
9529
|
return JSON.stringify(value) ?? "null";
|
|
@@ -9519,18 +9531,9 @@ function canonicalJson(value) {
|
|
|
9519
9531
|
if (Array.isArray(value)) {
|
|
9520
9532
|
return `[${value.map((item) => canonicalJson(item)).join(",")}]`;
|
|
9521
9533
|
}
|
|
9522
|
-
const entries = Object.entries(value).filter(([, entryValue]) => entryValue !== undefined).toSorted(([left], [right]) =>
|
|
9534
|
+
const entries = Object.entries(value).filter(([, entryValue]) => entryValue !== undefined).toSorted(([left], [right]) => compareCodeUnits(left, right)).map(([key, entryValue]) => `${JSON.stringify(key)}:${canonicalJson(entryValue)}`);
|
|
9523
9535
|
return `{${entries.join(",")}}`;
|
|
9524
9536
|
}
|
|
9525
|
-
function compareCanonicalKeys(left, right) {
|
|
9526
|
-
if (left < right) {
|
|
9527
|
-
return -1;
|
|
9528
|
-
}
|
|
9529
|
-
if (left > right) {
|
|
9530
|
-
return 1;
|
|
9531
|
-
}
|
|
9532
|
-
return 0;
|
|
9533
|
-
}
|
|
9534
9537
|
async function diagnosticRoutingRevision(manifest) {
|
|
9535
9538
|
const digest = await crypto.subtle.digest("SHA-256", new TextEncoder().encode(canonicalJson(manifest)));
|
|
9536
9539
|
return [...new Uint8Array(digest)].map((byte) => byte.toString(16).padStart(2, "0")).join("");
|
|
@@ -10830,15 +10833,6 @@ function assessmentPresentationForAttempt(assessment, attemptId) {
|
|
|
10830
10833
|
});
|
|
10831
10834
|
return { ...assessment, items };
|
|
10832
10835
|
}
|
|
10833
|
-
function compareCodeUnits(left, right) {
|
|
10834
|
-
if (left < right) {
|
|
10835
|
-
return -1;
|
|
10836
|
-
}
|
|
10837
|
-
if (left > right) {
|
|
10838
|
-
return 1;
|
|
10839
|
-
}
|
|
10840
|
-
return 0;
|
|
10841
|
-
}
|
|
10842
10836
|
function reviewStandardFieldsWithinLimits(input) {
|
|
10843
10837
|
return input.framework.trim().length <= TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.standardFieldLength && input.identifier.trim().length <= TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.standardFieldLength;
|
|
10844
10838
|
}
|
|
@@ -10849,6 +10843,19 @@ function canonicalReviewStandardRef(input) {
|
|
|
10849
10843
|
}
|
|
10850
10844
|
return standard.framework && standard.identifier ? standard : null;
|
|
10851
10845
|
}
|
|
10846
|
+
function isKnownDescriptiveCurriculumFramework(framework) {
|
|
10847
|
+
return framework === "CCSS" || framework.startsWith("CCSS.");
|
|
10848
|
+
}
|
|
10849
|
+
function reviewRoutingStandardRefsFromMetadata(metadata2, options = {}) {
|
|
10850
|
+
const refs = new Map;
|
|
10851
|
+
for (const authored of qtiAssessmentStandardRefsFromMetadata(metadata2)) {
|
|
10852
|
+
const standard = canonicalReviewStandardRef(authored);
|
|
10853
|
+
if (standard && (!options.customOnly || !isKnownDescriptiveCurriculumFramework(standard.framework))) {
|
|
10854
|
+
refs.set(assessmentStandardRefKey(standard), standard);
|
|
10855
|
+
}
|
|
10856
|
+
}
|
|
10857
|
+
return [...refs.values()];
|
|
10858
|
+
}
|
|
10852
10859
|
function canonicalRequestStandards(standards) {
|
|
10853
10860
|
if (standards.length > TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.standards) {
|
|
10854
10861
|
throw new RangeError(`A review request may contain at most ${TIMEBACK_ASSESSMENT_REVIEW_REQUEST_LIMITS.standards} standards`);
|
|
@@ -10904,42 +10911,172 @@ function qtiQuestionByIdentifier(questions) {
|
|
|
10904
10911
|
}
|
|
10905
10912
|
return byIdentifier;
|
|
10906
10913
|
}
|
|
10907
|
-
async function
|
|
10914
|
+
async function reviewBankRevision(input) {
|
|
10915
|
+
const alignmentSignature = input.items.map((item) => ({
|
|
10916
|
+
itemIdentifier: item.itemIdentifier,
|
|
10917
|
+
standardKeys: item.standards.map(assessmentStandardRefKey)
|
|
10918
|
+
}));
|
|
10919
|
+
const bankRevisionUuid = await deterministicUUID(`playcademy:review-bank:v1:${JSON.stringify({
|
|
10920
|
+
bankIdentifier: input.bankIdentifier,
|
|
10921
|
+
contentRevision: input.sourceContentRevision,
|
|
10922
|
+
alignments: alignmentSignature
|
|
10923
|
+
})}`);
|
|
10924
|
+
return `review-bank-v1:${bankRevisionUuid}`;
|
|
10925
|
+
}
|
|
10926
|
+
async function reviewBankIndex(input) {
|
|
10927
|
+
return {
|
|
10928
|
+
...input,
|
|
10929
|
+
bankRevision: await reviewBankRevision(input)
|
|
10930
|
+
};
|
|
10931
|
+
}
|
|
10932
|
+
async function reviewBankSourceFingerprint(input) {
|
|
10933
|
+
const fingerprintUuid = await deterministicUUID(`playcademy:review-bank-source:v1:${JSON.stringify({
|
|
10934
|
+
bankIdentifier: input.bankIdentifier,
|
|
10935
|
+
items: input.items.map((item) => ({
|
|
10936
|
+
itemIdentifier: item.itemIdentifier,
|
|
10937
|
+
standardKeys: item.standards.map(assessmentStandardRefKey)
|
|
10938
|
+
}))
|
|
10939
|
+
})}`);
|
|
10940
|
+
return `review-bank-source-v1:${fingerprintUuid}`;
|
|
10941
|
+
}
|
|
10942
|
+
async function buildReviewBankIndex(assessment, questions, options = {}) {
|
|
10908
10943
|
if (assessment.identifier !== questions.assessmentTest) {
|
|
10909
10944
|
throw new Error(`QTI questions for ${questions.assessmentTest} do not describe assessment ${assessment.identifier}`);
|
|
10910
10945
|
}
|
|
10911
10946
|
const questionByIdentifier = qtiQuestionByIdentifier(questions);
|
|
10912
10947
|
const items = assessment.items.map((item) => {
|
|
10913
10948
|
const question = questionByIdentifier.get(item.identifier);
|
|
10914
|
-
const
|
|
10915
|
-
for (const authored of qtiAssessmentStandardRefsFromMetadata(question?.metadata)) {
|
|
10916
|
-
const standard = canonicalReviewStandardRef(authored);
|
|
10917
|
-
if (standard) {
|
|
10918
|
-
canonical.set(assessmentStandardRefKey(standard), standard);
|
|
10919
|
-
}
|
|
10920
|
-
}
|
|
10921
|
-
const standards = [...canonical.values()].toSorted((left, right) => compareCodeUnits(assessmentStandardRefKey(left), assessmentStandardRefKey(right)));
|
|
10949
|
+
const standards = reviewRoutingStandardRefsFromMetadata(question?.metadata, options).toSorted((left, right) => compareCodeUnits(assessmentStandardRefKey(left), assessmentStandardRefKey(right)));
|
|
10922
10950
|
return {
|
|
10923
10951
|
itemIdentifier: item.identifier,
|
|
10924
10952
|
standards
|
|
10925
10953
|
};
|
|
10926
10954
|
});
|
|
10927
|
-
|
|
10928
|
-
itemIdentifier: item.itemIdentifier,
|
|
10929
|
-
standardKeys: item.standards.map(assessmentStandardRefKey)
|
|
10930
|
-
}));
|
|
10931
|
-
const bankRevisionUuid = await deterministicUUID(`playcademy:review-bank:v1:${JSON.stringify({
|
|
10932
|
-
bankIdentifier: assessment.identifier,
|
|
10933
|
-
contentRevision: assessment.contentRevision,
|
|
10934
|
-
alignments: alignmentSignature
|
|
10935
|
-
})}`);
|
|
10936
|
-
return {
|
|
10955
|
+
return reviewBankIndex({
|
|
10937
10956
|
bankIdentifier: assessment.identifier,
|
|
10938
10957
|
sourceContentRevision: assessment.contentRevision,
|
|
10939
|
-
bankRevision: `review-bank-v1:${bankRevisionUuid}`,
|
|
10940
10958
|
items
|
|
10959
|
+
});
|
|
10960
|
+
}
|
|
10961
|
+
function emptyReviewBankManifest(bankIdentifier) {
|
|
10962
|
+
return {
|
|
10963
|
+
version: PLAYCADEMY_REVIEW_BANK_MANIFEST_VERSION,
|
|
10964
|
+
bankIdentifier,
|
|
10965
|
+
sourceFingerprint: null,
|
|
10966
|
+
sourceContentRevision: null,
|
|
10967
|
+
bankRevision: null,
|
|
10968
|
+
itemsByStandard: {}
|
|
10941
10969
|
};
|
|
10942
10970
|
}
|
|
10971
|
+
function reviewBankMappingIssues(bank) {
|
|
10972
|
+
return {
|
|
10973
|
+
missingOrInvalidItemIdentifiers: bank.items.filter((item) => item.standards.length === 0).map((item) => item.itemIdentifier),
|
|
10974
|
+
multipleStandardItemIdentifiers: bank.items.filter((item) => item.standards.length > 1).map((item) => item.itemIdentifier)
|
|
10975
|
+
};
|
|
10976
|
+
}
|
|
10977
|
+
async function buildReviewBankManifest(bank) {
|
|
10978
|
+
const itemsByStandard = new Map;
|
|
10979
|
+
for (const item of bank.items) {
|
|
10980
|
+
for (const standard of item.standards) {
|
|
10981
|
+
const key = assessmentStandardRefKey(standard);
|
|
10982
|
+
const identifiers = itemsByStandard.get(key) ?? [];
|
|
10983
|
+
identifiers.push(item.itemIdentifier);
|
|
10984
|
+
itemsByStandard.set(key, identifiers);
|
|
10985
|
+
}
|
|
10986
|
+
}
|
|
10987
|
+
return {
|
|
10988
|
+
version: PLAYCADEMY_REVIEW_BANK_MANIFEST_VERSION,
|
|
10989
|
+
bankIdentifier: bank.bankIdentifier,
|
|
10990
|
+
sourceFingerprint: await reviewBankSourceFingerprint(bank),
|
|
10991
|
+
sourceContentRevision: bank.sourceContentRevision,
|
|
10992
|
+
bankRevision: bank.bankRevision,
|
|
10993
|
+
itemsByStandard: Object.fromEntries([...itemsByStandard.entries()].toSorted(([left], [right]) => compareCodeUnits(left, right)))
|
|
10994
|
+
};
|
|
10995
|
+
}
|
|
10996
|
+
function requiredManifestString(record, field) {
|
|
10997
|
+
const value = record[field];
|
|
10998
|
+
if (typeof value !== "string" || !value.trim()) {
|
|
10999
|
+
throw new Error(`Review-bank manifest requires ${field}`);
|
|
11000
|
+
}
|
|
11001
|
+
return value.trim();
|
|
11002
|
+
}
|
|
11003
|
+
function standardFromManifestKey(key) {
|
|
11004
|
+
try {
|
|
11005
|
+
const parsed = JSON.parse(key);
|
|
11006
|
+
if (!Array.isArray(parsed) || parsed.length !== 2 || typeof parsed[0] !== "string" || typeof parsed[1] !== "string") {
|
|
11007
|
+
return null;
|
|
11008
|
+
}
|
|
11009
|
+
const standard = canonicalReviewStandardRef({
|
|
11010
|
+
framework: parsed[0],
|
|
11011
|
+
identifier: parsed[1]
|
|
11012
|
+
});
|
|
11013
|
+
return standard && assessmentStandardRefKey(standard) === key ? standard : null;
|
|
11014
|
+
} catch {
|
|
11015
|
+
return null;
|
|
11016
|
+
}
|
|
11017
|
+
}
|
|
11018
|
+
async function reviewBankIndexFromManifest(metadata2, input) {
|
|
11019
|
+
const raw = isRecord(metadata2) ? metadata2[PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY] : undefined;
|
|
11020
|
+
if (raw === undefined) {
|
|
11021
|
+
return null;
|
|
11022
|
+
}
|
|
11023
|
+
if (!isRecord(raw) || raw.version !== PLAYCADEMY_REVIEW_BANK_MANIFEST_VERSION) {
|
|
11024
|
+
throw new Error("Review-bank manifest has an unsupported version or shape");
|
|
11025
|
+
}
|
|
11026
|
+
const manifest = raw;
|
|
11027
|
+
const bankIdentifier = requiredManifestString(manifest, "bankIdentifier");
|
|
11028
|
+
if (bankIdentifier !== input.bankIdentifier) {
|
|
11029
|
+
throw new Error(`Review-bank manifest for ${bankIdentifier} does not describe ${input.bankIdentifier}`);
|
|
11030
|
+
}
|
|
11031
|
+
if (manifest.sourceFingerprint === null && manifest.sourceContentRevision === null && manifest.bankRevision === null) {
|
|
11032
|
+
return null;
|
|
11033
|
+
}
|
|
11034
|
+
const sourceFingerprint = requiredManifestString(manifest, "sourceFingerprint");
|
|
11035
|
+
const sourceContentRevision = requiredManifestString(manifest, "sourceContentRevision");
|
|
11036
|
+
const declaredBankRevision = requiredManifestString(manifest, "bankRevision");
|
|
11037
|
+
const itemsByStandard = manifest.itemsByStandard;
|
|
11038
|
+
if (!isRecord(itemsByStandard)) {
|
|
11039
|
+
throw new Error("Review-bank manifest requires an itemsByStandard mapping");
|
|
11040
|
+
}
|
|
11041
|
+
const membership = new Set(input.membershipItemIdentifiers);
|
|
11042
|
+
const standardsByItem = new Map;
|
|
11043
|
+
for (const [standardKey, rawItemIdentifiers] of Object.entries(itemsByStandard)) {
|
|
11044
|
+
const standard = standardFromManifestKey(standardKey);
|
|
11045
|
+
if (!standard || !Array.isArray(rawItemIdentifiers)) {
|
|
11046
|
+
throw new Error("Review-bank manifest contains an invalid standard mapping");
|
|
11047
|
+
}
|
|
11048
|
+
for (const rawItemIdentifier of rawItemIdentifiers) {
|
|
11049
|
+
if (typeof rawItemIdentifier !== "string" || !rawItemIdentifier.trim()) {
|
|
11050
|
+
throw new Error("Review-bank manifest contains an invalid item identifier");
|
|
11051
|
+
}
|
|
11052
|
+
const itemIdentifier = rawItemIdentifier.trim();
|
|
11053
|
+
if (membership.has(itemIdentifier)) {
|
|
11054
|
+
const standards = standardsByItem.get(itemIdentifier) ?? new Map;
|
|
11055
|
+
standards.set(assessmentStandardRefKey(standard), standard);
|
|
11056
|
+
standardsByItem.set(itemIdentifier, standards);
|
|
11057
|
+
}
|
|
11058
|
+
}
|
|
11059
|
+
}
|
|
11060
|
+
const index = await reviewBankIndex({
|
|
11061
|
+
bankIdentifier,
|
|
11062
|
+
sourceContentRevision,
|
|
11063
|
+
items: input.membershipItemIdentifiers.map((itemIdentifier) => ({
|
|
11064
|
+
itemIdentifier,
|
|
11065
|
+
standards: [...standardsByItem.get(itemIdentifier)?.values() ?? []].toSorted((left, right) => compareCodeUnits(assessmentStandardRefKey(left), assessmentStandardRefKey(right)))
|
|
11066
|
+
}))
|
|
11067
|
+
});
|
|
11068
|
+
const issues = reviewBankMappingIssues(index);
|
|
11069
|
+
if (issues.missingOrInvalidItemIdentifiers.length > 0 || issues.multipleStandardItemIdentifiers.length > 0) {
|
|
11070
|
+
throw new Error("Review-bank manifest must map every member to exactly one standard/node key");
|
|
11071
|
+
}
|
|
11072
|
+
if (await reviewBankSourceFingerprint(index) !== sourceFingerprint) {
|
|
11073
|
+
throw new Error("Review-bank manifest source fingerprint is stale");
|
|
11074
|
+
}
|
|
11075
|
+
if (index.bankRevision !== declaredBankRevision) {
|
|
11076
|
+
throw new Error("Review-bank manifest revision does not match its candidate index");
|
|
11077
|
+
}
|
|
11078
|
+
return index;
|
|
11079
|
+
}
|
|
10943
11080
|
function exposureTimestamp(value) {
|
|
10944
11081
|
const timestamp = Date.parse(value);
|
|
10945
11082
|
return Number.isFinite(timestamp) ? timestamp : 0;
|
|
@@ -11390,7 +11527,7 @@ function playcademyDiagnosticAssessmentItemResultMetadata(value) {
|
|
|
11390
11527
|
const normalized = metadata2.responses === undefined ? { ...metadata2, responses: {} } : metadata2;
|
|
11391
11528
|
return isPlaycademyDiagnosticAssessmentItemResultMetadataV1(normalized) ? normalized : null;
|
|
11392
11529
|
}
|
|
11393
|
-
var SCRIPT_SCHEMES, DATA_RASTER_IMAGE_PATTERN, QTI_MATHML_ALLOWED_TAGS, metadataSymbol, parser, SUPPORTED_QTI_INTERACTION_TYPES, URL_ATTRIBUTES, IMAGE_URL_ATTRIBUTES, INTERACTION_TYPES, EMPHASIS_TAGS, STRONG_TAGS, CONTENT_SKIPPED_TAGS, BLOCK_CONTENT_KINDS, GAP_MATCH_TOKEN_TAGS, STAGE_GRAPHIC_EXCLUDED_CONTAINERS, BLANK_SENTINEL = "", MARKED_BLANK_PATTERN, MARKED_BLANK_GLOBAL_PATTERN, MATCH_CORRECT_TEMPLATE = "http://www.imsglobal.org/question/qti_v3p0/rptemplates/match_correct", MAP_RESPONSE_TEMPLATE = "https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/map_response", MAP_RESPONSE_TEMPLATES, RESPONSE_PROCESSING_TEMPLATES, VOID_BODY_ELEMENTS, URL_ATTRIBUTES2, IMAGE_URL_ATTRIBUTES2, VOID_CHOICE_ELEMENTS, NUMERIC_COMPARISON_ATTRIBUTES, RESPONSE_CARDINALITIES, RESPONSE_BASE_TYPES, POINT_INTERACTION_TYPES, INDEPENDENT_PROCESSING_TAGS, UUID_PATTERN, COMMON_CORE_MATH_FRAMEWORK_ALIASES, COMMON_CORE_ELA_FRAMEWORK_ALIASES, COMMON_CORE_FRAMEWORK_ALIASES, ASSESSMENT_ATTEMPT_OPEN, ASSESSMENT_ATTEMPT_COMPLETED, ASSESSMENT_RUNTIME_ERROR_STATUS, ROUTING_KEY_MAX_LENGTH = 128, ROUTING_RESULT_KEY_MAX_LENGTH = 256, ROUTING_STAGE_LIMIT = 32, ROUTING_TRACK_LIMIT = 128, ROUTING_NODE_LIMIT = 1000, ROUTING_TERMINAL_LIMIT = 1000, ROUTING_GROUP_LIMIT = 128, ROUTING_PREDICATE_DEPTH_LIMIT = 8, ROUTING_EXPLORATION_STATE_LIMIT = 200000, RoutingKeySchema, RoutingResultKeySchema, DiagnosticTrackOutcomeSchema, DiagnosticActivationPredicateSchema, DiagnosticRoutingTransitionSchema, DiagnosticRoutingManifestV1Schema, GRADE_VALUES, POINT_RESPONSE_PATTERN, REVIEW_SELECTION_POLICY_VERSION = "unseen-first-least-recently-delivered-v1", DEFAULT_REVIEW_SELECTION_POLICY, RuntimeSubjectSchema, RuntimeGradeSchema, OptionalQueryGradeSchema, AssessmentResponseValueSchema, AssessmentRuntimeIdentitySchema, ResponseKeySchema, StartAssessmentBaseSchema, AssessmentStandardRefSchema, LatestAssessmentFilterBaseSchema, LatestAssessmentFiltersSchema, LatestRuntimeAssessmentQuerySchema, StartAssessmentBodySchema, SaveAssessmentBodySchema, SubmitAssessmentItemBodySchema, SubmitAssessmentBodySchema, StartRuntimeAssessmentRequestSchema, SaveRuntimeAssessmentRequestSchema, SubmitAssessmentItemRuntimeRequestSchema, SubmitRuntimeAssessmentRequestSchema;
|
|
11530
|
+
var SCRIPT_SCHEMES, DATA_RASTER_IMAGE_PATTERN, QTI_MATHML_ALLOWED_TAGS, metadataSymbol, parser, SUPPORTED_QTI_INTERACTION_TYPES, URL_ATTRIBUTES, IMAGE_URL_ATTRIBUTES, INTERACTION_TYPES, EMPHASIS_TAGS, STRONG_TAGS, CONTENT_SKIPPED_TAGS, BLOCK_CONTENT_KINDS, GAP_MATCH_TOKEN_TAGS, STAGE_GRAPHIC_EXCLUDED_CONTAINERS, BLANK_SENTINEL = "", MARKED_BLANK_PATTERN, MARKED_BLANK_GLOBAL_PATTERN, MATCH_CORRECT_TEMPLATE = "http://www.imsglobal.org/question/qti_v3p0/rptemplates/match_correct", MAP_RESPONSE_TEMPLATE = "https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/map_response", MAP_RESPONSE_TEMPLATES, RESPONSE_PROCESSING_TEMPLATES, VOID_BODY_ELEMENTS, URL_ATTRIBUTES2, IMAGE_URL_ATTRIBUTES2, VOID_CHOICE_ELEMENTS, NUMERIC_COMPARISON_ATTRIBUTES, RESPONSE_CARDINALITIES, RESPONSE_BASE_TYPES, POINT_INTERACTION_TYPES, INDEPENDENT_PROCESSING_TAGS, UUID_PATTERN, COMMON_CORE_MATH_FRAMEWORK_ALIASES, COMMON_CORE_ELA_FRAMEWORK_ALIASES, COMMON_CORE_FRAMEWORK_ALIASES, ASSESSMENT_ATTEMPT_OPEN, ASSESSMENT_ATTEMPT_COMPLETED, ASSESSMENT_ATTEMPT_SUPERSEDED, ASSESSMENT_RUNTIME_ERROR_STATUS, ROUTING_KEY_MAX_LENGTH = 128, ROUTING_RESULT_KEY_MAX_LENGTH = 256, ROUTING_STAGE_LIMIT = 32, ROUTING_TRACK_LIMIT = 128, ROUTING_NODE_LIMIT = 1000, ROUTING_TERMINAL_LIMIT = 1000, ROUTING_GROUP_LIMIT = 128, ROUTING_PREDICATE_DEPTH_LIMIT = 8, ROUTING_EXPLORATION_STATE_LIMIT = 200000, RoutingKeySchema, RoutingResultKeySchema, DiagnosticTrackOutcomeSchema, DiagnosticActivationPredicateSchema, DiagnosticRoutingTransitionSchema, DiagnosticRoutingManifestV1Schema, GRADE_VALUES, POINT_RESPONSE_PATTERN, REVIEW_SELECTION_POLICY_VERSION = "unseen-first-least-recently-delivered-v1", PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY = "playcademyReviewBank", PLAYCADEMY_REVIEW_BANK_MANIFEST_VERSION = 1, DEFAULT_REVIEW_SELECTION_POLICY, RuntimeSubjectSchema, RuntimeGradeSchema, OptionalQueryGradeSchema, AssessmentResponseValueSchema, AssessmentRuntimeIdentitySchema, ResponseKeySchema, StartAssessmentBaseSchema, AssessmentStandardRefSchema, LatestAssessmentFilterBaseSchema, LatestAssessmentFiltersSchema, LatestRuntimeAssessmentQuerySchema, StartAssessmentBodySchema, SaveAssessmentBodySchema, SubmitAssessmentItemBodySchema, SubmitAssessmentBodySchema, StartRuntimeAssessmentRequestSchema, SaveRuntimeAssessmentRequestSchema, SubmitAssessmentItemRuntimeRequestSchema, SubmitRuntimeAssessmentRequestSchema;
|
|
11394
11531
|
var init_assessment_runtime2 = __esm(() => {
|
|
11395
11532
|
init_timeback3();
|
|
11396
11533
|
init_timeback3();
|
|
@@ -11575,6 +11712,10 @@ var init_assessment_runtime2 = __esm(() => {
|
|
|
11575
11712
|
inProgress: "false",
|
|
11576
11713
|
scoreStatus: "fully graded"
|
|
11577
11714
|
};
|
|
11715
|
+
ASSESSMENT_ATTEMPT_SUPERSEDED = {
|
|
11716
|
+
inProgress: "false",
|
|
11717
|
+
scoreStatus: "not submitted"
|
|
11718
|
+
};
|
|
11578
11719
|
ASSESSMENT_RUNTIME_ERROR_STATUS = {
|
|
11579
11720
|
NO_ELIGIBLE_TESTS: 404,
|
|
11580
11721
|
REVIEW_BANK_SHORTAGE: 422,
|
|
@@ -89177,73 +89318,6 @@ function stringField2(value) {
|
|
|
89177
89318
|
function firstStringField2(...values) {
|
|
89178
89319
|
return values.map(stringField2).find(Boolean) ?? "";
|
|
89179
89320
|
}
|
|
89180
|
-
function normalizedWhitespace2(value) {
|
|
89181
|
-
return value.normalize("NFKC").trim().replace(/\s+/g, " ");
|
|
89182
|
-
}
|
|
89183
|
-
function normalizedIdentityCase2(value) {
|
|
89184
|
-
return value.toLocaleUpperCase("en-US");
|
|
89185
|
-
}
|
|
89186
|
-
function frameworkAliasKey2(value) {
|
|
89187
|
-
return value.normalize("NFKC").toLocaleLowerCase("en-US").replace(/[^a-z0-9]+/g, "");
|
|
89188
|
-
}
|
|
89189
|
-
function isUuidShaped2(value) {
|
|
89190
|
-
return UUID_PATTERN2.test(value.trim());
|
|
89191
|
-
}
|
|
89192
|
-
function isCommonCoreMathIdentifier2(identifier) {
|
|
89193
|
-
const canonical = normalizedIdentityCase2(normalizedWhitespace2(identifier));
|
|
89194
|
-
return canonical.startsWith("CCSS.MATH.") || /^(?:K|[1-8])\.(?:CC|OA|NBT|NF|MD|RP|NS|EE|F|G|SP)\./.test(canonical) || /^(?:HS)?(?:N|A|F|G|S)-[A-Z]+\./.test(canonical) || /^MP\.?\d/.test(canonical);
|
|
89195
|
-
}
|
|
89196
|
-
function isCommonCoreElaIdentifier2(identifier) {
|
|
89197
|
-
const canonical = normalizedIdentityCase2(normalizedWhitespace2(identifier));
|
|
89198
|
-
return canonical.startsWith("CCSS.ELA-LITERACY.") || /^(?:RL|RI|RF|W|SL|L)\.(?:K|[1-9]|1[0-2])\./.test(canonical) || /^(?:RH|RST|WHST)\.(?:6-8|9-10|11-12)\./.test(canonical) || /^CCRA\.(?:R|W|SL|L)\./.test(canonical);
|
|
89199
|
-
}
|
|
89200
|
-
function canonicalFramework2(authoredFramework, identifier) {
|
|
89201
|
-
const aliasKey = frameworkAliasKey2(authoredFramework);
|
|
89202
|
-
if (COMMON_CORE_MATH_FRAMEWORK_ALIASES2.has(aliasKey) || COMMON_CORE_FRAMEWORK_ALIASES2.has(aliasKey) && isCommonCoreMathIdentifier2(identifier)) {
|
|
89203
|
-
return "CCSS.Math";
|
|
89204
|
-
}
|
|
89205
|
-
if (COMMON_CORE_ELA_FRAMEWORK_ALIASES2.has(aliasKey) || COMMON_CORE_FRAMEWORK_ALIASES2.has(aliasKey) && isCommonCoreElaIdentifier2(identifier)) {
|
|
89206
|
-
return "CCSS.ELA-Literacy";
|
|
89207
|
-
}
|
|
89208
|
-
if (COMMON_CORE_FRAMEWORK_ALIASES2.has(aliasKey)) {
|
|
89209
|
-
return "CCSS";
|
|
89210
|
-
}
|
|
89211
|
-
return normalizedIdentityCase2(authoredFramework);
|
|
89212
|
-
}
|
|
89213
|
-
function canonicalIdentifier2(framework, authoredIdentifier) {
|
|
89214
|
-
const identifier = normalizedIdentityCase2(normalizedWhitespace2(authoredIdentifier));
|
|
89215
|
-
if (framework === "CCSS.Math") {
|
|
89216
|
-
return identifier.replace(/^CCSS\.MATH\.CONTENT\./, "").replace(/^CCSS\.MATH\.PRACTICE\./, "").replace(/^CCSS\.MATH\./, "");
|
|
89217
|
-
}
|
|
89218
|
-
if (framework === "CCSS.ELA-Literacy") {
|
|
89219
|
-
return identifier.replace(/^CCSS\.ELA-LITERACY\./, "");
|
|
89220
|
-
}
|
|
89221
|
-
return identifier;
|
|
89222
|
-
}
|
|
89223
|
-
function readableAlignmentIdentifier2(alignment) {
|
|
89224
|
-
const identifier = alignment.identifier?.trim() ?? "";
|
|
89225
|
-
if (identifier && !isUuidShaped2(identifier)) {
|
|
89226
|
-
return identifier;
|
|
89227
|
-
}
|
|
89228
|
-
const id = alignment.id.trim();
|
|
89229
|
-
return id && !isUuidShaped2(id) ? id : "";
|
|
89230
|
-
}
|
|
89231
|
-
function canonicalAssessmentStandardRef2(standard) {
|
|
89232
|
-
const authoredFramework = normalizedWhitespace2(standard.framework);
|
|
89233
|
-
const framework = canonicalFramework2(authoredFramework, standard.identifier);
|
|
89234
|
-
const identifier = canonicalIdentifier2(framework, standard.identifier);
|
|
89235
|
-
return {
|
|
89236
|
-
framework,
|
|
89237
|
-
identifier
|
|
89238
|
-
};
|
|
89239
|
-
}
|
|
89240
|
-
function assessmentStandardRefKey2(standard) {
|
|
89241
|
-
const canonical = canonicalAssessmentStandardRef2(standard);
|
|
89242
|
-
return JSON.stringify([
|
|
89243
|
-
normalizedIdentityCase2(canonical.framework),
|
|
89244
|
-
normalizedIdentityCase2(canonical.identifier)
|
|
89245
|
-
]);
|
|
89246
|
-
}
|
|
89247
89321
|
function dedupeStandards2(standards) {
|
|
89248
89322
|
const deduped = new Map;
|
|
89249
89323
|
for (const standard of standards) {
|
|
@@ -89334,23 +89408,7 @@ function qtiStandardsFromMetadata(metadata2) {
|
|
|
89334
89408
|
standards.push(...qtiAlignmentStandardsFromMetadata2(metadata2));
|
|
89335
89409
|
return dedupeStandards2(standards);
|
|
89336
89410
|
}
|
|
89337
|
-
|
|
89338
|
-
const refs = new Map;
|
|
89339
|
-
for (const alignment of qtiAlignmentStandardsFromMetadata2(metadata2)) {
|
|
89340
|
-
const identifier = readableAlignmentIdentifier2(alignment);
|
|
89341
|
-
if (identifier) {
|
|
89342
|
-
const canonical = canonicalAssessmentStandardRef2({
|
|
89343
|
-
framework: alignment.source,
|
|
89344
|
-
identifier
|
|
89345
|
-
});
|
|
89346
|
-
if (canonical.framework && canonical.identifier) {
|
|
89347
|
-
refs.set(assessmentStandardRefKey2(canonical), canonical);
|
|
89348
|
-
}
|
|
89349
|
-
}
|
|
89350
|
-
}
|
|
89351
|
-
return [...refs.values()];
|
|
89352
|
-
}
|
|
89353
|
-
var EVENT_HANDLER_ATTRIBUTE, SCRIPT_SCHEMES2, DATA_RASTER_IMAGE_PATTERN2, QTI_MATHML_ALLOWED_TAGS2, metadataSymbol2, parser2, SUPPORTED_QTI_INTERACTION_TYPES2, URL_ATTRIBUTES3, IMAGE_URL_ATTRIBUTES3, INTERACTION_TYPES2, XML_NAMED_ENTITIES, EMPHASIS_TAGS2, STRONG_TAGS2, CONTENT_SKIPPED_TAGS2, BLOCK_CONTENT_KINDS2, GAP_MATCH_TOKEN_TAGS2, STAGE_GRAPHIC_EXCLUDED_CONTAINERS2, BLANK_SENTINEL2 = "", MARKED_BLANK_PATTERN2, MARKED_BLANK_GLOBAL_PATTERN2, REGION_SHAPE_COORDS, QTI_NAMESPACE = "http://www.imsglobal.org/xsd/imsqtiasi_v3p0", QTI_AUTHORING_BLANK_MARKER = "___", MATCH_CORRECT_TEMPLATE2 = "http://www.imsglobal.org/question/qti_v3p0/rptemplates/match_correct", MAP_RESPONSE_TEMPLATE2 = "https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/map_response", MAP_RESPONSE_TEMPLATES2, RESPONSE_PROCESSING_TEMPLATES2, GLOBAL_BODY_ATTRIBUTES, BODY_ELEMENT_ATTRIBUTES, MATHML_ATTRIBUTES, VOID_BODY_ELEMENTS2, URL_ATTRIBUTES22, IMAGE_URL_ATTRIBUTES22, INTERACTION_ATTRIBUTES, CHOICE_ATTRIBUTES, CHOICE_ELEMENT_ATTRIBUTES, VOID_CHOICE_ELEMENTS2, NUMERIC_COMPARISON_ATTRIBUTES2, RESPONSE_CARDINALITIES2, RESPONSE_BASE_TYPES2, BLANK_SENTINEL22 = "", PLAYABLE_POINT_VALUE, POINT_INTERACTION_TYPES2, INDEPENDENT_PROCESSING_TAGS2, UUID_PATTERN2, COMMON_CORE_MATH_FRAMEWORK_ALIASES2, COMMON_CORE_ELA_FRAMEWORK_ALIASES2, COMMON_CORE_FRAMEWORK_ALIASES2;
|
|
89411
|
+
var EVENT_HANDLER_ATTRIBUTE, SCRIPT_SCHEMES2, DATA_RASTER_IMAGE_PATTERN2, QTI_MATHML_ALLOWED_TAGS2, metadataSymbol2, parser2, SUPPORTED_QTI_INTERACTION_TYPES2, URL_ATTRIBUTES3, IMAGE_URL_ATTRIBUTES3, INTERACTION_TYPES2, XML_NAMED_ENTITIES, EMPHASIS_TAGS2, STRONG_TAGS2, CONTENT_SKIPPED_TAGS2, BLOCK_CONTENT_KINDS2, GAP_MATCH_TOKEN_TAGS2, STAGE_GRAPHIC_EXCLUDED_CONTAINERS2, BLANK_SENTINEL2 = "", MARKED_BLANK_PATTERN2, MARKED_BLANK_GLOBAL_PATTERN2, REGION_SHAPE_COORDS, QTI_NAMESPACE = "http://www.imsglobal.org/xsd/imsqtiasi_v3p0", QTI_AUTHORING_BLANK_MARKER = "___", MATCH_CORRECT_TEMPLATE2 = "http://www.imsglobal.org/question/qti_v3p0/rptemplates/match_correct", MAP_RESPONSE_TEMPLATE2 = "https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/map_response", MAP_RESPONSE_TEMPLATES2, RESPONSE_PROCESSING_TEMPLATES2, GLOBAL_BODY_ATTRIBUTES, BODY_ELEMENT_ATTRIBUTES, MATHML_ATTRIBUTES, VOID_BODY_ELEMENTS2, URL_ATTRIBUTES22, IMAGE_URL_ATTRIBUTES22, INTERACTION_ATTRIBUTES, CHOICE_ATTRIBUTES, CHOICE_ELEMENT_ATTRIBUTES, VOID_CHOICE_ELEMENTS2, NUMERIC_COMPARISON_ATTRIBUTES2, RESPONSE_CARDINALITIES2, RESPONSE_BASE_TYPES2, BLANK_SENTINEL22 = "", PLAYABLE_POINT_VALUE, POINT_INTERACTION_TYPES2, INDEPENDENT_PROCESSING_TAGS2, COMMON_CORE_MATH_FRAMEWORK_ALIASES2, COMMON_CORE_ELA_FRAMEWORK_ALIASES2, COMMON_CORE_FRAMEWORK_ALIASES2;
|
|
89354
89412
|
var init_qti = __esm(() => {
|
|
89355
89413
|
init_timeback3();
|
|
89356
89414
|
init_timeback3();
|
|
@@ -89600,7 +89658,6 @@ var init_qti = __esm(() => {
|
|
|
89600
89658
|
"qti-sum",
|
|
89601
89659
|
"qti-base-value"
|
|
89602
89660
|
]);
|
|
89603
|
-
UUID_PATTERN2 = /^(?:urn:uuid:)?\{?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}?$/i;
|
|
89604
89661
|
COMMON_CORE_MATH_FRAMEWORK_ALIASES2 = new Set([
|
|
89605
89662
|
"ccssm",
|
|
89606
89663
|
"ccssmath",
|
|
@@ -92006,8 +92063,8 @@ function assertAssessmentHasQuestions(questions) {
|
|
|
92006
92063
|
}
|
|
92007
92064
|
}
|
|
92008
92065
|
function assertReviewAssessmentHasStandards(standardCounts) {
|
|
92009
|
-
if (standardCounts.some((count) => count
|
|
92010
|
-
throw new ValidationError("Every question in a review assessment must have
|
|
92066
|
+
if (standardCounts.some((count) => count !== 1)) {
|
|
92067
|
+
throw new ValidationError("Every question in a review assessment must have exactly one standards alignment");
|
|
92011
92068
|
}
|
|
92012
92069
|
}
|
|
92013
92070
|
function planAssessmentRemoval(status) {
|
|
@@ -92072,6 +92129,41 @@ var init_timeback_assessment_rules_util = __esm(() => {
|
|
|
92072
92129
|
});
|
|
92073
92130
|
|
|
92074
92131
|
// ../api-core/src/utils/timeback-assessment-runtime.util.ts
|
|
92132
|
+
function stageAssessmentAttemptSupersession(attempt) {
|
|
92133
|
+
Object.assign(attempt.result, ASSESSMENT_ATTEMPT_SUPERSEDED);
|
|
92134
|
+
Object.assign(attempt.selection, ASSESSMENT_ATTEMPT_SUPERSEDED);
|
|
92135
|
+
}
|
|
92136
|
+
function shouldConfirmAssessmentVersionUnavailable(details) {
|
|
92137
|
+
return typeof details === "object" && details !== null && "expectedRevision" in details && typeof details.expectedRevision === "string" && !("currentRevision" in details);
|
|
92138
|
+
}
|
|
92139
|
+
function buildAssessmentAttemptSupersessionUpdate(result) {
|
|
92140
|
+
if (result.status !== "active" || !isAssessmentAttemptOpen({
|
|
92141
|
+
inProgress: result.inProgress ?? "",
|
|
92142
|
+
scoreStatus: result.scoreStatus
|
|
92143
|
+
})) {
|
|
92144
|
+
return null;
|
|
92145
|
+
}
|
|
92146
|
+
return {
|
|
92147
|
+
status: "active",
|
|
92148
|
+
assessmentLineItem: result.assessmentLineItem,
|
|
92149
|
+
student: result.student,
|
|
92150
|
+
score: result.score ?? 0,
|
|
92151
|
+
scoreDate: result.scoreDate,
|
|
92152
|
+
...ASSESSMENT_ATTEMPT_SUPERSEDED,
|
|
92153
|
+
metadata: result.metadata ?? {}
|
|
92154
|
+
};
|
|
92155
|
+
}
|
|
92156
|
+
async function resolveAvailableAssessmentAttempt(initialDecision, select3, resume) {
|
|
92157
|
+
let decision = initialDecision;
|
|
92158
|
+
while (decision?.kind === "resume") {
|
|
92159
|
+
const snapshot = await resume(decision);
|
|
92160
|
+
if (snapshot !== null) {
|
|
92161
|
+
return { kind: "resumed", snapshot };
|
|
92162
|
+
}
|
|
92163
|
+
decision = select3();
|
|
92164
|
+
}
|
|
92165
|
+
return { kind: "continue", decision };
|
|
92166
|
+
}
|
|
92075
92167
|
function buildReviewItemResultMetadata(input) {
|
|
92076
92168
|
const administration = itemAdministration(input.metadata.itemSubmissions, input.selection.itemIdentifier);
|
|
92077
92169
|
if (!administration) {
|
|
@@ -92516,8 +92608,8 @@ function parsePlayableItem(question) {
|
|
|
92516
92608
|
function assessmentContentRevision(test, questions) {
|
|
92517
92609
|
return sha256Hex(canonicalJson2({
|
|
92518
92610
|
identifier: test.identifier,
|
|
92611
|
+
title: test.title,
|
|
92519
92612
|
qtiVersion: test.qtiVersion,
|
|
92520
|
-
rawXml: test.rawXml,
|
|
92521
92613
|
parts: test["qti-test-part"],
|
|
92522
92614
|
questions: questions.questions.map(({ reference, question }) => ({
|
|
92523
92615
|
identifier: reference.identifier,
|
|
@@ -92675,11 +92767,30 @@ async function hydrateQtiTestQuestions(client2, references) {
|
|
|
92675
92767
|
}));
|
|
92676
92768
|
return { ...references, questions };
|
|
92677
92769
|
}
|
|
92678
|
-
async function
|
|
92770
|
+
async function hydrateQtiTestQuestionSelection(client2, references, itemIdentifiers) {
|
|
92771
|
+
const referenceByIdentifier = new Map(references.questions.map((reference) => [reference.reference.identifier, reference]));
|
|
92772
|
+
const selectedReferences = itemIdentifiers.map((itemIdentifier) => {
|
|
92773
|
+
const reference = referenceByIdentifier.get(itemIdentifier);
|
|
92774
|
+
if (!reference) {
|
|
92775
|
+
throw new Error(`QTI assessment ${references.assessmentTest} does not contain item ${itemIdentifier}`);
|
|
92776
|
+
}
|
|
92777
|
+
return reference;
|
|
92778
|
+
});
|
|
92779
|
+
return hydrateQtiTestQuestions(client2, {
|
|
92780
|
+
...references,
|
|
92781
|
+
totalQuestions: selectedReferences.length,
|
|
92782
|
+
questions: selectedReferences
|
|
92783
|
+
});
|
|
92784
|
+
}
|
|
92785
|
+
async function loadQtiTestReferences(client2, identifier) {
|
|
92679
92786
|
const [test, references] = await Promise.all([
|
|
92680
92787
|
client2.qtiApi.assessmentTests.get(identifier),
|
|
92681
92788
|
client2.qtiApi.assessmentTests.getQuestions(identifier)
|
|
92682
92789
|
]);
|
|
92790
|
+
return { test, references };
|
|
92791
|
+
}
|
|
92792
|
+
async function loadHydratedQtiTest(client2, identifier) {
|
|
92793
|
+
const { test, references } = await loadQtiTestReferences(client2, identifier);
|
|
92683
92794
|
return {
|
|
92684
92795
|
test,
|
|
92685
92796
|
questions: await hydrateQtiTestQuestions(client2, references)
|
|
@@ -92694,6 +92805,7 @@ class TimebackAssessmentRuntimeService {
|
|
|
92694
92805
|
static ASSESSMENT_CACHE_TTL_MS = 60000;
|
|
92695
92806
|
static EXPORT_CONCURRENCY = 4;
|
|
92696
92807
|
static ITEM_SUBMISSION_MAX_PREPARATION_ATTEMPTS = 3;
|
|
92808
|
+
static ASSESSMENT_VERSION_CONFIRMATION_DELAY_MS = 25;
|
|
92697
92809
|
static PROJECTION_BARRIER_MAX_ATTEMPTS = 5;
|
|
92698
92810
|
static PROJECTION_BARRIER_RETRY_DELAY_MS = 25;
|
|
92699
92811
|
static SCORING_CONCURRENCY = 4;
|
|
@@ -92706,6 +92818,10 @@ class TimebackAssessmentRuntimeService {
|
|
|
92706
92818
|
defaultTTL: TimebackAssessmentRuntimeService.ASSESSMENT_CACHE_TTL_MS,
|
|
92707
92819
|
maxSize: TimebackAssessmentRuntimeService.ASSESSMENT_CACHE_LIMIT
|
|
92708
92820
|
});
|
|
92821
|
+
reviewSelectionCache = new TimebackCache({
|
|
92822
|
+
defaultTTL: TimebackAssessmentRuntimeService.ASSESSMENT_CACHE_TTL_MS,
|
|
92823
|
+
maxSize: TimebackAssessmentRuntimeService.ASSESSMENT_CACHE_LIMIT
|
|
92824
|
+
});
|
|
92709
92825
|
constructor(deps) {
|
|
92710
92826
|
this.deps = deps;
|
|
92711
92827
|
}
|
|
@@ -92724,6 +92840,7 @@ class TimebackAssessmentRuntimeService {
|
|
|
92724
92840
|
}
|
|
92725
92841
|
const masteryStandard = input.purpose === "mastery" ? normalizeMasteryStandard(input.standard) : undefined;
|
|
92726
92842
|
const requestFingerprint = this.startRequestFingerprint(input);
|
|
92843
|
+
const pendingSupersessions = new Set;
|
|
92727
92844
|
const candidates = await this.candidateIntegrations(gameId, studentId, input);
|
|
92728
92845
|
const lockKeys = candidates.rows.map((row) => assessmentRuntimeStartLockKey({
|
|
92729
92846
|
studentId,
|
|
@@ -92746,20 +92863,21 @@ class TimebackAssessmentRuntimeService {
|
|
|
92746
92863
|
this.listAttempts(attemptScope)
|
|
92747
92864
|
]);
|
|
92748
92865
|
const attempts = listing.attempts;
|
|
92749
|
-
const
|
|
92750
|
-
|
|
92751
|
-
|
|
92752
|
-
return
|
|
92866
|
+
const initialResolution = await this.resumeAvailableAttempt(tests, attempts, requestFingerprint, pendingSupersessions, (resumeDecision) => this.resumeSnapshot(resumeDecision, attempts, context2.integration));
|
|
92867
|
+
if (initialResolution.kind === "resumed") {
|
|
92868
|
+
await this.persistPendingSupersessionsBestEffort(pendingSupersessions);
|
|
92869
|
+
return initialResolution.snapshot;
|
|
92753
92870
|
}
|
|
92871
|
+
let decision = this.requireDecision(initialResolution.decision, input.purpose);
|
|
92754
92872
|
let assessment = await this.loadAssessment(decision.test.qtiTestIdentifier);
|
|
92755
92873
|
const refreshedListing = await this.listAttempts(attemptScope);
|
|
92756
92874
|
const refreshed = refreshedListing.attempts;
|
|
92757
|
-
const
|
|
92758
|
-
|
|
92759
|
-
|
|
92760
|
-
return
|
|
92875
|
+
const refreshedResolution = await this.resumeAvailableAttempt(tests, refreshed, requestFingerprint, pendingSupersessions, (resumeDecision) => this.resumeSnapshot(resumeDecision, refreshed, context2.integration));
|
|
92876
|
+
if (refreshedResolution.kind === "resumed") {
|
|
92877
|
+
await this.persistPendingSupersessionsBestEffort(pendingSupersessions);
|
|
92878
|
+
return refreshedResolution.snapshot;
|
|
92761
92879
|
}
|
|
92762
|
-
decision =
|
|
92880
|
+
decision = this.requireDecision(refreshedResolution.decision, input.purpose);
|
|
92763
92881
|
if (assessment.identifier !== decision.test.qtiTestIdentifier) {
|
|
92764
92882
|
assessment = await this.loadAssessment(decision.test.qtiTestIdentifier);
|
|
92765
92883
|
}
|
|
@@ -92817,6 +92935,7 @@ class TimebackAssessmentRuntimeService {
|
|
|
92817
92935
|
grade: context2.integration.grade
|
|
92818
92936
|
})
|
|
92819
92937
|
});
|
|
92938
|
+
await this.persistPendingSupersessionsBestEffort(pendingSupersessions);
|
|
92820
92939
|
return this.snapshot(result, metadata2, assessment, context2.integration);
|
|
92821
92940
|
});
|
|
92822
92941
|
}
|
|
@@ -92826,6 +92945,7 @@ class TimebackAssessmentRuntimeService {
|
|
|
92826
92945
|
input
|
|
92827
92946
|
}) {
|
|
92828
92947
|
const requestFingerprint = this.startRequestFingerprint(input);
|
|
92948
|
+
const pendingSupersessions = new Set;
|
|
92829
92949
|
const candidates = await this.candidateIntegrations(gameId, studentId, input);
|
|
92830
92950
|
const lockKeys = candidates.rows.map((row) => assessmentRuntimeStartLockKey({
|
|
92831
92951
|
studentId,
|
|
@@ -92847,22 +92967,23 @@ class TimebackAssessmentRuntimeService {
|
|
|
92847
92967
|
this.liveTests(context2.integration.id, input.purpose, db2, undefined, input.diagnosticKey),
|
|
92848
92968
|
this.listAttempts(attemptScope)
|
|
92849
92969
|
]);
|
|
92850
|
-
const
|
|
92851
|
-
|
|
92852
|
-
|
|
92853
|
-
return
|
|
92970
|
+
const initialResolution = await this.resumeAvailableAttempt(tests, listing.attempts, requestFingerprint, pendingSupersessions, (resumeDecision) => this.resumeDiagnosticSnapshot(resumeDecision, listing.attempts, context2.integration, db2));
|
|
92971
|
+
if (initialResolution.kind === "resumed") {
|
|
92972
|
+
await this.persistPendingSupersessionsBestEffort(pendingSupersessions);
|
|
92973
|
+
return initialResolution.snapshot;
|
|
92854
92974
|
}
|
|
92975
|
+
let decision = this.requireDecision(initialResolution.decision, input.purpose);
|
|
92855
92976
|
let definition = await this.requireDiagnosticDefinition(decision.test.id, context2.integration.id, db2, true);
|
|
92856
92977
|
let assessment = await this.loadAssessment(definition.qtiTestIdentifier);
|
|
92857
92978
|
let routing = await this.initializeHostedDiagnostic(definition, assessment);
|
|
92858
92979
|
const refreshedListing = await this.listAttempts(attemptScope);
|
|
92859
92980
|
const refreshed = refreshedListing.attempts;
|
|
92860
|
-
const
|
|
92861
|
-
|
|
92862
|
-
|
|
92863
|
-
return
|
|
92981
|
+
const refreshedResolution = await this.resumeAvailableAttempt(tests, refreshed, requestFingerprint, pendingSupersessions, (resumeDecision) => this.resumeDiagnosticSnapshot(resumeDecision, refreshed, context2.integration, db2));
|
|
92982
|
+
if (refreshedResolution.kind === "resumed") {
|
|
92983
|
+
await this.persistPendingSupersessionsBestEffort(pendingSupersessions);
|
|
92984
|
+
return refreshedResolution.snapshot;
|
|
92864
92985
|
}
|
|
92865
|
-
decision =
|
|
92986
|
+
decision = this.requireDecision(refreshedResolution.decision, input.purpose);
|
|
92866
92987
|
if (definition.id !== decision.test.id) {
|
|
92867
92988
|
definition = await this.requireDiagnosticDefinition(decision.test.id, context2.integration.id, db2, true);
|
|
92868
92989
|
assessment = await this.loadAssessment(definition.qtiTestIdentifier);
|
|
@@ -92931,6 +93052,7 @@ class TimebackAssessmentRuntimeService {
|
|
|
92931
93052
|
grade: context2.integration.grade
|
|
92932
93053
|
})
|
|
92933
93054
|
});
|
|
93055
|
+
await this.persistPendingSupersessionsBestEffort(pendingSupersessions);
|
|
92934
93056
|
return this.diagnosticSnapshot(result, metadata2, assessment, context2.integration, routing.state);
|
|
92935
93057
|
});
|
|
92936
93058
|
}
|
|
@@ -92939,7 +93061,9 @@ class TimebackAssessmentRuntimeService {
|
|
|
92939
93061
|
studentId,
|
|
92940
93062
|
input
|
|
92941
93063
|
}) {
|
|
93064
|
+
const preparationStartedAt = Date.now();
|
|
92942
93065
|
const requestFingerprint = reviewRequestFingerprint(input, DEFAULT_REVIEW_SELECTION_POLICY);
|
|
93066
|
+
const pendingSupersessions = new Set;
|
|
92943
93067
|
const normalizedRequest = normalizeReviewRequest(input);
|
|
92944
93068
|
const candidates = await this.candidateIntegrations(gameId, studentId, input);
|
|
92945
93069
|
const lockKeys = candidates.rows.map((row) => assessmentRuntimeStartLockKey({
|
|
@@ -92963,38 +93087,44 @@ class TimebackAssessmentRuntimeService {
|
|
|
92963
93087
|
this.listAttempts(attemptScope)
|
|
92964
93088
|
]);
|
|
92965
93089
|
const attempts = listing.attempts;
|
|
92966
|
-
const
|
|
92967
|
-
|
|
92968
|
-
|
|
92969
|
-
return
|
|
93090
|
+
const initialResolution = await this.resumeAvailableAttempt(tests, attempts, requestFingerprint, pendingSupersessions, (resumeDecision) => this.resumeReviewSnapshot(resumeDecision, attempts, listing.reviewChildren, context2));
|
|
93091
|
+
if (initialResolution.kind === "resumed") {
|
|
93092
|
+
await this.persistPendingSupersessionsBestEffort(pendingSupersessions);
|
|
93093
|
+
return initialResolution.snapshot;
|
|
92970
93094
|
}
|
|
93095
|
+
let decision = initialResolution.decision;
|
|
92971
93096
|
if (tests.length === 0 || !decision) {
|
|
92972
93097
|
throw AssessmentRuntimeError.from(assessmentNoEligibleTests(input.purpose));
|
|
92973
93098
|
}
|
|
92974
93099
|
if (tests.length > 1) {
|
|
92975
93100
|
throw new ValidationError("Standards review requires exactly one live review-bank assessment.");
|
|
92976
93101
|
}
|
|
92977
|
-
|
|
93102
|
+
const manifestStartedAt = Date.now();
|
|
93103
|
+
let catalog = await this.loadReviewBankCatalog(decision.test.qtiTestIdentifier);
|
|
93104
|
+
this.recordReviewPreparationPhase("manifest_load", manifestStartedAt, {
|
|
93105
|
+
candidateCount: catalog.bank.items.length
|
|
93106
|
+
});
|
|
92978
93107
|
const refreshedListing = await this.listAttempts(attemptScope);
|
|
92979
93108
|
const refreshed = refreshedListing.attempts;
|
|
92980
|
-
const
|
|
92981
|
-
|
|
92982
|
-
|
|
92983
|
-
return
|
|
93109
|
+
const refreshedResolution = await this.resumeAvailableAttempt(tests, refreshed, requestFingerprint, pendingSupersessions, (resumeDecision) => this.resumeReviewSnapshot(resumeDecision, refreshed, refreshedListing.reviewChildren, context2));
|
|
93110
|
+
if (refreshedResolution.kind === "resumed") {
|
|
93111
|
+
await this.persistPendingSupersessionsBestEffort(pendingSupersessions);
|
|
93112
|
+
return refreshedResolution.snapshot;
|
|
92984
93113
|
}
|
|
93114
|
+
const refreshedDecision = refreshedResolution.decision;
|
|
92985
93115
|
if (!refreshedDecision || refreshedDecision.kind !== "start") {
|
|
92986
93116
|
throw AssessmentRuntimeError.from(assessmentNoEligibleTests(input.purpose));
|
|
92987
93117
|
}
|
|
92988
93118
|
decision = refreshedDecision;
|
|
92989
|
-
if (
|
|
92990
|
-
|
|
93119
|
+
if (catalog.test.identifier !== decision.test.qtiTestIdentifier) {
|
|
93120
|
+
catalog = await this.loadReviewBankCatalog(decision.test.qtiTestIdentifier);
|
|
92991
93121
|
}
|
|
92992
93122
|
const exposures = this.reviewExposures(refreshedListing.reviewChildren.values(), {
|
|
92993
|
-
bankRevision:
|
|
93123
|
+
bankRevision: catalog.bank.bankRevision
|
|
92994
93124
|
});
|
|
92995
93125
|
const selected = selectReviewItems({
|
|
92996
93126
|
request: input,
|
|
92997
|
-
bank:
|
|
93127
|
+
bank: catalog.bank,
|
|
92998
93128
|
exposures
|
|
92999
93129
|
});
|
|
93000
93130
|
if (selected.selections.length === 0) {
|
|
@@ -93003,7 +93133,13 @@ class TimebackAssessmentRuntimeService {
|
|
|
93003
93133
|
}
|
|
93004
93134
|
throw AssessmentRuntimeError.from(assessmentReviewBankShortage(selected.fulfillment));
|
|
93005
93135
|
}
|
|
93136
|
+
const hydrationStartedAt = Date.now();
|
|
93137
|
+
const source = await this.hydrateReviewBankSelection(catalog, selected.selections.map((selection) => selection.itemIdentifier));
|
|
93138
|
+
this.recordReviewPreparationPhase("selected_hydration", hydrationStartedAt, {
|
|
93139
|
+
selectedCount: selected.selections.length
|
|
93140
|
+
});
|
|
93006
93141
|
const assessment = projectReviewAssessment(source.assessment, source.bank, selected.selections);
|
|
93142
|
+
const provisioningStartedAt = Date.now();
|
|
93007
93143
|
const parentLineItemId = await this.ensureReviewBankLineItem({
|
|
93008
93144
|
integration: context2.integration,
|
|
93009
93145
|
activityId: input.activityId,
|
|
@@ -93066,6 +93202,12 @@ class TimebackAssessmentRuntimeService {
|
|
|
93066
93202
|
existingChildResults: refreshedListing.reviewChildren,
|
|
93067
93203
|
lookupMissingResults: false
|
|
93068
93204
|
});
|
|
93205
|
+
await this.persistPendingSupersessionsBestEffort(pendingSupersessions);
|
|
93206
|
+
this.recordReviewPreparationPhase("provisioning", provisioningStartedAt);
|
|
93207
|
+
this.recordReviewPreparationPhase("total", preparationStartedAt, {
|
|
93208
|
+
candidateCount: catalog.bank.items.length,
|
|
93209
|
+
selectedCount: selected.selections.length
|
|
93210
|
+
});
|
|
93069
93211
|
return this.snapshot(result, metadata2, assessment, context2.integration);
|
|
93070
93212
|
});
|
|
93071
93213
|
}
|
|
@@ -93419,7 +93561,7 @@ class TimebackAssessmentRuntimeService {
|
|
|
93419
93561
|
preparationAttempts += 1;
|
|
93420
93562
|
preview = await this.scoreDiagnosticItemSubmission(params, input);
|
|
93421
93563
|
} else {
|
|
93422
|
-
await this.projectDiagnosticItemResponse(committed.projection);
|
|
93564
|
+
await this.projectDiagnosticItemResponse(committed.projection, preview?.assessment);
|
|
93423
93565
|
return committed.response;
|
|
93424
93566
|
}
|
|
93425
93567
|
}
|
|
@@ -93833,7 +93975,7 @@ class TimebackAssessmentRuntimeService {
|
|
|
93833
93975
|
where: and(eq(gameTimebackAssessmentTests.id, test.id), eq(gameTimebackAssessmentTests.integrationId, integration.id))
|
|
93834
93976
|
}) : undefined;
|
|
93835
93977
|
const diagnostic = definition?.diagnosticKey ? await this.initializeHostedDiagnostic(definition, loaded.assessment) : null;
|
|
93836
|
-
const reviewBank = purpose === "review" ? await buildReviewBankIndex(loaded.assessment, loaded.questions) : null;
|
|
93978
|
+
const reviewBank = purpose === "review" ? await buildReviewBankIndex(loaded.assessment, loaded.questions, { customOnly: true }) : null;
|
|
93837
93979
|
return {
|
|
93838
93980
|
...test,
|
|
93839
93981
|
live: true,
|
|
@@ -93896,7 +94038,13 @@ class TimebackAssessmentRuntimeService {
|
|
|
93896
94038
|
const enrollmentByCourse = new Map(enrollments.map((enrollment) => [enrollment.course.id, enrollment]));
|
|
93897
94039
|
const candidateRows = integrations.filter((integration) => enrollmentByCourse.has(integration.courseId));
|
|
93898
94040
|
const liveTestRows = candidateRows.length === 0 ? [] : await db2.query.gameTimebackAssessmentTests.findMany({
|
|
93899
|
-
where: and(inArray(gameTimebackAssessmentTests.integrationId, candidateRows.map((row) => row.id)), eq(gameTimebackAssessmentTests.purpose, input.purpose), eq(gameTimebackAssessmentTests.status, "live"))
|
|
94041
|
+
where: and(inArray(gameTimebackAssessmentTests.integrationId, candidateRows.map((row) => row.id)), eq(gameTimebackAssessmentTests.purpose, input.purpose), eq(gameTimebackAssessmentTests.status, "live")),
|
|
94042
|
+
columns: {
|
|
94043
|
+
integrationId: true,
|
|
94044
|
+
standardFramework: true,
|
|
94045
|
+
standardIdentifier: true,
|
|
94046
|
+
diagnosticKey: true
|
|
94047
|
+
}
|
|
93900
94048
|
});
|
|
93901
94049
|
const matchesMasteryStandard = input.purpose === "mastery" ? masteryStandardMatcher(input.standard) : undefined;
|
|
93902
94050
|
const integrationIdsWithLiveTests = new Set(liveTestRows.filter((row) => (!matchesMasteryStandard || matchesMasteryStandard(assessmentStandardForRow(row))) && (input.purpose !== "diagnostic" || row.diagnosticKey === input.diagnosticKey)).map((row) => row.integrationId));
|
|
@@ -94089,6 +94237,75 @@ class TimebackAssessmentRuntimeService {
|
|
|
94089
94237
|
});
|
|
94090
94238
|
return this.diagnosticSnapshot(resumed.result, resumed.metadata, assessment, integration, routing.state);
|
|
94091
94239
|
}
|
|
94240
|
+
async resumeAvailableAttempt(tests, attempts, requestFingerprint, pendingSupersessions, resume) {
|
|
94241
|
+
const select3 = () => selectRuntimeAssessment(tests, [...this.attemptsMatchingRequest(attempts, requestFingerprint).values()].map((entry) => entry.selection));
|
|
94242
|
+
return resolveAvailableAssessmentAttempt(select3(), select3, (decision) => this.resumeSnapshotOrStageSupersession(decision, attempts, pendingSupersessions, () => resume(decision)));
|
|
94243
|
+
}
|
|
94244
|
+
async resumeSnapshotOrStageSupersession(decision, attempts, pendingSupersessions, resume) {
|
|
94245
|
+
try {
|
|
94246
|
+
return await resume();
|
|
94247
|
+
} catch (error88) {
|
|
94248
|
+
if (!(error88 instanceof AssessmentRuntimeError) || error88.runtimeCode !== "SELECTED_TEST_VERSION_UNAVAILABLE") {
|
|
94249
|
+
throw error88;
|
|
94250
|
+
}
|
|
94251
|
+
if (shouldConfirmAssessmentVersionUnavailable(error88.details)) {
|
|
94252
|
+
await sleep(TimebackAssessmentRuntimeService.ASSESSMENT_VERSION_CONFIRMATION_DELAY_MS);
|
|
94253
|
+
try {
|
|
94254
|
+
return await resume();
|
|
94255
|
+
} catch (confirmationError) {
|
|
94256
|
+
if (!(confirmationError instanceof AssessmentRuntimeError) || confirmationError.runtimeCode !== "SELECTED_TEST_VERSION_UNAVAILABLE") {
|
|
94257
|
+
throw confirmationError;
|
|
94258
|
+
}
|
|
94259
|
+
}
|
|
94260
|
+
}
|
|
94261
|
+
const staged = attempts.get(decision.attempt.attemptId);
|
|
94262
|
+
pendingSupersessions.add(staged.result.sourcedId);
|
|
94263
|
+
stageAssessmentAttemptSupersession(staged);
|
|
94264
|
+
addEvent("assessment.attempt_supersession_staged", {
|
|
94265
|
+
"app.assessment.attempt_id": decision.attempt.attemptId,
|
|
94266
|
+
"app.assessment.reason": "selected_test_version_unavailable"
|
|
94267
|
+
});
|
|
94268
|
+
return null;
|
|
94269
|
+
}
|
|
94270
|
+
}
|
|
94271
|
+
async persistPendingSupersessions(attemptIds) {
|
|
94272
|
+
for (const attemptId of [...attemptIds].toSorted()) {
|
|
94273
|
+
await this.deps.assessmentRuntimeLock(this.deps.db, assessmentRuntimeAttemptLockKey(attemptId), async () => {
|
|
94274
|
+
let result;
|
|
94275
|
+
try {
|
|
94276
|
+
result = await this.requireClient().api.oneroster.assessmentResults.get(attemptId);
|
|
94277
|
+
} catch (error88) {
|
|
94278
|
+
if (isApiError(error88) && error88.statusCode === 404) {
|
|
94279
|
+
return;
|
|
94280
|
+
}
|
|
94281
|
+
throw error88;
|
|
94282
|
+
}
|
|
94283
|
+
const update2 = buildAssessmentAttemptSupersessionUpdate(result);
|
|
94284
|
+
if (!update2) {
|
|
94285
|
+
return;
|
|
94286
|
+
}
|
|
94287
|
+
await this.requireClient().api.oneroster.assessmentResults.upsert(attemptId, update2);
|
|
94288
|
+
addEvent("assessment.attempt_superseded", {
|
|
94289
|
+
"app.assessment.attempt_id": attemptId,
|
|
94290
|
+
"app.assessment.reason": "selected_test_version_unavailable"
|
|
94291
|
+
});
|
|
94292
|
+
});
|
|
94293
|
+
}
|
|
94294
|
+
}
|
|
94295
|
+
async persistPendingSupersessionsBestEffort(attemptIds) {
|
|
94296
|
+
if (attemptIds.size === 0) {
|
|
94297
|
+
return;
|
|
94298
|
+
}
|
|
94299
|
+
try {
|
|
94300
|
+
await this.persistPendingSupersessions(attemptIds);
|
|
94301
|
+
} catch (error88) {
|
|
94302
|
+
addEvent("assessment.attempt_supersession_persist_failed", {
|
|
94303
|
+
"app.assessment.attempt_ids": [...attemptIds].toSorted().join(","),
|
|
94304
|
+
"exception.type": errorType(error88),
|
|
94305
|
+
"app.error.message": errorMessage(error88)
|
|
94306
|
+
});
|
|
94307
|
+
}
|
|
94308
|
+
}
|
|
94092
94309
|
async resumeReviewSnapshot(decision, attempts, existingChildResults, context2) {
|
|
94093
94310
|
if (decision.additionalResumableAttemptIds.length > 0) {
|
|
94094
94311
|
addEvent("assessment.multiple_resumable_attempts", {
|
|
@@ -94100,7 +94317,8 @@ class TimebackAssessmentRuntimeService {
|
|
|
94100
94317
|
if (resumed.metadata.purpose !== "review") {
|
|
94101
94318
|
throw new Error("A standards-review request selected a non-review attempt");
|
|
94102
94319
|
}
|
|
94103
|
-
const
|
|
94320
|
+
const catalog = await this.loadReviewBankCatalog(resumed.metadata.selectedTest.identifier, resumed.metadata.selectedTest.contentRevision);
|
|
94321
|
+
const currentSource = await this.hydrateReviewBankSelection(catalog, resumed.metadata.review.selections.map((selection) => selection.itemIdentifier));
|
|
94104
94322
|
const source = this.pinnedReviewBankSource(currentSource, resumed.metadata);
|
|
94105
94323
|
const assessment = projectReviewAssessment(source.assessment, source.bank, resumed.metadata.review.selections);
|
|
94106
94324
|
await this.ensureReviewChildResults({
|
|
@@ -94114,20 +94332,68 @@ class TimebackAssessmentRuntimeService {
|
|
|
94114
94332
|
});
|
|
94115
94333
|
return this.snapshot(resumed.result, resumed.metadata, assessment, context2.integration);
|
|
94116
94334
|
}
|
|
94117
|
-
|
|
94335
|
+
recordReviewPreparationPhase(phase, startedAt, counts = {}) {
|
|
94336
|
+
addEvent("assessment.review_preparation_phase", {
|
|
94337
|
+
"app.assessment.review_phase": phase,
|
|
94338
|
+
"app.assessment.duration_ms": Date.now() - startedAt,
|
|
94339
|
+
...counts.candidateCount === undefined ? {} : { "app.assessment.candidate_count": counts.candidateCount },
|
|
94340
|
+
...counts.selectedCount === undefined ? {} : { "app.assessment.selected_count": counts.selectedCount }
|
|
94341
|
+
});
|
|
94342
|
+
}
|
|
94343
|
+
async loadReviewBankCatalog(identifier, expectedSourceRevision) {
|
|
94118
94344
|
if (expectedSourceRevision) {
|
|
94119
94345
|
const cached3 = this.reviewBankCache.get(`${identifier}\x00${expectedSourceRevision}`);
|
|
94120
94346
|
if (cached3) {
|
|
94121
94347
|
return cached3;
|
|
94122
94348
|
}
|
|
94123
94349
|
}
|
|
94124
|
-
|
|
94125
|
-
|
|
94126
|
-
|
|
94127
|
-
|
|
94128
|
-
|
|
94129
|
-
|
|
94130
|
-
|
|
94350
|
+
let loaded;
|
|
94351
|
+
try {
|
|
94352
|
+
loaded = await loadQtiTestReferences(this.requireClient(), identifier);
|
|
94353
|
+
} catch (error88) {
|
|
94354
|
+
if (expectedSourceRevision && isApiError(error88) && error88.statusCode === 404) {
|
|
94355
|
+
throw new AssessmentRuntimeError("SELECTED_TEST_VERSION_UNAVAILABLE", `The selected assessment version for ${identifier} is no longer available.`, { identifier, expectedRevision: expectedSourceRevision });
|
|
94356
|
+
}
|
|
94357
|
+
throw error88;
|
|
94358
|
+
}
|
|
94359
|
+
let bank;
|
|
94360
|
+
try {
|
|
94361
|
+
bank = await reviewBankIndexFromManifest(loaded.test.metadata, {
|
|
94362
|
+
bankIdentifier: identifier,
|
|
94363
|
+
membershipItemIdentifiers: loaded.references.questions.map((question) => question.reference.identifier)
|
|
94364
|
+
});
|
|
94365
|
+
} catch (error88) {
|
|
94366
|
+
throw new ServiceUnavailableError(`Review bank ${identifier} has stale or invalid routing metadata. Use Update Mapping in assessment authoring.`, { reason: errorMessage(error88) });
|
|
94367
|
+
}
|
|
94368
|
+
if (!bank) {
|
|
94369
|
+
throw new ServiceUnavailableError(`Review bank ${identifier} needs its authoring mapping updated before it can serve review questions.`);
|
|
94370
|
+
}
|
|
94371
|
+
if (expectedSourceRevision && bank.sourceContentRevision !== expectedSourceRevision) {
|
|
94372
|
+
throw new AssessmentRuntimeError("SELECTED_TEST_VERSION_UNAVAILABLE", `The selected assessment version for ${identifier} is no longer available.`, {
|
|
94373
|
+
identifier,
|
|
94374
|
+
expectedRevision: expectedSourceRevision,
|
|
94375
|
+
currentRevision: bank.sourceContentRevision
|
|
94376
|
+
});
|
|
94377
|
+
}
|
|
94378
|
+
const catalog = { ...loaded, bank };
|
|
94379
|
+
this.reviewBankCache.set(`${identifier}\x00${bank.sourceContentRevision}`, catalog);
|
|
94380
|
+
return catalog;
|
|
94381
|
+
}
|
|
94382
|
+
async hydrateReviewBankSelection(catalog, itemIdentifiers) {
|
|
94383
|
+
const cacheKey2 = [
|
|
94384
|
+
catalog.test.identifier,
|
|
94385
|
+
catalog.bank.sourceContentRevision,
|
|
94386
|
+
...itemIdentifiers
|
|
94387
|
+
].join("\x00");
|
|
94388
|
+
const cached3 = this.reviewSelectionCache.get(cacheKey2);
|
|
94389
|
+
if (cached3) {
|
|
94390
|
+
return { assessment: cached3, bank: catalog.bank };
|
|
94391
|
+
}
|
|
94392
|
+
const questions = await hydrateQtiTestQuestionSelection(this.requireClient(), catalog.references, itemIdentifiers);
|
|
94393
|
+
const assessment = await buildPlayableAssessment(catalog.test, questions);
|
|
94394
|
+
assessment.contentRevision = catalog.bank.sourceContentRevision;
|
|
94395
|
+
this.reviewSelectionCache.set(cacheKey2, assessment);
|
|
94396
|
+
return { assessment, bank: catalog.bank };
|
|
94131
94397
|
}
|
|
94132
94398
|
pinnedReviewBankSource(source, metadata2) {
|
|
94133
94399
|
return {
|
|
@@ -94158,7 +94424,8 @@ class TimebackAssessmentRuntimeService {
|
|
|
94158
94424
|
if (metadata2.purpose !== "review") {
|
|
94159
94425
|
return this.loadAssessment(metadata2.selectedTest.identifier, metadata2.selectedTest.contentRevision);
|
|
94160
94426
|
}
|
|
94161
|
-
const
|
|
94427
|
+
const catalog = await this.loadReviewBankCatalog(metadata2.selectedTest.identifier, metadata2.selectedTest.contentRevision);
|
|
94428
|
+
const currentSource = await this.hydrateReviewBankSelection(catalog, metadata2.review.selections.map((selection) => selection.itemIdentifier));
|
|
94162
94429
|
const source = this.pinnedReviewBankSource(currentSource, metadata2);
|
|
94163
94430
|
return projectReviewAssessment(source.assessment, source.bank, metadata2.review.selections);
|
|
94164
94431
|
}
|
|
@@ -94781,6 +95048,14 @@ class TimebackAssessmentRuntimeService {
|
|
|
94781
95048
|
return AssessmentRuntimeError.from(assessmentAttemptUnauthorized(attemptId));
|
|
94782
95049
|
}
|
|
94783
95050
|
async snapshotForResult(result, metadata2, integration) {
|
|
95051
|
+
if (isAssessmentAttemptSuperseded({
|
|
95052
|
+
inProgress: result.inProgress ?? "",
|
|
95053
|
+
scoreStatus: result.scoreStatus
|
|
95054
|
+
})) {
|
|
95055
|
+
throw new GoneError("This assessment attempt has been superseded.", {
|
|
95056
|
+
attemptId: result.sourcedId
|
|
95057
|
+
});
|
|
95058
|
+
}
|
|
94784
95059
|
const assessment = await this.loadAttemptAssessment(metadata2);
|
|
94785
95060
|
if (this.isPlatformRoutedDiagnosticMetadata(metadata2)) {
|
|
94786
95061
|
const routing = await this.loadAttemptDiagnosticRouting(metadata2, assessment);
|
|
@@ -96129,6 +96404,62 @@ function buildQtiLibraryListPlan(params) {
|
|
|
96129
96404
|
}
|
|
96130
96405
|
var PLAYCADEMY_QTI_SOURCE_PREFIX = "playcademy-test-", PLAYCADEMY_QTI_SOURCE_UPPER_BOUND = "playcademy-test.";
|
|
96131
96406
|
|
|
96407
|
+
// ../api-core/src/utils/timeback-review-mapping.util.ts
|
|
96408
|
+
function reviewMappingIssueSummary(label, identifiers) {
|
|
96409
|
+
if (identifiers.length === 0) {
|
|
96410
|
+
return null;
|
|
96411
|
+
}
|
|
96412
|
+
const displayed = identifiers.slice(0, 10).join(", ");
|
|
96413
|
+
const remainder = identifiers.length > 10 ? `, and ${identifiers.length - 10} more` : "";
|
|
96414
|
+
return `${label} (${identifiers.length}): ${displayed}${remainder}`;
|
|
96415
|
+
}
|
|
96416
|
+
async function prepareReviewMappingUpdate(test, questions) {
|
|
96417
|
+
const contentRevision = await assessmentContentRevision(test, questions);
|
|
96418
|
+
const assessment = {
|
|
96419
|
+
identifier: test.identifier,
|
|
96420
|
+
contractVersion: 1,
|
|
96421
|
+
contentRevision,
|
|
96422
|
+
title: test.title,
|
|
96423
|
+
items: questions.questions.map(({ question }) => ({
|
|
96424
|
+
identifier: question.identifier,
|
|
96425
|
+
title: question.title,
|
|
96426
|
+
prompt: "",
|
|
96427
|
+
maxScore: 1,
|
|
96428
|
+
interactions: []
|
|
96429
|
+
}))
|
|
96430
|
+
};
|
|
96431
|
+
const bank = await buildReviewBankIndex(assessment, questions, { customOnly: true });
|
|
96432
|
+
const issues = reviewBankMappingIssues(bank);
|
|
96433
|
+
if (issues.missingOrInvalidItemIdentifiers.length > 0 || issues.multipleStandardItemIdentifiers.length > 0) {
|
|
96434
|
+
const details = [
|
|
96435
|
+
reviewMappingIssueSummary("missing or invalid associations", issues.missingOrInvalidItemIdentifiers),
|
|
96436
|
+
reviewMappingIssueSummary("multiple associations", issues.multipleStandardItemIdentifiers)
|
|
96437
|
+
].filter((detail) => detail !== null);
|
|
96438
|
+
throw new ValidationError(`Every review question must have exactly one valid standard/node association. ${details.join("; ")}.`);
|
|
96439
|
+
}
|
|
96440
|
+
const manifest = await buildReviewBankManifest(bank);
|
|
96441
|
+
return {
|
|
96442
|
+
update: {
|
|
96443
|
+
...buildQtiTestUpdateInput(test, test.title),
|
|
96444
|
+
metadata: {
|
|
96445
|
+
...test.metadata,
|
|
96446
|
+
[PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY]: manifest
|
|
96447
|
+
}
|
|
96448
|
+
},
|
|
96449
|
+
summary: {
|
|
96450
|
+
itemCount: bank.items.length,
|
|
96451
|
+
standardCount: Object.keys(manifest.itemsByStandard).length,
|
|
96452
|
+
sourceFingerprint: manifest.sourceFingerprint
|
|
96453
|
+
}
|
|
96454
|
+
};
|
|
96455
|
+
}
|
|
96456
|
+
var init_timeback_review_mapping_util = __esm(() => {
|
|
96457
|
+
init_assessment_runtime2();
|
|
96458
|
+
init_errors2();
|
|
96459
|
+
init_timeback_assessment_runtime_util();
|
|
96460
|
+
init_timeback_qti_authoring_util();
|
|
96461
|
+
});
|
|
96462
|
+
|
|
96132
96463
|
// ../api-core/src/services/timeback-assessments.service.ts
|
|
96133
96464
|
function databaseConstraintName(error88) {
|
|
96134
96465
|
if (typeof error88 !== "object" || error88 === null) {
|
|
@@ -96211,7 +96542,8 @@ class TimebackAssessmentsService {
|
|
|
96211
96542
|
ownerGameSlug: ownership.gameSlug,
|
|
96212
96543
|
integrationId,
|
|
96213
96544
|
subject: integration.subject,
|
|
96214
|
-
grade: String(integration.grade)
|
|
96545
|
+
grade: String(integration.grade),
|
|
96546
|
+
[PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY]: emptyReviewBankManifest(input.qtiTestIdentifier)
|
|
96215
96547
|
}
|
|
96216
96548
|
});
|
|
96217
96549
|
try {
|
|
@@ -96280,7 +96612,9 @@ class TimebackAssessmentsService {
|
|
|
96280
96612
|
assertAssessmentHasQuestions(loaded.questions.questions);
|
|
96281
96613
|
assertPlayableAssessmentImportQuestions(manifest.targetStatus, loaded.questions.questions.map(({ question }) => question));
|
|
96282
96614
|
if (entry.purpose === "review") {
|
|
96283
|
-
assertReviewAssessmentHasStandards(loaded.questions.questions.map(({ question }) =>
|
|
96615
|
+
assertReviewAssessmentHasStandards(loaded.questions.questions.map(({ question }) => reviewRoutingStandardRefsFromMetadata(question.metadata, {
|
|
96616
|
+
customOnly: true
|
|
96617
|
+
}).length));
|
|
96284
96618
|
}
|
|
96285
96619
|
return { entry, test: loaded.test };
|
|
96286
96620
|
} catch (error88) {
|
|
@@ -96432,6 +96766,10 @@ class TimebackAssessmentsService {
|
|
|
96432
96766
|
if (input.status !== undefined) {
|
|
96433
96767
|
validateAssessmentStatusTransition(row.status, input.status);
|
|
96434
96768
|
}
|
|
96769
|
+
if (!publishing && !activatingReview && nextPurpose === "review" && row.purpose !== "review") {
|
|
96770
|
+
const ownership = await this.requireQtiTestOwnershipContext(integrationId, tx);
|
|
96771
|
+
await this.rebuildReviewMapping(this.requireClient(), qtiTestIdentifier, ownership.gameSlug);
|
|
96772
|
+
}
|
|
96435
96773
|
if (input.title !== undefined) {
|
|
96436
96774
|
assertDraftAssessment(row);
|
|
96437
96775
|
assertAllAssessmentAssociationsDraft(associations);
|
|
@@ -96450,7 +96788,8 @@ class TimebackAssessmentsService {
|
|
|
96450
96788
|
updates.diagnosticKey = nextDiagnostic.diagnosticKey;
|
|
96451
96789
|
updates.diagnosticRoutingManifest = nextDiagnostic.routingManifest;
|
|
96452
96790
|
} else {
|
|
96453
|
-
|
|
96791
|
+
const reviewOwnership = nextPurpose === "review" && nextStatus === "live" ? await this.requireQtiTestOwnershipContext(integrationId, tx) : undefined;
|
|
96792
|
+
await this.validateAssessmentHasQuestions(qtiTestIdentifier, reviewOwnership?.gameSlug);
|
|
96454
96793
|
}
|
|
96455
96794
|
}
|
|
96456
96795
|
let updated = row;
|
|
@@ -96545,6 +96884,19 @@ class TimebackAssessmentsService {
|
|
|
96545
96884
|
});
|
|
96546
96885
|
return { ...result, questions };
|
|
96547
96886
|
}
|
|
96887
|
+
async updateReviewMapping(integrationId, qtiTestIdentifier) {
|
|
96888
|
+
return this.withLockedAssessmentAssociations(integrationId, qtiTestIdentifier, async (row, tx, associations) => {
|
|
96889
|
+
if (!associations.some((association) => association.purpose === "review")) {
|
|
96890
|
+
throw new ValidationError("Review mapping is available only for standards-review assessments.");
|
|
96891
|
+
}
|
|
96892
|
+
const client2 = this.requireClient();
|
|
96893
|
+
const ownership = await this.requireQtiTestOwnershipContext(integrationId, tx);
|
|
96894
|
+
const result = await this.rebuildReviewMapping(client2, qtiTestIdentifier, ownership.gameSlug);
|
|
96895
|
+
setAttribute("app.assessment.operation", "update_review_mapping");
|
|
96896
|
+
setAttribute("app.assessment.review_mapping_item_count", result.itemCount);
|
|
96897
|
+
return result;
|
|
96898
|
+
});
|
|
96899
|
+
}
|
|
96548
96900
|
async listQuestionLibrary(integrationId, params) {
|
|
96549
96901
|
const client2 = this.requireClient();
|
|
96550
96902
|
await this.requireIntegration(integrationId);
|
|
@@ -96585,7 +96937,8 @@ class TimebackAssessmentsService {
|
|
|
96585
96937
|
integrationId,
|
|
96586
96938
|
subject: integration.subject,
|
|
96587
96939
|
grade: String(integration.grade),
|
|
96588
|
-
copiedFromTestIdentifier: sourceTestIdentifier
|
|
96940
|
+
copiedFromTestIdentifier: sourceTestIdentifier,
|
|
96941
|
+
[PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY]: emptyReviewBankManifest(targetTestIdentifier)
|
|
96589
96942
|
}, itemPlan);
|
|
96590
96943
|
const attemptedItemIdentifiers = [];
|
|
96591
96944
|
let testCreationAttempted = false;
|
|
@@ -96973,16 +97326,20 @@ class TimebackAssessmentsService {
|
|
|
96973
97326
|
const plan = buildQtiLibraryListPlan(params);
|
|
96974
97327
|
return list(plan.params);
|
|
96975
97328
|
}
|
|
96976
|
-
async validateAssessmentHasQuestions(qtiTestIdentifier,
|
|
97329
|
+
async validateAssessmentHasQuestions(qtiTestIdentifier, reviewGameSlug) {
|
|
96977
97330
|
const client2 = this.requireClient();
|
|
96978
|
-
const result = await
|
|
97331
|
+
const [test, result] = await Promise.all([
|
|
97332
|
+
client2.qtiApi.assessmentTests.get(qtiTestIdentifier),
|
|
97333
|
+
client2.qtiApi.assessmentTests.getQuestions(qtiTestIdentifier)
|
|
97334
|
+
]);
|
|
96979
97335
|
assertAssessmentHasQuestions(result.questions);
|
|
96980
97336
|
const hydrated = await hydrateQtiTestQuestions(client2, result);
|
|
96981
97337
|
for (const { question } of hydrated.questions) {
|
|
96982
97338
|
assertPlayableQtiQuestion(question);
|
|
96983
97339
|
}
|
|
96984
|
-
if (
|
|
96985
|
-
|
|
97340
|
+
if (reviewGameSlug) {
|
|
97341
|
+
assertQtiTestOwnedByGame(test, reviewGameSlug);
|
|
97342
|
+
await this.writeReviewMapping(client2, test, hydrated);
|
|
96986
97343
|
}
|
|
96987
97344
|
}
|
|
96988
97345
|
async prepareDiagnosticAssociationChanges(row, nextPurpose, requested) {
|
|
@@ -97047,6 +97404,19 @@ class TimebackAssessmentsService {
|
|
|
97047
97404
|
routingManifest: validation.manifest
|
|
97048
97405
|
};
|
|
97049
97406
|
}
|
|
97407
|
+
async rebuildReviewMapping(client2, qtiTestIdentifier, gameSlug) {
|
|
97408
|
+
const [test, references] = await Promise.all([
|
|
97409
|
+
client2.qtiApi.assessmentTests.get(qtiTestIdentifier),
|
|
97410
|
+
client2.qtiApi.assessmentTests.getQuestions(qtiTestIdentifier)
|
|
97411
|
+
]);
|
|
97412
|
+
assertQtiTestOwnedByGame(test, gameSlug);
|
|
97413
|
+
return this.writeReviewMapping(client2, test, await hydrateQtiTestQuestions(client2, references));
|
|
97414
|
+
}
|
|
97415
|
+
async writeReviewMapping(client2, test, questions) {
|
|
97416
|
+
const prepared = await prepareReviewMappingUpdate(test, questions);
|
|
97417
|
+
await client2.qtiApi.assessmentTests.update(test.identifier, prepared.update);
|
|
97418
|
+
return prepared.summary;
|
|
97419
|
+
}
|
|
97050
97420
|
}
|
|
97051
97421
|
var init_timeback_assessments_service = __esm(async () => {
|
|
97052
97422
|
init_drizzle_orm();
|
|
@@ -97054,7 +97424,6 @@ var init_timeback_assessments_service = __esm(async () => {
|
|
|
97054
97424
|
init_tables_index();
|
|
97055
97425
|
init_spans();
|
|
97056
97426
|
init_assessment_runtime2();
|
|
97057
|
-
init_qti();
|
|
97058
97427
|
init_timeback3();
|
|
97059
97428
|
init_errors2();
|
|
97060
97429
|
init_timeback_assessment_import_util();
|
|
@@ -97062,6 +97431,7 @@ var init_timeback_assessments_service = __esm(async () => {
|
|
|
97062
97431
|
init_timeback_assessment_runtime_util();
|
|
97063
97432
|
init_timeback_qti_authoring_util();
|
|
97064
97433
|
init_timeback_qti_hydration_util();
|
|
97434
|
+
init_timeback_review_mapping_util();
|
|
97065
97435
|
await init_errors8();
|
|
97066
97436
|
});
|
|
97067
97437
|
|
|
@@ -157039,7 +157409,7 @@ function parseQtiLibraryParams(searchParams) {
|
|
|
157039
157409
|
limit
|
|
157040
157410
|
};
|
|
157041
157411
|
}
|
|
157042
|
-
var populateStudent, getUser, getUserEnrollments, getUserById, setupIntegration, getIntegrations, getRemovedIntegrations, createIntegration, updateIntegration, deactivateCourse, reactivateCourse, getIntegrationConfig, verifyIntegration, getConfig, deleteIntegrations, endActivity, heartbeat, advanceCourse, unenrollCourse, startRuntimeAssessment, getRuntimeAssessment, getLatestRuntimeAssessment, saveRuntimeAssessment, submitRuntimeAssessmentItem, submitRuntimeAssessment, exportRuntimeAssessmentFixture, getStudentXp, getStudentMastery, getStudentHighestGradeMastered, getRoster, getStudentOverview, getGameMetrics, getStudentActivity, getGradeLevelTestResults, getGradeLevelTestReview, getActivityDetail, listMetricDiscrepancies, verifyMetricDiscrepancy, grantXp, adjustTime, adjustMastery, reconcileMasteryForConfigChange, searchStudents, enrollStudent, unenrollStudent, reactivateEnrollment, listAssessments, createAssessment, attachExistingAssessments, updateAssessment, reorderAssessments, reorderQuestions, removeAssessment, listQuestions, listQuestionLibrary, listTestLibrary, copyAssessment, createQuestion, updateQuestion, removeQuestion, timeback2;
|
|
157412
|
+
var populateStudent, getUser, getUserEnrollments, getUserById, setupIntegration, getIntegrations, getRemovedIntegrations, createIntegration, updateIntegration, deactivateCourse, reactivateCourse, getIntegrationConfig, verifyIntegration, getConfig, deleteIntegrations, endActivity, heartbeat, advanceCourse, unenrollCourse, startRuntimeAssessment, getRuntimeAssessment, getLatestRuntimeAssessment, saveRuntimeAssessment, submitRuntimeAssessmentItem, submitRuntimeAssessment, exportRuntimeAssessmentFixture, getStudentXp, getStudentMastery, getStudentHighestGradeMastered, getRoster, getStudentOverview, getGameMetrics, getStudentActivity, getGradeLevelTestResults, getGradeLevelTestReview, getActivityDetail, listMetricDiscrepancies, verifyMetricDiscrepancy, grantXp, adjustTime, adjustMastery, reconcileMasteryForConfigChange, searchStudents, enrollStudent, unenrollStudent, reactivateEnrollment, listAssessments, createAssessment, attachExistingAssessments, updateAssessment, updateReviewMapping, reorderAssessments, reorderQuestions, removeAssessment, listQuestions, listQuestionLibrary, listTestLibrary, copyAssessment, createQuestion, updateQuestion, removeQuestion, timeback2;
|
|
157043
157413
|
var init_timeback_controller = __esm(() => {
|
|
157044
157414
|
init_esm();
|
|
157045
157415
|
init_src();
|
|
@@ -157713,6 +158083,14 @@ var init_timeback_controller = __esm(() => {
|
|
|
157713
158083
|
const body2 = await parseRequestBody(ctx.request, UpdateAssessmentRequestSchema);
|
|
157714
158084
|
return ctx.services.timebackAssessments.updateAssessment(integrationId, testIdentifier, body2);
|
|
157715
158085
|
});
|
|
158086
|
+
updateReviewMapping = requireDeveloper(async (ctx) => {
|
|
158087
|
+
const { gameId, courseId, testIdentifier } = ctx.params;
|
|
158088
|
+
if (!gameId || !courseId || !testIdentifier) {
|
|
158089
|
+
throw ApiError.badRequest("Missing gameId, courseId, or testIdentifier parameter");
|
|
158090
|
+
}
|
|
158091
|
+
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
158092
|
+
return ctx.services.timebackAssessments.updateReviewMapping(integrationId, testIdentifier);
|
|
158093
|
+
});
|
|
157716
158094
|
reorderAssessments = requireDeveloper(async (ctx) => {
|
|
157717
158095
|
const { gameId, courseId } = ctx.params;
|
|
157718
158096
|
if (!gameId || !courseId) {
|
|
@@ -157855,6 +158233,7 @@ var init_timeback_controller = __esm(() => {
|
|
|
157855
158233
|
createAssessment,
|
|
157856
158234
|
attachExistingAssessments,
|
|
157857
158235
|
updateAssessment,
|
|
158236
|
+
updateReviewMapping,
|
|
157858
158237
|
reorderAssessments,
|
|
157859
158238
|
removeAssessment,
|
|
157860
158239
|
reorderQuestions,
|