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