@playcademy/sandbox 0.8.1-beta.2 → 0.8.1-beta.4
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 +3001 -980
- package/dist/server.js +3001 -980
- package/package.json +1 -1
package/dist/cli.js
CHANGED
|
@@ -1128,7 +1128,7 @@ var package_default;
|
|
|
1128
1128
|
var init_package = __esm(() => {
|
|
1129
1129
|
package_default = {
|
|
1130
1130
|
name: "@playcademy/sandbox",
|
|
1131
|
-
version: "0.8.1-beta.
|
|
1131
|
+
version: "0.8.1-beta.4",
|
|
1132
1132
|
description: "Local development server for Playcademy game development",
|
|
1133
1133
|
type: "module",
|
|
1134
1134
|
exports: {
|
|
@@ -10767,39 +10767,21 @@ function compareNewestCompletedAssessmentAttempt(left, right) {
|
|
|
10767
10767
|
return newestAttempt(left, right, "scoreDate");
|
|
10768
10768
|
}
|
|
10769
10769
|
function compareAssessmentCatalogOrder(left, right) {
|
|
10770
|
-
if (left.sortOrder !== null || right.sortOrder !== null) {
|
|
10771
|
-
if (left.sortOrder === null) {
|
|
10772
|
-
return 1;
|
|
10773
|
-
}
|
|
10774
|
-
if (right.sortOrder === null) {
|
|
10775
|
-
return -1;
|
|
10776
|
-
}
|
|
10777
|
-
const orderDifference = left.sortOrder - right.sortOrder;
|
|
10778
|
-
if (orderDifference) {
|
|
10779
|
-
return orderDifference;
|
|
10780
|
-
}
|
|
10781
|
-
}
|
|
10782
|
-
const leftUpdatedAt = Date.parse(left.updatedAt);
|
|
10783
|
-
const rightUpdatedAt = Date.parse(right.updatedAt);
|
|
10784
|
-
const leftHasValidDate = Number.isFinite(leftUpdatedAt);
|
|
10785
|
-
const rightHasValidDate = Number.isFinite(rightUpdatedAt);
|
|
10786
|
-
if (leftHasValidDate && rightHasValidDate) {
|
|
10787
|
-
const updatedDifference = leftUpdatedAt - rightUpdatedAt;
|
|
10788
|
-
if (updatedDifference) {
|
|
10789
|
-
return updatedDifference;
|
|
10790
|
-
}
|
|
10791
|
-
} else if (leftHasValidDate !== rightHasValidDate) {
|
|
10792
|
-
return leftHasValidDate ? -1 : 1;
|
|
10793
|
-
}
|
|
10794
10770
|
if (left.assessmentKey !== null && right.assessmentKey !== null) {
|
|
10795
|
-
const keyDifference = left.assessmentKey
|
|
10771
|
+
const keyDifference = compareCodePoints(left.assessmentKey, right.assessmentKey);
|
|
10796
10772
|
if (keyDifference) {
|
|
10797
10773
|
return keyDifference;
|
|
10798
10774
|
}
|
|
10799
10775
|
} else if (left.assessmentKey !== null || right.assessmentKey !== null) {
|
|
10800
10776
|
return left.assessmentKey === null ? 1 : -1;
|
|
10801
10777
|
}
|
|
10802
|
-
return left.qtiTestIdentifier
|
|
10778
|
+
return compareCodePoints(left.qtiTestIdentifier, right.qtiTestIdentifier);
|
|
10779
|
+
}
|
|
10780
|
+
function compareCodePoints(left, right) {
|
|
10781
|
+
if (left === right) {
|
|
10782
|
+
return 0;
|
|
10783
|
+
}
|
|
10784
|
+
return left < right ? -1 : 1;
|
|
10803
10785
|
}
|
|
10804
10786
|
function orderRuntimeAssessmentTests(tests) {
|
|
10805
10787
|
return [...tests].toSorted(compareAssessmentCatalogOrder);
|
|
@@ -11583,7 +11565,7 @@ function playcademyDiagnosticAssessmentItemResultMetadata(value) {
|
|
|
11583
11565
|
const normalized = metadata2.responses === undefined ? { ...metadata2, responses: {} } : metadata2;
|
|
11584
11566
|
return isPlaycademyDiagnosticAssessmentItemResultMetadataV1(normalized) ? normalized : null;
|
|
11585
11567
|
}
|
|
11586
|
-
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_AWAITING_AWARD, ASSESSMENT_ATTEMPT_SUPERSEDED, ASSESSMENT_RUNTIME_ERROR_STATUS, ROUTING_KEY_MAX_LENGTH = 128, ROUTING_RESULT_KEY_MAX_LENGTH = 256, ROUTING_STAGE_LIMIT = 32, ROUTING_TRACK_LIMIT = 128, ROUTING_NODE_LIMIT = 1000, ROUTING_TERMINAL_LIMIT = 1000, ROUTING_GROUP_LIMIT = 128, ROUTING_PREDICATE_DEPTH_LIMIT = 8, ROUTING_EXPLORATION_STATE_LIMIT = 200000, RoutingKeySchema, RoutingResultKeySchema, DiagnosticTrackOutcomeSchema, DiagnosticActivationPredicateSchema, DiagnosticRoutingTransitionSchema, DiagnosticRoutingManifestV1Schema, GRADE_VALUES, POINT_RESPONSE_PATTERN, REVIEW_SELECTION_POLICY_VERSION = "unseen-first-least-recently-delivered-v1", PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY = "playcademyReviewBank", PLAYCADEMY_REVIEW_BANK_MANIFEST_VERSION = 1, DEFAULT_REVIEW_SELECTION_POLICY, ASSESSMENT_RUNTIME_FIXTURE_BUNDLE_VERSION =
|
|
11568
|
+
var SCRIPT_SCHEMES, DATA_RASTER_IMAGE_PATTERN, QTI_MATHML_ALLOWED_TAGS, metadataSymbol, parser, SUPPORTED_QTI_INTERACTION_TYPES, URL_ATTRIBUTES, IMAGE_URL_ATTRIBUTES, INTERACTION_TYPES, EMPHASIS_TAGS, STRONG_TAGS, CONTENT_SKIPPED_TAGS, BLOCK_CONTENT_KINDS, GAP_MATCH_TOKEN_TAGS, STAGE_GRAPHIC_EXCLUDED_CONTAINERS, BLANK_SENTINEL = "", MARKED_BLANK_PATTERN, MARKED_BLANK_GLOBAL_PATTERN, MATCH_CORRECT_TEMPLATE = "http://www.imsglobal.org/question/qti_v3p0/rptemplates/match_correct", MAP_RESPONSE_TEMPLATE = "https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/map_response", MAP_RESPONSE_TEMPLATES, RESPONSE_PROCESSING_TEMPLATES, VOID_BODY_ELEMENTS, URL_ATTRIBUTES2, IMAGE_URL_ATTRIBUTES2, VOID_CHOICE_ELEMENTS, NUMERIC_COMPARISON_ATTRIBUTES, RESPONSE_CARDINALITIES, RESPONSE_BASE_TYPES, POINT_INTERACTION_TYPES, INDEPENDENT_PROCESSING_TAGS, SUPPORTED_BASE_TYPES, SUPPORTED_CARDINALITIES, MATCH_CORRECT_TEMPLATES, UUID_PATTERN, COMMON_CORE_MATH_FRAMEWORK_ALIASES, COMMON_CORE_ELA_FRAMEWORK_ALIASES, COMMON_CORE_FRAMEWORK_ALIASES, ASSESSMENT_ATTEMPT_OPEN, ASSESSMENT_ATTEMPT_COMPLETED, ASSESSMENT_ATTEMPT_AWAITING_AWARD, ASSESSMENT_ATTEMPT_SUPERSEDED, ASSESSMENT_RUNTIME_ERROR_STATUS, ROUTING_KEY_MAX_LENGTH = 128, ROUTING_RESULT_KEY_MAX_LENGTH = 256, ROUTING_STAGE_LIMIT = 32, ROUTING_TRACK_LIMIT = 128, ROUTING_NODE_LIMIT = 1000, ROUTING_TERMINAL_LIMIT = 1000, ROUTING_GROUP_LIMIT = 128, ROUTING_PREDICATE_DEPTH_LIMIT = 8, ROUTING_EXPLORATION_STATE_LIMIT = 200000, RoutingKeySchema, RoutingResultKeySchema, DiagnosticTrackOutcomeSchema, DiagnosticActivationPredicateSchema, DiagnosticRoutingTransitionSchema, DiagnosticRoutingManifestV1Schema, GRADE_VALUES, POINT_RESPONSE_PATTERN, REVIEW_SELECTION_POLICY_VERSION = "unseen-first-least-recently-delivered-v1", PLAYCADEMY_REVIEW_BANK_MANIFEST_METADATA_KEY = "playcademyReviewBank", PLAYCADEMY_REVIEW_BANK_MANIFEST_VERSION = 1, DEFAULT_REVIEW_SELECTION_POLICY, ASSESSMENT_RUNTIME_FIXTURE_BUNDLE_VERSION = 8, RuntimeSubjectSchema, RuntimeGradeSchema, OptionalQueryGradeSchema, AssessmentResponseValueSchema, AssessmentRuntimeIdentitySchema, ResponseKeySchema, StartAssessmentBaseSchema, AssessmentStandardRefSchema, LatestAssessmentFilterBaseSchema, LatestAssessmentFiltersSchema, LatestRuntimeAssessmentQuerySchema, StartAssessmentBodySchema, AssessmentPreparationReceiptSchema, StartAssessmentRequestBodySchema, SaveAssessmentBodySchema, SubmitAssessmentItemBodySchema, SubmitAssessmentBodySchema, FinalizeAssessmentBodySchema, PrepareRuntimeAssessmentRequestSchema, StartRuntimeAssessmentRequestSchema, SaveRuntimeAssessmentRequestSchema, SubmitAssessmentItemRuntimeRequestSchema, SubmitRuntimeAssessmentRequestSchema, FinalizeRuntimeAssessmentRequestSchema;
|
|
11587
11569
|
var init_assessment_runtime2 = __esm(() => {
|
|
11588
11570
|
init_timeback3();
|
|
11589
11571
|
init_timeback3();
|
|
@@ -11737,6 +11719,23 @@ var init_assessment_runtime2 = __esm(() => {
|
|
|
11737
11719
|
"qti-sum",
|
|
11738
11720
|
"qti-base-value"
|
|
11739
11721
|
]);
|
|
11722
|
+
SUPPORTED_BASE_TYPES = new Set([
|
|
11723
|
+
"string",
|
|
11724
|
+
"identifier",
|
|
11725
|
+
"integer",
|
|
11726
|
+
"float",
|
|
11727
|
+
"directedPair"
|
|
11728
|
+
]);
|
|
11729
|
+
SUPPORTED_CARDINALITIES = new Set([
|
|
11730
|
+
"single",
|
|
11731
|
+
"multiple",
|
|
11732
|
+
"ordered"
|
|
11733
|
+
]);
|
|
11734
|
+
MATCH_CORRECT_TEMPLATES = new Set([
|
|
11735
|
+
"http://www.imsglobal.org/question/qti_v3p0/rptemplates/match_correct",
|
|
11736
|
+
"https://www.imsglobal.org/question/qti_v3p0/rptemplates/match_correct",
|
|
11737
|
+
"https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/match_correct"
|
|
11738
|
+
].flatMap((template) => [template, `${template}.xml`]));
|
|
11740
11739
|
UUID_PATTERN = /^(?:urn:uuid:)?\{?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}?$/i;
|
|
11741
11740
|
COMMON_CORE_MATH_FRAMEWORK_ALIASES = new Set([
|
|
11742
11741
|
"ccssm",
|
|
@@ -11913,6 +11912,8 @@ var init_assessment_runtime2 = __esm(() => {
|
|
|
11913
11912
|
standard: AssessmentStandardRefSchema
|
|
11914
11913
|
})
|
|
11915
11914
|
]);
|
|
11915
|
+
AssessmentPreparationReceiptSchema = exports_external.string().max(32000);
|
|
11916
|
+
StartAssessmentRequestBodySchema = exports_external.intersection(StartAssessmentBodySchema, exports_external.object({ preparationReceipt: AssessmentPreparationReceiptSchema.optional() }));
|
|
11916
11917
|
SaveAssessmentBodySchema = exports_external.object({
|
|
11917
11918
|
expectedResponseVersion: exports_external.number().int().nonnegative(),
|
|
11918
11919
|
responses: exports_external.record(ResponseKeySchema, exports_external.record(ResponseKeySchema, AssessmentResponseValueSchema.nullable()))
|
|
@@ -11940,7 +11941,8 @@ var init_assessment_runtime2 = __esm(() => {
|
|
|
11940
11941
|
message: "Provide either masteredUnits or masteredUnitsAbsolute, not both",
|
|
11941
11942
|
path: ["masteredUnits"]
|
|
11942
11943
|
});
|
|
11943
|
-
|
|
11944
|
+
PrepareRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.and(StartAssessmentBodySchema);
|
|
11945
|
+
StartRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.and(StartAssessmentRequestBodySchema);
|
|
11944
11946
|
SaveRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.extend(SaveAssessmentBodySchema.shape);
|
|
11945
11947
|
SubmitAssessmentItemRuntimeRequestSchema = AssessmentRuntimeIdentitySchema.extend(SubmitAssessmentItemBodySchema.shape);
|
|
11946
11948
|
SubmitRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.extend(SubmitAssessmentBodySchema.shape).extend({
|
|
@@ -12468,6 +12470,7 @@ var init_schema = __esm(() => {
|
|
|
12468
12470
|
ltiTestMode: exports_external.boolean().default(false),
|
|
12469
12471
|
platformServiceJwt: platformServiceJwtConfigSchema.optional(),
|
|
12470
12472
|
uploadBucket: exports_external.string().optional(),
|
|
12473
|
+
assessmentArtifactBucket: exports_external.string().optional(),
|
|
12471
12474
|
cdnOrigin: exports_external.string().url().startsWith("https://").optional(),
|
|
12472
12475
|
queueIngressSecret: exports_external.string().optional(),
|
|
12473
12476
|
secretsManifestPepper: exports_external.string().optional()
|
|
@@ -38547,6 +38550,319 @@ var init_infra2 = __esm(() => {
|
|
|
38547
38550
|
init_kv_backup_service();
|
|
38548
38551
|
});
|
|
38549
38552
|
|
|
38553
|
+
// ../api-core/src/utils/assessment-artifact-envelope.util.ts
|
|
38554
|
+
function nonEmptyIdentityValue(value) {
|
|
38555
|
+
return typeof value === "string" && value.length > 0 && value === value.trim();
|
|
38556
|
+
}
|
|
38557
|
+
function assessmentArtifactPairKeys(identity) {
|
|
38558
|
+
assertAssessmentArtifactIdentity(identity);
|
|
38559
|
+
const identitySegments = identity.kind === "assessment" ? ["qti", identity.qtiTestIdentifier] : [
|
|
38560
|
+
"review",
|
|
38561
|
+
identity.qtiTestIdentifier,
|
|
38562
|
+
identity.sourceContentRevision,
|
|
38563
|
+
identity.bankRevision
|
|
38564
|
+
];
|
|
38565
|
+
const identityPath = identitySegments.map(encodeURIComponent).join("/");
|
|
38566
|
+
const versionPath = `artifact-v${ASSESSMENT_ARTIFACT_SCHEMA_VERSION}/grader-v${ASSESSMENT_GRADER_PROTOCOL_VERSION}`;
|
|
38567
|
+
const prefix = `${ARTIFACT_KEY_PREFIX}/${identityPath}/${versionPath}`;
|
|
38568
|
+
return {
|
|
38569
|
+
playable: `${prefix}/playable.json`,
|
|
38570
|
+
scoring: `${prefix}/scoring.json`
|
|
38571
|
+
};
|
|
38572
|
+
}
|
|
38573
|
+
function jsonValidationMessage(value, path, ancestors) {
|
|
38574
|
+
if (value === null || typeof value === "string" || typeof value === "boolean") {
|
|
38575
|
+
return null;
|
|
38576
|
+
}
|
|
38577
|
+
if (typeof value === "number") {
|
|
38578
|
+
return Number.isFinite(value) ? null : `${path} must contain a finite number`;
|
|
38579
|
+
}
|
|
38580
|
+
if (typeof value !== "object") {
|
|
38581
|
+
return `${path} is not JSON-serializable`;
|
|
38582
|
+
}
|
|
38583
|
+
if (ancestors.has(value)) {
|
|
38584
|
+
return `${path} contains a circular reference`;
|
|
38585
|
+
}
|
|
38586
|
+
const prototype = Object.getPrototypeOf(value);
|
|
38587
|
+
if (!Array.isArray(value) && prototype !== Object.prototype && prototype !== null) {
|
|
38588
|
+
return `${path} must contain only plain JSON objects`;
|
|
38589
|
+
}
|
|
38590
|
+
if (Array.isArray(value) && (Object.keys(value).length !== value.length || Object.keys(value).some((key, index2) => key !== String(index2)) || Reflect.ownKeys(value).length !== value.length + 1)) {
|
|
38591
|
+
return `${path} must contain a dense JSON array`;
|
|
38592
|
+
}
|
|
38593
|
+
if (!Array.isArray(value) && Reflect.ownKeys(value).length !== Object.keys(value).length) {
|
|
38594
|
+
return `${path} contains non-JSON object properties`;
|
|
38595
|
+
}
|
|
38596
|
+
ancestors.add(value);
|
|
38597
|
+
const entries = Array.isArray(value) ? value.map((entry, index2) => [String(index2), entry]) : Object.entries(value);
|
|
38598
|
+
for (const [key, entry] of entries) {
|
|
38599
|
+
const message = jsonValidationMessage(entry, `${path}.${key}`, ancestors);
|
|
38600
|
+
if (message) {
|
|
38601
|
+
ancestors.delete(value);
|
|
38602
|
+
return message;
|
|
38603
|
+
}
|
|
38604
|
+
}
|
|
38605
|
+
ancestors.delete(value);
|
|
38606
|
+
return null;
|
|
38607
|
+
}
|
|
38608
|
+
function assertJsonValue(value, path) {
|
|
38609
|
+
const message = jsonValidationMessage(value, path, new Set);
|
|
38610
|
+
if (message) {
|
|
38611
|
+
throw new ValidationError(message);
|
|
38612
|
+
}
|
|
38613
|
+
}
|
|
38614
|
+
function isRecord6(value) {
|
|
38615
|
+
return typeof value === "object" && value !== null && !Array.isArray(value);
|
|
38616
|
+
}
|
|
38617
|
+
function hasExactlyKeys(value, keys) {
|
|
38618
|
+
const actual = Object.keys(value).toSorted();
|
|
38619
|
+
const expected = [...keys].toSorted();
|
|
38620
|
+
return actual.length === expected.length && actual.every((key, index2) => key === expected[index2]);
|
|
38621
|
+
}
|
|
38622
|
+
function assertAssessmentArtifactIdentity(identity) {
|
|
38623
|
+
if (!isRecord6(identity) || !nonEmptyIdentityValue(identity.qtiTestIdentifier)) {
|
|
38624
|
+
throw new ValidationError("Assessment artifact identity is invalid");
|
|
38625
|
+
}
|
|
38626
|
+
if (identity.kind === "assessment" && hasExactlyKeys(identity, ["kind", "qtiTestIdentifier", "contentRevision"]) && nonEmptyIdentityValue(identity.contentRevision)) {
|
|
38627
|
+
return;
|
|
38628
|
+
}
|
|
38629
|
+
if (identity.kind === "review-bank" && hasExactlyKeys(identity, [
|
|
38630
|
+
"kind",
|
|
38631
|
+
"qtiTestIdentifier",
|
|
38632
|
+
"sourceContentRevision",
|
|
38633
|
+
"bankRevision"
|
|
38634
|
+
]) && nonEmptyIdentityValue(identity.sourceContentRevision) && nonEmptyIdentityValue(identity.bankRevision)) {
|
|
38635
|
+
return;
|
|
38636
|
+
}
|
|
38637
|
+
throw new ValidationError("Assessment artifact identity is invalid");
|
|
38638
|
+
}
|
|
38639
|
+
function envelopeWithoutChecksum({
|
|
38640
|
+
checksum: _checksum,
|
|
38641
|
+
...content
|
|
38642
|
+
}) {
|
|
38643
|
+
return content;
|
|
38644
|
+
}
|
|
38645
|
+
async function prepareAssessmentArtifact(input) {
|
|
38646
|
+
assertAssessmentArtifactIdentity(input.identity);
|
|
38647
|
+
assertJsonValue(input.payload, "Assessment artifact payload");
|
|
38648
|
+
const content = {
|
|
38649
|
+
artifactKind: input.artifactKind,
|
|
38650
|
+
artifactSchemaVersion: ASSESSMENT_ARTIFACT_SCHEMA_VERSION,
|
|
38651
|
+
graderProtocolVersion: ASSESSMENT_GRADER_PROTOCOL_VERSION,
|
|
38652
|
+
identity: input.identity,
|
|
38653
|
+
payload: input.payload
|
|
38654
|
+
};
|
|
38655
|
+
const checksum = await sha256Hex(canonicalJson2(content));
|
|
38656
|
+
const envelope = {
|
|
38657
|
+
...content,
|
|
38658
|
+
checksum: { algorithm: "sha256", value: checksum }
|
|
38659
|
+
};
|
|
38660
|
+
return {
|
|
38661
|
+
envelope,
|
|
38662
|
+
bytes: textEncoder.encode(canonicalJson2(envelope)),
|
|
38663
|
+
key: assessmentArtifactPairKeys(input.identity)[input.artifactKind]
|
|
38664
|
+
};
|
|
38665
|
+
}
|
|
38666
|
+
function parseEnvelope(value) {
|
|
38667
|
+
if (!isRecord6(value) || !hasExactlyKeys(value, [
|
|
38668
|
+
"artifactKind",
|
|
38669
|
+
"artifactSchemaVersion",
|
|
38670
|
+
"graderProtocolVersion",
|
|
38671
|
+
"identity",
|
|
38672
|
+
"checksum",
|
|
38673
|
+
"payload"
|
|
38674
|
+
]) || value.artifactKind !== "playable" && value.artifactKind !== "scoring" || value.artifactSchemaVersion !== ASSESSMENT_ARTIFACT_SCHEMA_VERSION || value.graderProtocolVersion !== ASSESSMENT_GRADER_PROTOCOL_VERSION) {
|
|
38675
|
+
throw new ValidationError("Assessment artifact envelope is invalid or incompatible");
|
|
38676
|
+
}
|
|
38677
|
+
assertAssessmentArtifactIdentity(value.identity);
|
|
38678
|
+
if (!isRecord6(value.checksum) || !hasExactlyKeys(value.checksum, ["algorithm", "value"]) || value.checksum.algorithm !== "sha256" || typeof value.checksum.value !== "string" || !SHA256_HEX.test(value.checksum.value)) {
|
|
38679
|
+
throw new ValidationError("Assessment artifact checksum is invalid");
|
|
38680
|
+
}
|
|
38681
|
+
assertJsonValue(value.payload, "Assessment artifact payload");
|
|
38682
|
+
return value;
|
|
38683
|
+
}
|
|
38684
|
+
async function verifyAssessmentArtifactBytes(bytes) {
|
|
38685
|
+
let source;
|
|
38686
|
+
let parsed;
|
|
38687
|
+
try {
|
|
38688
|
+
source = new TextDecoder("utf-8", { fatal: true, ignoreBOM: true }).decode(bytes);
|
|
38689
|
+
parsed = JSON.parse(source);
|
|
38690
|
+
} catch {
|
|
38691
|
+
throw new ValidationError("Assessment artifact is not valid UTF-8 JSON");
|
|
38692
|
+
}
|
|
38693
|
+
const envelope = parseEnvelope(parsed);
|
|
38694
|
+
const canonicalEnvelope = canonicalJson2(envelope);
|
|
38695
|
+
if (source !== canonicalEnvelope) {
|
|
38696
|
+
throw new ValidationError("Assessment artifact bytes are not canonically encoded");
|
|
38697
|
+
}
|
|
38698
|
+
const expectedChecksum = await sha256Hex(canonicalJson2(envelopeWithoutChecksum(envelope)));
|
|
38699
|
+
if (envelope.checksum.value !== expectedChecksum) {
|
|
38700
|
+
throw new ValidationError("Assessment artifact checksum does not match its contents");
|
|
38701
|
+
}
|
|
38702
|
+
return envelope;
|
|
38703
|
+
}
|
|
38704
|
+
function assessmentArtifactIdentitiesMatch(left, right) {
|
|
38705
|
+
return canonicalJson2(left) === canonicalJson2(right);
|
|
38706
|
+
}
|
|
38707
|
+
function assertMatchingAssessmentArtifactPair(playable, scoring, expectedIdentity) {
|
|
38708
|
+
if (playable.artifactKind !== "playable" || scoring.artifactKind !== "scoring" || playable.artifactSchemaVersion !== scoring.artifactSchemaVersion || playable.graderProtocolVersion !== scoring.graderProtocolVersion || !assessmentArtifactIdentitiesMatch(playable.identity, scoring.identity) || expectedIdentity && !assessmentArtifactIdentitiesMatch(playable.identity, expectedIdentity)) {
|
|
38709
|
+
throw new ValidationError("Assessment artifact pair does not describe one revision");
|
|
38710
|
+
}
|
|
38711
|
+
}
|
|
38712
|
+
var ASSESSMENT_ARTIFACT_SCHEMA_VERSION = 1, ASSESSMENT_GRADER_PROTOCOL_VERSION = 1, ARTIFACT_KEY_PREFIX = "assessment-artifacts", SHA256_HEX, textEncoder;
|
|
38713
|
+
var init_assessment_artifact_envelope_util = __esm(() => {
|
|
38714
|
+
init_errors2();
|
|
38715
|
+
SHA256_HEX = /^[a-f0-9]{64}$/;
|
|
38716
|
+
textEncoder = new TextEncoder;
|
|
38717
|
+
});
|
|
38718
|
+
|
|
38719
|
+
// ../api-core/src/utils/assessment-artifact-storage.util.ts
|
|
38720
|
+
function requireBucketName(deps) {
|
|
38721
|
+
const bucketName = deps.bucketName?.trim();
|
|
38722
|
+
if (!bucketName) {
|
|
38723
|
+
throw new ServiceUnavailableError("Assessment artifact storage is not configured");
|
|
38724
|
+
}
|
|
38725
|
+
return bucketName;
|
|
38726
|
+
}
|
|
38727
|
+
function bytesEqual(left, right) {
|
|
38728
|
+
return left.length === right.length && left.every((byte, index2) => byte === right[index2]);
|
|
38729
|
+
}
|
|
38730
|
+
async function holdsExpectedBytes(deps, bucketName, artifact) {
|
|
38731
|
+
const stored = await deps.storage.getObject(bucketName, artifact.key);
|
|
38732
|
+
if (!stored) {
|
|
38733
|
+
return false;
|
|
38734
|
+
}
|
|
38735
|
+
if (!bytesEqual(stored.body, artifact.bytes)) {
|
|
38736
|
+
throw new ConflictError("Immutable assessment artifact key contains different bytes", {
|
|
38737
|
+
key: artifact.key
|
|
38738
|
+
});
|
|
38739
|
+
}
|
|
38740
|
+
return true;
|
|
38741
|
+
}
|
|
38742
|
+
async function ensureStored(deps, bucketName, artifact, alreadyStored) {
|
|
38743
|
+
const created = !alreadyStored && await deps.storage.putObjectIfAbsent(bucketName, artifact.key, artifact.bytes, {
|
|
38744
|
+
contentType: "application/json"
|
|
38745
|
+
});
|
|
38746
|
+
if (!await holdsExpectedBytes(deps, bucketName, artifact)) {
|
|
38747
|
+
throw new ServiceUnavailableError("Assessment artifact disappeared during storage", {
|
|
38748
|
+
key: artifact.key
|
|
38749
|
+
});
|
|
38750
|
+
}
|
|
38751
|
+
return created ? "created" : "existing";
|
|
38752
|
+
}
|
|
38753
|
+
async function storeAssessmentArtifactPair(deps, input) {
|
|
38754
|
+
const bucketName = requireBucketName(deps);
|
|
38755
|
+
const [playable, scoring] = await Promise.all([
|
|
38756
|
+
prepareAssessmentArtifact({
|
|
38757
|
+
artifactKind: "playable",
|
|
38758
|
+
identity: input.identity,
|
|
38759
|
+
payload: input.playablePayload
|
|
38760
|
+
}),
|
|
38761
|
+
prepareAssessmentArtifact({
|
|
38762
|
+
artifactKind: "scoring",
|
|
38763
|
+
identity: input.identity,
|
|
38764
|
+
payload: input.scoringPayload
|
|
38765
|
+
})
|
|
38766
|
+
]);
|
|
38767
|
+
const [playableStored, scoringStored] = await Promise.all([
|
|
38768
|
+
holdsExpectedBytes(deps, bucketName, playable),
|
|
38769
|
+
holdsExpectedBytes(deps, bucketName, scoring)
|
|
38770
|
+
]);
|
|
38771
|
+
const playableStatus = await ensureStored(deps, bucketName, playable, playableStored);
|
|
38772
|
+
const scoringStatus = await ensureStored(deps, bucketName, scoring, scoringStored);
|
|
38773
|
+
const [storedPlayable, storedScoring] = await Promise.all([
|
|
38774
|
+
verifyAssessmentArtifactBytes(playable.bytes),
|
|
38775
|
+
verifyAssessmentArtifactBytes(scoring.bytes)
|
|
38776
|
+
]);
|
|
38777
|
+
assertMatchingAssessmentArtifactPair(storedPlayable, storedScoring, input.identity);
|
|
38778
|
+
return {
|
|
38779
|
+
playable: {
|
|
38780
|
+
key: playable.key,
|
|
38781
|
+
checksum: playable.envelope.checksum.value,
|
|
38782
|
+
status: playableStatus
|
|
38783
|
+
},
|
|
38784
|
+
scoring: {
|
|
38785
|
+
key: scoring.key,
|
|
38786
|
+
checksum: scoring.envelope.checksum.value,
|
|
38787
|
+
status: scoringStatus
|
|
38788
|
+
}
|
|
38789
|
+
};
|
|
38790
|
+
}
|
|
38791
|
+
async function loadAssessmentArtifactPair(deps, identity) {
|
|
38792
|
+
const bucketName = requireBucketName(deps);
|
|
38793
|
+
const keys = assessmentArtifactPairKeys(identity);
|
|
38794
|
+
const [playableObject, scoringObject] = await Promise.all([
|
|
38795
|
+
deps.storage.getObject(bucketName, keys.playable),
|
|
38796
|
+
deps.storage.getObject(bucketName, keys.scoring)
|
|
38797
|
+
]);
|
|
38798
|
+
if (!playableObject || !scoringObject) {
|
|
38799
|
+
return null;
|
|
38800
|
+
}
|
|
38801
|
+
const [playable, scoring] = await Promise.all([
|
|
38802
|
+
verifyAssessmentArtifactBytes(playableObject.body),
|
|
38803
|
+
verifyAssessmentArtifactBytes(scoringObject.body)
|
|
38804
|
+
]);
|
|
38805
|
+
assertMatchingAssessmentArtifactPair(playable, scoring, identity);
|
|
38806
|
+
return {
|
|
38807
|
+
playable,
|
|
38808
|
+
scoring
|
|
38809
|
+
};
|
|
38810
|
+
}
|
|
38811
|
+
async function loadVerifiedArtifact(deps, key) {
|
|
38812
|
+
const bucketName = requireBucketName(deps);
|
|
38813
|
+
const object = await deps.storage.getObject(bucketName, key);
|
|
38814
|
+
return object ? verifyAssessmentArtifactBytes(object.body) : null;
|
|
38815
|
+
}
|
|
38816
|
+
async function loadAssessmentArtifact(deps, artifactKind, identity) {
|
|
38817
|
+
const artifact = await loadVerifiedArtifact(deps, assessmentArtifactPairKeys(identity)[artifactKind]);
|
|
38818
|
+
if (artifact && (artifact.artifactKind !== artifactKind || !assessmentArtifactIdentitiesMatch(artifact.identity, identity))) {
|
|
38819
|
+
throw new ValidationError("Assessment artifact does not describe the requested revision");
|
|
38820
|
+
}
|
|
38821
|
+
return artifact;
|
|
38822
|
+
}
|
|
38823
|
+
async function loadManagedPlayableArtifact(deps, qtiTestIdentifier) {
|
|
38824
|
+
const key = assessmentArtifactPairKeys({
|
|
38825
|
+
kind: "assessment",
|
|
38826
|
+
qtiTestIdentifier,
|
|
38827
|
+
contentRevision: "unbound-managed-lookup"
|
|
38828
|
+
}).playable;
|
|
38829
|
+
const artifact = await loadVerifiedArtifact(deps, key);
|
|
38830
|
+
if (artifact && (artifact.artifactKind !== "playable" || artifact.identity.kind !== "assessment" || artifact.identity.qtiTestIdentifier !== qtiTestIdentifier)) {
|
|
38831
|
+
throw new ValidationError("Assessment artifact does not describe the requested assessment");
|
|
38832
|
+
}
|
|
38833
|
+
return artifact;
|
|
38834
|
+
}
|
|
38835
|
+
var init_assessment_artifact_storage_util = __esm(() => {
|
|
38836
|
+
init_errors2();
|
|
38837
|
+
init_assessment_artifact_envelope_util();
|
|
38838
|
+
});
|
|
38839
|
+
|
|
38840
|
+
// ../api-core/src/services/assessment-artifacts.service.ts
|
|
38841
|
+
class AssessmentArtifactsService {
|
|
38842
|
+
deps;
|
|
38843
|
+
constructor(deps) {
|
|
38844
|
+
this.deps = deps;
|
|
38845
|
+
}
|
|
38846
|
+
storePair(input) {
|
|
38847
|
+
return storeAssessmentArtifactPair(this.deps, input);
|
|
38848
|
+
}
|
|
38849
|
+
loadPair(identity) {
|
|
38850
|
+
return loadAssessmentArtifactPair(this.deps, identity);
|
|
38851
|
+
}
|
|
38852
|
+
loadPlayable(identity) {
|
|
38853
|
+
return loadAssessmentArtifact(this.deps, "playable", identity);
|
|
38854
|
+
}
|
|
38855
|
+
loadScoring(identity) {
|
|
38856
|
+
return loadAssessmentArtifact(this.deps, "scoring", identity);
|
|
38857
|
+
}
|
|
38858
|
+
loadManagedPlayable(qtiTestIdentifier) {
|
|
38859
|
+
return loadManagedPlayableArtifact(this.deps, qtiTestIdentifier);
|
|
38860
|
+
}
|
|
38861
|
+
}
|
|
38862
|
+
var init_assessment_artifacts_service = __esm(() => {
|
|
38863
|
+
init_assessment_artifact_storage_util();
|
|
38864
|
+
});
|
|
38865
|
+
|
|
38550
38866
|
// ../../node_modules/.bun/sharp@0.34.2/node_modules/sharp/lib/is.js
|
|
38551
38867
|
var require_is = __commonJS((exports, module2) => {
|
|
38552
38868
|
var defined = function(val) {
|
|
@@ -47142,7 +47458,7 @@ function rejectSystemManagedReview(input, context2) {
|
|
|
47142
47458
|
});
|
|
47143
47459
|
}
|
|
47144
47460
|
}
|
|
47145
|
-
var TimebackGradeSchema, TimebackSubjectSchema, CourseGoalsSchema, UpdateGameTimebackIntegrationRequestSchema, CreateGameTimebackIntegrationRequestSchema, TimebackActivityDataSchema, EndActivityRequestSchema, GameRunMetricsSchema, GameCourseMetricsSchema, GameMetricsResponseSchema, AdvanceCourseRequestSchema, UnenrollCourseRequestSchema, HeartbeatRequestSchema, PopulateStudentRequestSchema, DerivedPlatformCourseConfigSchema, TimebackBaseConfigSchema, PlatformTimebackSetupRequestSchema, AdminTimebackMutationBaseSchema, AdminAttributionDateSchema, ADMIN_GRANT_XP_MIN = -1e5, ADMIN_GRANT_XP_MAX = 1e5, ADMIN_GRANT_XP_AMOUNT_RANGE_MESSAGE, GrantTimebackXpRequestSchema, AdjustTimebackTimeRequestSchema, AdjustTimebackMasteryRequestSchema, ReconcileMasteryForConfigChangeSchema, EnrollStudentRequestSchema, UnenrollStudentRequestSchema, ReactivateEnrollmentRequestSchema, VerifyTimebackMetricDiscrepancyRequestSchema, AssessmentPurposeSchema, AssessmentStatusSchema, AssessmentKeySchema, DiagnosticKeySchema, DiagnosticRoutingManifestSchema, DiagnosticAssessmentDefinitionInputSchema, AssessmentStandardRefSchema2, CreateAssessmentRequestSchema, UpdateAssessmentRequestSchema, CopyAssessmentRequestSchema, AssessmentAssociationImportEntrySchema, AttachExistingAssessmentsRequestSchema,
|
|
47461
|
+
var TimebackGradeSchema, TimebackSubjectSchema, CourseGoalsSchema, UpdateGameTimebackIntegrationRequestSchema, CreateGameTimebackIntegrationRequestSchema, TimebackActivityDataSchema, EndActivityRequestSchema, GameRunMetricsSchema, GameCourseMetricsSchema, GameMetricsResponseSchema, AdvanceCourseRequestSchema, UnenrollCourseRequestSchema, HeartbeatRequestSchema, PopulateStudentRequestSchema, DerivedPlatformCourseConfigSchema, TimebackBaseConfigSchema, PlatformTimebackSetupRequestSchema, AdminTimebackMutationBaseSchema, AdminAttributionDateSchema, ADMIN_GRANT_XP_MIN = -1e5, ADMIN_GRANT_XP_MAX = 1e5, ADMIN_GRANT_XP_AMOUNT_RANGE_MESSAGE, GrantTimebackXpRequestSchema, AdjustTimebackTimeRequestSchema, AdjustTimebackMasteryRequestSchema, ReconcileMasteryForConfigChangeSchema, EnrollStudentRequestSchema, UnenrollStudentRequestSchema, ReactivateEnrollmentRequestSchema, VerifyTimebackMetricDiscrepancyRequestSchema, AssessmentPurposeSchema, AssessmentStatusSchema, AssessmentKeySchema, DiagnosticKeySchema, DiagnosticRoutingManifestSchema, DiagnosticAssessmentDefinitionInputSchema, AssessmentStandardRefSchema2, CreateAssessmentRequestSchema, UpdateAssessmentRequestSchema, CopyAssessmentRequestSchema, AssessmentAssociationImportEntrySchema, AttachExistingAssessmentsRequestSchema, ReorderQuestionsRequestSchema;
|
|
47146
47462
|
var init_schemas4 = __esm(() => {
|
|
47147
47463
|
init_esm();
|
|
47148
47464
|
init_src();
|
|
@@ -47429,8 +47745,7 @@ var init_schemas4 = __esm(() => {
|
|
|
47429
47745
|
assessmentKey: AssessmentKeySchema,
|
|
47430
47746
|
qtiTestIdentifier: exports_external.string().trim().min(1, "Assessment identifier is required"),
|
|
47431
47747
|
purpose: AssessmentPurposeSchema,
|
|
47432
|
-
standard: AssessmentStandardRefSchema2.optional()
|
|
47433
|
-
sortOrder: exports_external.number().int().nonnegative().nullable()
|
|
47748
|
+
standard: AssessmentStandardRefSchema2.optional()
|
|
47434
47749
|
}).superRefine((input, context2) => {
|
|
47435
47750
|
requireMasteryStandard(input, context2);
|
|
47436
47751
|
rejectSystemManagedReview(input, context2);
|
|
@@ -47480,10 +47795,6 @@ var init_schemas4 = __esm(() => {
|
|
|
47480
47795
|
seenKeys.add(assessment.assessmentKey);
|
|
47481
47796
|
});
|
|
47482
47797
|
});
|
|
47483
|
-
ReorderAssessmentsRequestSchema = exports_external.object({
|
|
47484
|
-
purpose: AssessmentPurposeSchema,
|
|
47485
|
-
testIdentifiers: exports_external.array(exports_external.string().trim().min(1, "Assessment identifier is required")).min(1, "At least one assessment is required")
|
|
47486
|
-
}).superRefine(rejectSystemManagedReview);
|
|
47487
47798
|
ReorderQuestionsRequestSchema = exports_external.object({
|
|
47488
47799
|
items: exports_external.array(exports_external.object({
|
|
47489
47800
|
identifier: exports_external.string().min(1),
|
|
@@ -95680,6 +95991,365 @@ function supportedQtiQuestionInteractionType(question) {
|
|
|
95680
95991
|
return null;
|
|
95681
95992
|
}
|
|
95682
95993
|
}
|
|
95994
|
+
function childElements(node, localName2) {
|
|
95995
|
+
return node.children.filter((child) => child.kind === "element" && (!localName2 || child.localName === localName2));
|
|
95996
|
+
}
|
|
95997
|
+
function onlyWhitespaceText(nodes) {
|
|
95998
|
+
return nodes.every((node) => node.kind === "element" || node.value.trim() === "");
|
|
95999
|
+
}
|
|
96000
|
+
function onlyAttributes(node, allowed) {
|
|
96001
|
+
const names = Object.keys(node.attributes);
|
|
96002
|
+
return names.length === allowed.length && names.every((name3) => allowed.includes(name3));
|
|
96003
|
+
}
|
|
96004
|
+
function directText(node) {
|
|
96005
|
+
if (childElements(node).length > 0) {
|
|
96006
|
+
return null;
|
|
96007
|
+
}
|
|
96008
|
+
return node.children.map((child) => child.kind === "text" ? child.value : "").join("").trim();
|
|
96009
|
+
}
|
|
96010
|
+
function itemElement(root) {
|
|
96011
|
+
return root.localName === "qti-assessment-item" ? root : xmlDescendants(root, (candidate) => candidate.localName === "qti-assessment-item")[0];
|
|
96012
|
+
}
|
|
96013
|
+
function fallback(itemIdentifier, maxScore, reason) {
|
|
96014
|
+
return {
|
|
96015
|
+
strategy: "qti",
|
|
96016
|
+
itemIdentifier,
|
|
96017
|
+
maxScore,
|
|
96018
|
+
reason
|
|
96019
|
+
};
|
|
96020
|
+
}
|
|
96021
|
+
function parsedNumericValue(value, baseType) {
|
|
96022
|
+
const syntax = baseType === "integer" ? /^[+-]?\d+$/ : /^[+-]?(?:\d+(?:\.\d*)?|\.\d+)(?:e[+-]?\d+)?$/i;
|
|
96023
|
+
if (!syntax.test(value.trim())) {
|
|
96024
|
+
return null;
|
|
96025
|
+
}
|
|
96026
|
+
const numeric3 = Number(value);
|
|
96027
|
+
return Number.isFinite(numeric3) && (baseType !== "integer" || Number.isSafeInteger(numeric3)) ? numeric3 : null;
|
|
96028
|
+
}
|
|
96029
|
+
function numericCorrectValue(declaration) {
|
|
96030
|
+
if (declaration.cardinality !== "single" || declaration.correctValues.length !== 1 || declaration.baseType !== "integer" && declaration.baseType !== "float") {
|
|
96031
|
+
return null;
|
|
96032
|
+
}
|
|
96033
|
+
return parsedNumericValue(declaration.correctValues[0], declaration.baseType);
|
|
96034
|
+
}
|
|
96035
|
+
function declarationResult(raw, normalized) {
|
|
96036
|
+
const identifier = raw.attributes.identifier?.trim();
|
|
96037
|
+
if (!normalized || !identifier || normalized.identifier !== identifier) {
|
|
96038
|
+
return { reason: "invalid-correct-response" };
|
|
96039
|
+
}
|
|
96040
|
+
const cardinality = (raw.attributes.cardinality || "single").trim();
|
|
96041
|
+
if (!SUPPORTED_CARDINALITIES2.has(cardinality)) {
|
|
96042
|
+
return { reason: "unsupported-cardinality" };
|
|
96043
|
+
}
|
|
96044
|
+
const baseType = (raw.attributes["base-type"] || "string").trim();
|
|
96045
|
+
if (!SUPPORTED_BASE_TYPES2.has(baseType)) {
|
|
96046
|
+
return { reason: "unsupported-base-type" };
|
|
96047
|
+
}
|
|
96048
|
+
if (baseType === "directedPair" && cardinality !== "multiple") {
|
|
96049
|
+
return { reason: "unsupported-cardinality" };
|
|
96050
|
+
}
|
|
96051
|
+
const declaration = {
|
|
96052
|
+
identifier,
|
|
96053
|
+
cardinality,
|
|
96054
|
+
baseType,
|
|
96055
|
+
correctValues: [...normalized.correctValues]
|
|
96056
|
+
};
|
|
96057
|
+
if (declaration.correctValues.length === 0 || declaration.correctValues.some((value) => value.trim() === "") || declaration.cardinality === "single" && declaration.correctValues.length !== 1 && declaration.baseType !== "string") {
|
|
96058
|
+
return { reason: "invalid-correct-response" };
|
|
96059
|
+
}
|
|
96060
|
+
if (declaration.baseType === "directedPair" && (declaration.correctValues.some((value) => !DIRECTED_PAIR_VALUE.test(value)) || new Set(declaration.correctValues).size !== declaration.correctValues.length)) {
|
|
96061
|
+
return { reason: "invalid-correct-response" };
|
|
96062
|
+
}
|
|
96063
|
+
if ((declaration.baseType === "integer" || declaration.baseType === "float") && declaration.correctValues.some((value) => parsedNumericValue(value, declaration.baseType) === null)) {
|
|
96064
|
+
return { reason: "invalid-correct-response" };
|
|
96065
|
+
}
|
|
96066
|
+
return { declaration };
|
|
96067
|
+
}
|
|
96068
|
+
function scoringDeclarations(rawDeclarations, normalizedDeclarations) {
|
|
96069
|
+
const rawIdentifiers = rawDeclarations.map((declaration) => declaration.attributes.identifier?.trim());
|
|
96070
|
+
if (rawDeclarations.length !== normalizedDeclarations.length || rawIdentifiers.some((identifier) => !identifier) || new Set(rawIdentifiers).size !== rawIdentifiers.length) {
|
|
96071
|
+
return { reason: "invalid-correct-response" };
|
|
96072
|
+
}
|
|
96073
|
+
const declarations = new Map;
|
|
96074
|
+
for (const [index2, rawDeclaration] of rawDeclarations.entries()) {
|
|
96075
|
+
const normalized = normalizedDeclarations[index2];
|
|
96076
|
+
if (normalized?.correctValues.length) {
|
|
96077
|
+
const result = declarationResult(rawDeclaration, normalized);
|
|
96078
|
+
if ("reason" in result) {
|
|
96079
|
+
return result;
|
|
96080
|
+
}
|
|
96081
|
+
if (declarations.has(result.declaration.identifier)) {
|
|
96082
|
+
return { reason: "invalid-correct-response" };
|
|
96083
|
+
}
|
|
96084
|
+
declarations.set(result.declaration.identifier, result.declaration);
|
|
96085
|
+
}
|
|
96086
|
+
}
|
|
96087
|
+
return declarations.size > 0 ? { declarations } : { reason: "no-scorable-response" };
|
|
96088
|
+
}
|
|
96089
|
+
function declarationConstructReason(declarations) {
|
|
96090
|
+
if (declarations.some((declaration) => childElements(declaration, "qti-mapping").length > 0)) {
|
|
96091
|
+
return "response-mapping";
|
|
96092
|
+
}
|
|
96093
|
+
return declarations.some((declaration) => childElements(declaration, "qti-area-mapping").length > 0) ? "area-mapping" : null;
|
|
96094
|
+
}
|
|
96095
|
+
function sameScore(left, right) {
|
|
96096
|
+
return Math.abs(left - right) <= Number.EPSILON * Math.max(1, Math.abs(left), Math.abs(right));
|
|
96097
|
+
}
|
|
96098
|
+
function platformResult(itemIdentifier, parsedMaxScore, rules) {
|
|
96099
|
+
const ruleMaximum = rules.reduce((sum, rule) => sum + rule.points, 0);
|
|
96100
|
+
if (!Number.isFinite(ruleMaximum) || ruleMaximum <= 0) {
|
|
96101
|
+
return fallback(itemIdentifier, parsedMaxScore, "no-scorable-response");
|
|
96102
|
+
}
|
|
96103
|
+
if (!sameScore(parsedMaxScore, ruleMaximum)) {
|
|
96104
|
+
return fallback(itemIdentifier, parsedMaxScore, "inconsistent-max-score");
|
|
96105
|
+
}
|
|
96106
|
+
return {
|
|
96107
|
+
strategy: "platform",
|
|
96108
|
+
itemIdentifier,
|
|
96109
|
+
maxScore: ruleMaximum,
|
|
96110
|
+
rules
|
|
96111
|
+
};
|
|
96112
|
+
}
|
|
96113
|
+
function matchRule(declaration, points) {
|
|
96114
|
+
return {
|
|
96115
|
+
responseIdentifier: declaration.identifier,
|
|
96116
|
+
cardinality: declaration.cardinality,
|
|
96117
|
+
baseType: declaration.baseType,
|
|
96118
|
+
points,
|
|
96119
|
+
comparison: {
|
|
96120
|
+
kind: "match",
|
|
96121
|
+
correctValues: [...declaration.correctValues]
|
|
96122
|
+
}
|
|
96123
|
+
};
|
|
96124
|
+
}
|
|
96125
|
+
function operandIdentifiers(comparison) {
|
|
96126
|
+
const operands = childElements(comparison);
|
|
96127
|
+
if (operands.length !== 2 || !onlyWhitespaceText(comparison.children) || operands.some((operand) => !onlyAttributes(operand, ["identifier"]))) {
|
|
96128
|
+
return null;
|
|
96129
|
+
}
|
|
96130
|
+
const variable = operands.find((operand) => operand.localName === "qti-variable");
|
|
96131
|
+
const correct = operands.find((operand) => operand.localName === "qti-correct");
|
|
96132
|
+
if (!variable || !correct || childElements(variable).length > 0 || childElements(correct).length > 0) {
|
|
96133
|
+
return null;
|
|
96134
|
+
}
|
|
96135
|
+
if (!onlyWhitespaceText(variable.children) || !onlyWhitespaceText(correct.children)) {
|
|
96136
|
+
return null;
|
|
96137
|
+
}
|
|
96138
|
+
const variableIdentifier = variable.attributes.identifier?.trim();
|
|
96139
|
+
const correctIdentifier = correct.attributes.identifier?.trim();
|
|
96140
|
+
return variableIdentifier && correctIdentifier ? { variable: variableIdentifier, correct: correctIdentifier } : null;
|
|
96141
|
+
}
|
|
96142
|
+
function comparisonResult(comparison, declaration) {
|
|
96143
|
+
const identifiers = operandIdentifiers(comparison);
|
|
96144
|
+
if (!identifiers || identifiers.variable !== declaration.identifier || identifiers.correct !== declaration.identifier) {
|
|
96145
|
+
return null;
|
|
96146
|
+
}
|
|
96147
|
+
if (comparison.localName === "qti-match" && onlyAttributes(comparison, [])) {
|
|
96148
|
+
return {
|
|
96149
|
+
kind: "match",
|
|
96150
|
+
correctValues: [...declaration.correctValues]
|
|
96151
|
+
};
|
|
96152
|
+
}
|
|
96153
|
+
const correctValue = numericCorrectValue(declaration);
|
|
96154
|
+
if (correctValue === null) {
|
|
96155
|
+
return null;
|
|
96156
|
+
}
|
|
96157
|
+
if (comparison.localName === "qti-equal" && onlyAttributes(comparison, [])) {
|
|
96158
|
+
return { kind: "numeric-equal", correctValue };
|
|
96159
|
+
}
|
|
96160
|
+
if (comparison.localName !== "qti-equal-rounded" || !onlyAttributes(comparison, ["rounding-mode", "figures"])) {
|
|
96161
|
+
return null;
|
|
96162
|
+
}
|
|
96163
|
+
const roundingMode = comparison.attributes["rounding-mode"];
|
|
96164
|
+
const figures = Number(comparison.attributes.figures);
|
|
96165
|
+
const validFigures = Number.isInteger(figures) && (roundingMode === "decimalPlaces" ? figures >= 0 : figures > 0);
|
|
96166
|
+
if (roundingMode !== "decimalPlaces" && roundingMode !== "significantFigures" || !validFigures) {
|
|
96167
|
+
return null;
|
|
96168
|
+
}
|
|
96169
|
+
return {
|
|
96170
|
+
kind: "numeric-equal-rounded",
|
|
96171
|
+
correctValue,
|
|
96172
|
+
roundingMode,
|
|
96173
|
+
figures
|
|
96174
|
+
};
|
|
96175
|
+
}
|
|
96176
|
+
function responseReferences(node, responseIdentifiers) {
|
|
96177
|
+
return new Set(xmlDescendants(node, (candidate) => candidate.localName === "qti-variable" || candidate.localName === "qti-correct").map((candidate) => candidate.attributes.identifier?.trim()).filter((identifier) => Boolean(identifier) && responseIdentifiers.has(identifier)));
|
|
96178
|
+
}
|
|
96179
|
+
function additivePoints(setOutcome) {
|
|
96180
|
+
if (!onlyAttributes(setOutcome, ["identifier"]) || setOutcome.attributes.identifier !== "SCORE" || !onlyWhitespaceText(setOutcome.children)) {
|
|
96181
|
+
return null;
|
|
96182
|
+
}
|
|
96183
|
+
const outcomeChildren = childElements(setOutcome);
|
|
96184
|
+
if (outcomeChildren.length !== 1 || outcomeChildren[0]?.localName !== "qti-sum") {
|
|
96185
|
+
return null;
|
|
96186
|
+
}
|
|
96187
|
+
const sum = outcomeChildren[0];
|
|
96188
|
+
if (!onlyAttributes(sum, []) || !onlyWhitespaceText(sum.children)) {
|
|
96189
|
+
return null;
|
|
96190
|
+
}
|
|
96191
|
+
const operands = childElements(sum);
|
|
96192
|
+
const scoreVariable = operands.find((operand) => operand.localName === "qti-variable" && operand.attributes.identifier === "SCORE");
|
|
96193
|
+
const pointValue = operands.find((operand) => operand.localName === "qti-base-value");
|
|
96194
|
+
if (operands.length !== 2 || !scoreVariable || !pointValue || !onlyAttributes(scoreVariable, ["identifier"]) || !onlyWhitespaceText(scoreVariable.children) || childElements(scoreVariable).length > 0 || !onlyAttributes(pointValue, ["base-type"]) || pointValue.attributes["base-type"] !== "float") {
|
|
96195
|
+
return null;
|
|
96196
|
+
}
|
|
96197
|
+
const rawPoints = directText(pointValue);
|
|
96198
|
+
const points = rawPoints === null ? Number.NaN : Number(rawPoints);
|
|
96199
|
+
return Number.isFinite(points) && points > 0 ? points : null;
|
|
96200
|
+
}
|
|
96201
|
+
function additiveRule(condition, declarations) {
|
|
96202
|
+
const responseIdentifiers = new Set(declarations.keys());
|
|
96203
|
+
const references = responseReferences(condition, responseIdentifiers);
|
|
96204
|
+
if (references.size > 1) {
|
|
96205
|
+
return { reason: "cross-response-processing" };
|
|
96206
|
+
}
|
|
96207
|
+
if (references.size !== 1 || !onlyAttributes(condition, []) || !onlyWhitespaceText(condition.children)) {
|
|
96208
|
+
return { reason: "unsupported-response-processing" };
|
|
96209
|
+
}
|
|
96210
|
+
const conditionChildren = childElements(condition);
|
|
96211
|
+
if (conditionChildren.length !== 1 || conditionChildren[0]?.localName !== "qti-response-if") {
|
|
96212
|
+
return { reason: "unsupported-response-processing" };
|
|
96213
|
+
}
|
|
96214
|
+
const responseIf = conditionChildren[0];
|
|
96215
|
+
if (!onlyAttributes(responseIf, []) || !onlyWhitespaceText(responseIf.children)) {
|
|
96216
|
+
return { reason: "unsupported-response-processing" };
|
|
96217
|
+
}
|
|
96218
|
+
const responseIfChildren = childElements(responseIf);
|
|
96219
|
+
if (responseIfChildren.length !== 2) {
|
|
96220
|
+
return { reason: "unsupported-response-processing" };
|
|
96221
|
+
}
|
|
96222
|
+
const [comparison, setOutcome] = responseIfChildren;
|
|
96223
|
+
if (!comparison || !setOutcome || !["qti-match", "qti-equal", "qti-equal-rounded"].includes(comparison.localName) || setOutcome.localName !== "qti-set-outcome-value") {
|
|
96224
|
+
return { reason: "unsupported-response-processing" };
|
|
96225
|
+
}
|
|
96226
|
+
const responseIdentifier = [...references][0];
|
|
96227
|
+
const declaration = declarations.get(responseIdentifier);
|
|
96228
|
+
const points = additivePoints(setOutcome);
|
|
96229
|
+
const comparisonPayload = declaration ? comparisonResult(comparison, declaration) : null;
|
|
96230
|
+
if (!declaration || points === null || !comparisonPayload) {
|
|
96231
|
+
return { reason: "unsupported-response-processing" };
|
|
96232
|
+
}
|
|
96233
|
+
return {
|
|
96234
|
+
rule: {
|
|
96235
|
+
responseIdentifier,
|
|
96236
|
+
cardinality: declaration.cardinality,
|
|
96237
|
+
baseType: declaration.baseType,
|
|
96238
|
+
points,
|
|
96239
|
+
comparison: comparisonPayload
|
|
96240
|
+
}
|
|
96241
|
+
};
|
|
96242
|
+
}
|
|
96243
|
+
function additiveRules(processingRules, declarations) {
|
|
96244
|
+
const rules = [];
|
|
96245
|
+
const scored = new Set;
|
|
96246
|
+
for (const condition of processingRules) {
|
|
96247
|
+
const result = additiveRule(condition, declarations);
|
|
96248
|
+
if ("reason" in result) {
|
|
96249
|
+
return result;
|
|
96250
|
+
}
|
|
96251
|
+
if (scored.has(result.rule.responseIdentifier)) {
|
|
96252
|
+
return { reason: "unsupported-response-processing" };
|
|
96253
|
+
}
|
|
96254
|
+
scored.add(result.rule.responseIdentifier);
|
|
96255
|
+
rules.push(result.rule);
|
|
96256
|
+
}
|
|
96257
|
+
if (scored.size !== declarations.size || [...declarations.keys()].some((identifier) => !scored.has(identifier))) {
|
|
96258
|
+
return { reason: "unsupported-response-processing" };
|
|
96259
|
+
}
|
|
96260
|
+
return { rules };
|
|
96261
|
+
}
|
|
96262
|
+
function scoreDefaultIsZero(item) {
|
|
96263
|
+
const scoreOutcomes = childElements(item, "qti-outcome-declaration").filter((outcome) => outcome.attributes.identifier === "SCORE");
|
|
96264
|
+
if (scoreOutcomes.length !== 1) {
|
|
96265
|
+
return false;
|
|
96266
|
+
}
|
|
96267
|
+
const scoreOutcome = scoreOutcomes[0];
|
|
96268
|
+
if (!onlyAttributes(scoreOutcome, ["identifier", "cardinality", "base-type"]) || scoreOutcome.attributes.cardinality !== "single" || scoreOutcome.attributes["base-type"] !== "float") {
|
|
96269
|
+
return false;
|
|
96270
|
+
}
|
|
96271
|
+
const defaults = childElements(scoreOutcome, "qti-default-value");
|
|
96272
|
+
if (defaults.length !== 1) {
|
|
96273
|
+
return false;
|
|
96274
|
+
}
|
|
96275
|
+
const values = childElements(defaults[0], "qti-value");
|
|
96276
|
+
const defaultText = values.length === 1 ? directText(values[0]) : null;
|
|
96277
|
+
return defaultText !== null && defaultText !== "" && Number(defaultText) === 0;
|
|
96278
|
+
}
|
|
96279
|
+
function compileResponseProcessing(item, itemIdentifier, maxScore, declarations) {
|
|
96280
|
+
const processingElements = childElements(item, "qti-response-processing");
|
|
96281
|
+
if (processingElements.length === 0) {
|
|
96282
|
+
return platformResult(itemIdentifier, maxScore, [...declarations.values()].map((declaration) => matchRule(declaration, 1)));
|
|
96283
|
+
}
|
|
96284
|
+
if (processingElements.length !== 1) {
|
|
96285
|
+
return fallback(itemIdentifier, maxScore, "unsupported-response-processing");
|
|
96286
|
+
}
|
|
96287
|
+
const processing = processingElements[0];
|
|
96288
|
+
const template = processing.attributes.template;
|
|
96289
|
+
const processingRules = childElements(processing);
|
|
96290
|
+
if (template !== undefined) {
|
|
96291
|
+
if (!MATCH_CORRECT_TEMPLATES2.has(template.trim())) {
|
|
96292
|
+
return fallback(itemIdentifier, maxScore, "unsupported-processing-template");
|
|
96293
|
+
}
|
|
96294
|
+
if (processingRules.length > 0 || !onlyWhitespaceText(processing.children) || !onlyAttributes(processing, ["template"])) {
|
|
96295
|
+
return fallback(itemIdentifier, maxScore, "unsupported-response-processing");
|
|
96296
|
+
}
|
|
96297
|
+
const response = declarations.get("RESPONSE");
|
|
96298
|
+
return response && declarations.size === 1 ? platformResult(itemIdentifier, maxScore, [matchRule(response, 1)]) : fallback(itemIdentifier, maxScore, "unsupported-response-processing");
|
|
96299
|
+
}
|
|
96300
|
+
if (!onlyAttributes(processing, []) || !onlyWhitespaceText(processing.children) || processingRules.length === 0 || processingRules.some((rule) => rule.localName !== "qti-response-condition") || !scoreDefaultIsZero(item)) {
|
|
96301
|
+
return fallback(itemIdentifier, maxScore, "unsupported-response-processing");
|
|
96302
|
+
}
|
|
96303
|
+
const compiled = additiveRules(processingRules, declarations);
|
|
96304
|
+
return "reason" in compiled ? fallback(itemIdentifier, maxScore, compiled.reason) : platformResult(itemIdentifier, maxScore, compiled.rules);
|
|
96305
|
+
}
|
|
96306
|
+
function compileQtiScoringArtifactItem(rawXml) {
|
|
96307
|
+
let root;
|
|
96308
|
+
let parsed;
|
|
96309
|
+
try {
|
|
96310
|
+
root = parseXmlDocument(rawXml);
|
|
96311
|
+
parsed = parseQtiItemXml(rawXml);
|
|
96312
|
+
} catch {
|
|
96313
|
+
throw new QtiScoringArtifactValidationError("QTI assessment item contains invalid XML");
|
|
96314
|
+
}
|
|
96315
|
+
const item = itemElement(root);
|
|
96316
|
+
if (!item) {
|
|
96317
|
+
throw new QtiScoringArtifactValidationError("QTI XML is missing a qti-assessment-item document element");
|
|
96318
|
+
}
|
|
96319
|
+
const itemIdentifier = parsed.identifier.trim();
|
|
96320
|
+
if (!itemIdentifier) {
|
|
96321
|
+
throw new QtiScoringArtifactValidationError("QTI assessment item is missing an identifier");
|
|
96322
|
+
}
|
|
96323
|
+
const maxScore = qtiItemMaxScore(parsed);
|
|
96324
|
+
const rawDeclarations = childElements(item, "qti-response-declaration");
|
|
96325
|
+
const constructReason = declarationConstructReason(rawDeclarations);
|
|
96326
|
+
if (constructReason) {
|
|
96327
|
+
return fallback(itemIdentifier, maxScore, constructReason);
|
|
96328
|
+
}
|
|
96329
|
+
if (childElements(item, "qti-template-processing").length > 0 || childElements(item, "qti-template-declaration").length > 0) {
|
|
96330
|
+
return fallback(itemIdentifier, maxScore, "unsupported-response-processing");
|
|
96331
|
+
}
|
|
96332
|
+
const compiledDeclarations = scoringDeclarations(rawDeclarations, parsed.responseDeclarations);
|
|
96333
|
+
return "reason" in compiledDeclarations ? fallback(itemIdentifier, maxScore, compiledDeclarations.reason) : compileResponseProcessing(item, itemIdentifier, maxScore, compiledDeclarations.declarations);
|
|
96334
|
+
}
|
|
96335
|
+
function compileQtiScoringArtifact(questions) {
|
|
96336
|
+
const identifiers = new Set;
|
|
96337
|
+
const items = questions.questions.map(({ question, reference }) => {
|
|
96338
|
+
if (!question.rawXml) {
|
|
96339
|
+
throw new QtiScoringArtifactValidationError(`QTI question ${reference.identifier} is missing raw XML`);
|
|
96340
|
+
}
|
|
96341
|
+
const item = compileQtiScoringArtifactItem(question.rawXml);
|
|
96342
|
+
if (item.itemIdentifier !== question.identifier) {
|
|
96343
|
+
throw new QtiScoringArtifactValidationError(`Hydrated QTI question ${question.identifier} contains item ${item.itemIdentifier}`);
|
|
96344
|
+
}
|
|
96345
|
+
if (identifiers.has(item.itemIdentifier)) {
|
|
96346
|
+
throw new QtiScoringArtifactValidationError(`QTI questions contain duplicate item ${item.itemIdentifier}`);
|
|
96347
|
+
}
|
|
96348
|
+
identifiers.add(item.itemIdentifier);
|
|
96349
|
+
return item;
|
|
96350
|
+
});
|
|
96351
|
+
return { items };
|
|
96352
|
+
}
|
|
95683
96353
|
function isRecord22(value) {
|
|
95684
96354
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
95685
96355
|
}
|
|
@@ -95835,7 +96505,7 @@ function qtiStandardsFromMetadata(metadata2) {
|
|
|
95835
96505
|
standards.push(...qtiAlignmentStandardsFromMetadata2(metadata2));
|
|
95836
96506
|
return dedupeStandards2(standards);
|
|
95837
96507
|
}
|
|
95838
|
-
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;
|
|
96508
|
+
var EVENT_HANDLER_ATTRIBUTE, SCRIPT_SCHEMES2, DATA_RASTER_IMAGE_PATTERN2, QTI_MATHML_ALLOWED_TAGS2, metadataSymbol2, parser2, SUPPORTED_QTI_INTERACTION_TYPES2, URL_ATTRIBUTES3, IMAGE_URL_ATTRIBUTES3, INTERACTION_TYPES2, XML_NAMED_ENTITIES, EMPHASIS_TAGS2, STRONG_TAGS2, CONTENT_SKIPPED_TAGS2, BLOCK_CONTENT_KINDS2, GAP_MATCH_TOKEN_TAGS2, STAGE_GRAPHIC_EXCLUDED_CONTAINERS2, BLANK_SENTINEL2 = "", MARKED_BLANK_PATTERN2, MARKED_BLANK_GLOBAL_PATTERN2, REGION_SHAPE_COORDS, QTI_NAMESPACE = "http://www.imsglobal.org/xsd/imsqtiasi_v3p0", QTI_AUTHORING_BLANK_MARKER = "___", MATCH_CORRECT_TEMPLATE2 = "http://www.imsglobal.org/question/qti_v3p0/rptemplates/match_correct", MAP_RESPONSE_TEMPLATE2 = "https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/map_response", MAP_RESPONSE_TEMPLATES2, RESPONSE_PROCESSING_TEMPLATES2, GLOBAL_BODY_ATTRIBUTES, BODY_ELEMENT_ATTRIBUTES, MATHML_ATTRIBUTES, VOID_BODY_ELEMENTS2, URL_ATTRIBUTES22, IMAGE_URL_ATTRIBUTES22, INTERACTION_ATTRIBUTES, CHOICE_ATTRIBUTES, CHOICE_ELEMENT_ATTRIBUTES, VOID_CHOICE_ELEMENTS2, NUMERIC_COMPARISON_ATTRIBUTES2, RESPONSE_CARDINALITIES2, RESPONSE_BASE_TYPES2, BLANK_SENTINEL22 = "", PLAYABLE_POINT_VALUE, POINT_INTERACTION_TYPES2, INDEPENDENT_PROCESSING_TAGS2, SUPPORTED_BASE_TYPES2, SUPPORTED_CARDINALITIES2, MATCH_CORRECT_TEMPLATES2, DIRECTED_PAIR_VALUE, QtiScoringArtifactValidationError, COMMON_CORE_MATH_FRAMEWORK_ALIASES2, COMMON_CORE_ELA_FRAMEWORK_ALIASES2, COMMON_CORE_FRAMEWORK_ALIASES2;
|
|
95839
96509
|
var init_qti = __esm(() => {
|
|
95840
96510
|
init_timeback3();
|
|
95841
96511
|
init_timeback3();
|
|
@@ -96085,6 +96755,30 @@ var init_qti = __esm(() => {
|
|
|
96085
96755
|
"qti-sum",
|
|
96086
96756
|
"qti-base-value"
|
|
96087
96757
|
]);
|
|
96758
|
+
SUPPORTED_BASE_TYPES2 = new Set([
|
|
96759
|
+
"string",
|
|
96760
|
+
"identifier",
|
|
96761
|
+
"integer",
|
|
96762
|
+
"float",
|
|
96763
|
+
"directedPair"
|
|
96764
|
+
]);
|
|
96765
|
+
SUPPORTED_CARDINALITIES2 = new Set([
|
|
96766
|
+
"single",
|
|
96767
|
+
"multiple",
|
|
96768
|
+
"ordered"
|
|
96769
|
+
]);
|
|
96770
|
+
MATCH_CORRECT_TEMPLATES2 = new Set([
|
|
96771
|
+
"http://www.imsglobal.org/question/qti_v3p0/rptemplates/match_correct",
|
|
96772
|
+
"https://www.imsglobal.org/question/qti_v3p0/rptemplates/match_correct",
|
|
96773
|
+
"https://purl.imsglobal.org/spec/qti/v3p0/rptemplates/match_correct"
|
|
96774
|
+
].flatMap((template) => [template, `${template}.xml`]));
|
|
96775
|
+
DIRECTED_PAIR_VALUE = /^[A-Za-z_][\w.-]* [A-Za-z_][\w.-]*$/;
|
|
96776
|
+
QtiScoringArtifactValidationError = class QtiScoringArtifactValidationError extends Error {
|
|
96777
|
+
constructor(message) {
|
|
96778
|
+
super(message);
|
|
96779
|
+
this.name = "QtiScoringArtifactValidationError";
|
|
96780
|
+
}
|
|
96781
|
+
};
|
|
96088
96782
|
COMMON_CORE_MATH_FRAMEWORK_ALIASES2 = new Set([
|
|
96089
96783
|
"ccssm",
|
|
96090
96784
|
"ccssmath",
|
|
@@ -96110,11 +96804,11 @@ var init_qti = __esm(() => {
|
|
|
96110
96804
|
});
|
|
96111
96805
|
|
|
96112
96806
|
// ../api-core/src/utils/timeback-grade-level-results.util.ts
|
|
96113
|
-
function
|
|
96807
|
+
function isRecord7(value) {
|
|
96114
96808
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
96115
96809
|
}
|
|
96116
96810
|
function metadataOf(record3) {
|
|
96117
|
-
return
|
|
96811
|
+
return isRecord7(record3) && isRecord7(record3.metadata) ? record3.metadata : {};
|
|
96118
96812
|
}
|
|
96119
96813
|
function stringField3(value) {
|
|
96120
96814
|
return typeof value === "string" ? value : "";
|
|
@@ -96126,7 +96820,7 @@ function numberField(value) {
|
|
|
96126
96820
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
96127
96821
|
}
|
|
96128
96822
|
function sourceIdFromRef(value) {
|
|
96129
|
-
return
|
|
96823
|
+
return isRecord7(value) ? stringField3(value.sourcedId) : "";
|
|
96130
96824
|
}
|
|
96131
96825
|
function buildGradeLevelTestResultWhere(options) {
|
|
96132
96826
|
return {
|
|
@@ -96202,7 +96896,7 @@ function mapGradeLevelTestSummary(result, options) {
|
|
|
96202
96896
|
};
|
|
96203
96897
|
}
|
|
96204
96898
|
function readLearningObjectiveSets(record3) {
|
|
96205
|
-
if (!
|
|
96899
|
+
if (!isRecord7(record3)) {
|
|
96206
96900
|
return [];
|
|
96207
96901
|
}
|
|
96208
96902
|
const metadata2 = metadataOf(record3);
|
|
@@ -96232,7 +96926,7 @@ function dedupeStandards3(standards) {
|
|
|
96232
96926
|
function standardsFromLineItem(lineItem) {
|
|
96233
96927
|
const standards = [];
|
|
96234
96928
|
for (const set3 of readLearningObjectiveSets(lineItem)) {
|
|
96235
|
-
if (
|
|
96929
|
+
if (isRecord7(set3)) {
|
|
96236
96930
|
const source = stringField3(set3.source) || "OneRoster";
|
|
96237
96931
|
const ids = Array.isArray(set3.learningObjectiveIds) ? set3.learningObjectiveIds : [];
|
|
96238
96932
|
for (const id of ids) {
|
|
@@ -96248,11 +96942,11 @@ function standardsFromLineItem(lineItem) {
|
|
|
96248
96942
|
function standardsFromResult(result) {
|
|
96249
96943
|
const standards = [];
|
|
96250
96944
|
for (const set3 of readLearningObjectiveSets(result)) {
|
|
96251
|
-
if (
|
|
96945
|
+
if (isRecord7(set3)) {
|
|
96252
96946
|
const source = stringField3(set3.source) || "OneRoster";
|
|
96253
96947
|
const values = Array.isArray(set3.learningObjectiveResults) ? set3.learningObjectiveResults : [];
|
|
96254
96948
|
for (const item of values) {
|
|
96255
|
-
if (
|
|
96949
|
+
if (isRecord7(item)) {
|
|
96256
96950
|
const id = stringField3(item.learningObjectiveId).trim();
|
|
96257
96951
|
if (id) {
|
|
96258
96952
|
standards.push({ source, id });
|
|
@@ -96264,7 +96958,7 @@ function standardsFromResult(result) {
|
|
|
96264
96958
|
return dedupeStandards3(standards);
|
|
96265
96959
|
}
|
|
96266
96960
|
function standardsFromQtiQuestion(questionRef) {
|
|
96267
|
-
const question =
|
|
96961
|
+
const question = isRecord7(questionRef) && isRecord7(questionRef.question) ? questionRef.question : null;
|
|
96268
96962
|
return qtiStandardsFromMetadata(metadataOf(question));
|
|
96269
96963
|
}
|
|
96270
96964
|
function enrichStandardsFromQti(standards, qtiStandards) {
|
|
@@ -96401,8 +97095,8 @@ function mapGradeLevelReviewQuestions(options) {
|
|
|
96401
97095
|
return options.questionLineItems.toSorted((a, b) => naturalTitleSort.compare(a.title, b.title)).map((lineItem) => {
|
|
96402
97096
|
const result = options.latestResultsByLineItem.get(lineItem.sourcedId ?? "");
|
|
96403
97097
|
const resultMetadata = metadataOf(result);
|
|
96404
|
-
const attemptedQuestionData =
|
|
96405
|
-
const attemptedResult =
|
|
97098
|
+
const attemptedQuestionData = isRecord7(resultMetadata.attemptedQuestionData) ? resultMetadata.attemptedQuestionData : null;
|
|
97099
|
+
const attemptedResult = isRecord7(attemptedQuestionData?.result) ? attemptedQuestionData.result : null;
|
|
96406
97100
|
const index2 = parseQuestionIndex(lineItem.title);
|
|
96407
97101
|
const qtiQuestion = index2 ? qtiQuestionByIndex.get(index2) ?? options.qtiQuestions[index2 - 1] : undefined;
|
|
96408
97102
|
const qtiStandards = standardsFromQtiQuestion(qtiQuestion);
|
|
@@ -98445,6 +99139,452 @@ var init_timeback_admin_service = __esm(async () => {
|
|
|
98445
99139
|
]);
|
|
98446
99140
|
});
|
|
98447
99141
|
|
|
99142
|
+
// ../api-core/src/utils/assessment-artifact-runtime.util.ts
|
|
99143
|
+
function normalizedKey(key) {
|
|
99144
|
+
return key.replaceAll(/[^a-z0-9]/gi, "").toLowerCase();
|
|
99145
|
+
}
|
|
99146
|
+
function containsPrivatePlayableKey(value) {
|
|
99147
|
+
if (Array.isArray(value)) {
|
|
99148
|
+
return value.some(containsPrivatePlayableKey);
|
|
99149
|
+
}
|
|
99150
|
+
if (!isRecord(value)) {
|
|
99151
|
+
return false;
|
|
99152
|
+
}
|
|
99153
|
+
return Object.entries(value).some(([key, entry]) => PRIVATE_PLAYABLE_KEYS.has(normalizedKey(key)) || containsPrivatePlayableKey(entry));
|
|
99154
|
+
}
|
|
99155
|
+
function playableSchema(identity) {
|
|
99156
|
+
return exports_external.object({
|
|
99157
|
+
contractVersion: exports_external.literal(PLAYCADEMY_ASSESSMENT_CONTRACT_VERSION),
|
|
99158
|
+
identifier: exports_external.literal(identity.qtiTestIdentifier),
|
|
99159
|
+
contentRevision: exports_external.literal(identity.kind === "assessment" ? identity.contentRevision : identity.sourceContentRevision),
|
|
99160
|
+
title: trimmedNonEmptyString,
|
|
99161
|
+
items: exports_external.array(playableItemSchema)
|
|
99162
|
+
}).strict();
|
|
99163
|
+
}
|
|
99164
|
+
function scoringSchema(identity) {
|
|
99165
|
+
if (identity.kind === "assessment") {
|
|
99166
|
+
return exports_external.object({
|
|
99167
|
+
items: uniqueScoringItemsSchema,
|
|
99168
|
+
reviewBank: exports_external.undefined(),
|
|
99169
|
+
diagnostic: diagnosticContextSchema.optional()
|
|
99170
|
+
});
|
|
99171
|
+
}
|
|
99172
|
+
return exports_external.object({
|
|
99173
|
+
items: uniqueScoringItemsSchema,
|
|
99174
|
+
reviewBank: exports_external.object({
|
|
99175
|
+
sourceContentRevision: exports_external.literal(identity.sourceContentRevision),
|
|
99176
|
+
bankRevision: exports_external.literal(identity.bankRevision),
|
|
99177
|
+
items: exports_external.array(exports_external.object({
|
|
99178
|
+
itemIdentifier: trimmedNonEmptyString,
|
|
99179
|
+
standards: exports_external.array(exports_external.unknown())
|
|
99180
|
+
}))
|
|
99181
|
+
}),
|
|
99182
|
+
diagnostic: exports_external.undefined()
|
|
99183
|
+
}).refine((payload) => payload.reviewBank.items.length === payload.items.length && payload.reviewBank.items.every((item, index2) => item.itemIdentifier === payload.items[index2]?.itemIdentifier));
|
|
99184
|
+
}
|
|
99185
|
+
function assertPlayableAssessmentArtifact(payload, identity) {
|
|
99186
|
+
if (!playableSchema(identity).safeParse(payload).success || containsPrivatePlayableKey(payload)) {
|
|
99187
|
+
throw new ValidationError("Playable assessment artifact payload is invalid or private");
|
|
99188
|
+
}
|
|
99189
|
+
}
|
|
99190
|
+
function assertScoringAssessmentArtifact(payload, identity) {
|
|
99191
|
+
if (!scoringSchema(identity).safeParse(payload).success) {
|
|
99192
|
+
throw new ValidationError("Scoring assessment artifact payload is invalid or incompatible");
|
|
99193
|
+
}
|
|
99194
|
+
}
|
|
99195
|
+
function assertScoringArtifactMatchesPlayable(assessment, scoring, identity) {
|
|
99196
|
+
const scoringByIdentifier = new Map(scoring.items.map((item) => [item.itemIdentifier, item.maxScore]));
|
|
99197
|
+
const playableMatches = assessment.items.every((item) => scoringByIdentifier.get(item.identifier) === item.maxScore);
|
|
99198
|
+
const hasExactManagedMembership = identity.kind === "review-bank" || scoring.items.length === assessment.items.length;
|
|
99199
|
+
if (!playableMatches || !hasExactManagedMembership) {
|
|
99200
|
+
throw new ValidationError("Scoring artifact does not match the playable assessment");
|
|
99201
|
+
}
|
|
99202
|
+
}
|
|
99203
|
+
function assertPlayableArtifactContainsItems(assessment, itemIdentifiers) {
|
|
99204
|
+
const available = new Set(assessment.items.map((item) => item.identifier));
|
|
99205
|
+
const missing = [...new Set(itemIdentifiers)].filter((identifier) => !available.has(identifier));
|
|
99206
|
+
if (missing.length > 0) {
|
|
99207
|
+
throw new ValidationError("Playable assessment artifact is missing required items", {
|
|
99208
|
+
missingItemIdentifiers: missing
|
|
99209
|
+
});
|
|
99210
|
+
}
|
|
99211
|
+
}
|
|
99212
|
+
function assessmentArtifactIdentityFromAttempt(metadata2) {
|
|
99213
|
+
return metadata2.purpose === "review" ? {
|
|
99214
|
+
kind: "review-bank",
|
|
99215
|
+
qtiTestIdentifier: metadata2.selectedTest.identifier,
|
|
99216
|
+
sourceContentRevision: metadata2.selectedTest.contentRevision,
|
|
99217
|
+
bankRevision: metadata2.review.bankRevision
|
|
99218
|
+
} : {
|
|
99219
|
+
kind: "assessment",
|
|
99220
|
+
qtiTestIdentifier: metadata2.selectedTest.identifier,
|
|
99221
|
+
contentRevision: metadata2.selectedTest.contentRevision
|
|
99222
|
+
};
|
|
99223
|
+
}
|
|
99224
|
+
function pinnedReviewBankFromAttempt(metadata2, bankIdentifier) {
|
|
99225
|
+
return {
|
|
99226
|
+
bankIdentifier,
|
|
99227
|
+
sourceContentRevision: metadata2.selectedTest.contentRevision,
|
|
99228
|
+
bankRevision: metadata2.review.bankRevision,
|
|
99229
|
+
items: metadata2.review.selections.map((selection) => ({
|
|
99230
|
+
itemIdentifier: selection.itemIdentifier,
|
|
99231
|
+
standards: [selection.standard]
|
|
99232
|
+
}))
|
|
99233
|
+
};
|
|
99234
|
+
}
|
|
99235
|
+
var PRIVATE_PLAYABLE_KEYS, trimmedNonEmptyString, finiteNumber2, nonnegativeInteger, playableContentNodesSchema, playableChoiceSchema, playableMatchChoiceSchema, playableGraphicSchema, playableHotspotSchema, playableGapTextSchema, playableGapImageSchema, playableInteractionBase, playableInteractionSchema, playableItemSchema, scoringRuleSchema, scoringItemSchema, uniqueScoringItemsSchema, diagnosticContextSchema;
|
|
99236
|
+
var init_assessment_artifact_runtime_util = __esm(() => {
|
|
99237
|
+
init_esm();
|
|
99238
|
+
init_src();
|
|
99239
|
+
init_errors2();
|
|
99240
|
+
PRIVATE_PLAYABLE_KEYS = new Set([
|
|
99241
|
+
"answer",
|
|
99242
|
+
"answerkey",
|
|
99243
|
+
"correctanswer",
|
|
99244
|
+
"correctresponse",
|
|
99245
|
+
"correctresponses",
|
|
99246
|
+
"correct",
|
|
99247
|
+
"correctvalue",
|
|
99248
|
+
"correctvalues",
|
|
99249
|
+
"iscorrect",
|
|
99250
|
+
"responseprocessing",
|
|
99251
|
+
"rule",
|
|
99252
|
+
"rules",
|
|
99253
|
+
"scoring",
|
|
99254
|
+
"scoringrules",
|
|
99255
|
+
"solution",
|
|
99256
|
+
"expected",
|
|
99257
|
+
"expectedanswer",
|
|
99258
|
+
"expectedvalue",
|
|
99259
|
+
"expectedvalues"
|
|
99260
|
+
]);
|
|
99261
|
+
trimmedNonEmptyString = exports_external.string().min(1).refine((value) => value === value.trim());
|
|
99262
|
+
finiteNumber2 = exports_external.number().finite();
|
|
99263
|
+
nonnegativeInteger = exports_external.number().int().nonnegative();
|
|
99264
|
+
playableContentNodesSchema = exports_external.array(exports_external.unknown());
|
|
99265
|
+
playableChoiceSchema = exports_external.object({
|
|
99266
|
+
identifier: trimmedNonEmptyString,
|
|
99267
|
+
content: exports_external.string(),
|
|
99268
|
+
contentNodes: playableContentNodesSchema.optional()
|
|
99269
|
+
}).strict();
|
|
99270
|
+
playableMatchChoiceSchema = exports_external.object({
|
|
99271
|
+
identifier: trimmedNonEmptyString,
|
|
99272
|
+
content: exports_external.string(),
|
|
99273
|
+
contentNodes: playableContentNodesSchema.optional(),
|
|
99274
|
+
matchMax: nonnegativeInteger
|
|
99275
|
+
}).strict();
|
|
99276
|
+
playableGraphicSchema = exports_external.object({
|
|
99277
|
+
src: trimmedNonEmptyString,
|
|
99278
|
+
width: finiteNumber2.optional(),
|
|
99279
|
+
height: finiteNumber2.optional(),
|
|
99280
|
+
description: exports_external.string().optional()
|
|
99281
|
+
}).strict();
|
|
99282
|
+
playableHotspotSchema = exports_external.object({
|
|
99283
|
+
identifier: trimmedNonEmptyString,
|
|
99284
|
+
shape: exports_external.enum(["circle", "ellipse", "rect", "poly", "default"]),
|
|
99285
|
+
coords: exports_external.array(finiteNumber2),
|
|
99286
|
+
matchMax: nonnegativeInteger.optional(),
|
|
99287
|
+
description: exports_external.string().optional()
|
|
99288
|
+
}).strict();
|
|
99289
|
+
playableGapTextSchema = exports_external.object({
|
|
99290
|
+
identifier: trimmedNonEmptyString,
|
|
99291
|
+
content: exports_external.string(),
|
|
99292
|
+
matchMax: nonnegativeInteger
|
|
99293
|
+
}).strict();
|
|
99294
|
+
playableGapImageSchema = exports_external.object({
|
|
99295
|
+
identifier: trimmedNonEmptyString,
|
|
99296
|
+
image: playableGraphicSchema.optional(),
|
|
99297
|
+
matchMax: nonnegativeInteger,
|
|
99298
|
+
description: exports_external.string().optional()
|
|
99299
|
+
}).strict();
|
|
99300
|
+
playableInteractionBase = {
|
|
99301
|
+
responseIdentifier: trimmedNonEmptyString,
|
|
99302
|
+
prompt: exports_external.string().optional()
|
|
99303
|
+
};
|
|
99304
|
+
playableInteractionSchema = exports_external.discriminatedUnion("type", [
|
|
99305
|
+
exports_external.object({
|
|
99306
|
+
...playableInteractionBase,
|
|
99307
|
+
type: exports_external.literal("choice"),
|
|
99308
|
+
choices: exports_external.array(playableChoiceSchema),
|
|
99309
|
+
minChoices: nonnegativeInteger,
|
|
99310
|
+
maxChoices: nonnegativeInteger
|
|
99311
|
+
}).strict(),
|
|
99312
|
+
exports_external.object({
|
|
99313
|
+
...playableInteractionBase,
|
|
99314
|
+
type: exports_external.literal("inline-choice"),
|
|
99315
|
+
choices: exports_external.array(playableChoiceSchema)
|
|
99316
|
+
}).strict(),
|
|
99317
|
+
exports_external.object({
|
|
99318
|
+
...playableInteractionBase,
|
|
99319
|
+
type: exports_external.literal("text-entry"),
|
|
99320
|
+
expectedLength: nonnegativeInteger.optional(),
|
|
99321
|
+
placeholder: exports_external.string().optional()
|
|
99322
|
+
}).strict(),
|
|
99323
|
+
exports_external.object({
|
|
99324
|
+
...playableInteractionBase,
|
|
99325
|
+
type: exports_external.literal("order"),
|
|
99326
|
+
choices: exports_external.array(playableChoiceSchema),
|
|
99327
|
+
shuffle: exports_external.boolean()
|
|
99328
|
+
}).strict(),
|
|
99329
|
+
exports_external.object({
|
|
99330
|
+
...playableInteractionBase,
|
|
99331
|
+
type: exports_external.literal("match"),
|
|
99332
|
+
sourceChoices: exports_external.array(playableMatchChoiceSchema),
|
|
99333
|
+
targetChoices: exports_external.array(playableMatchChoiceSchema),
|
|
99334
|
+
maxAssociations: nonnegativeInteger,
|
|
99335
|
+
shuffle: exports_external.boolean()
|
|
99336
|
+
}).strict(),
|
|
99337
|
+
exports_external.object({
|
|
99338
|
+
...playableInteractionBase,
|
|
99339
|
+
type: exports_external.literal("hottext"),
|
|
99340
|
+
segments: exports_external.array(exports_external.object({
|
|
99341
|
+
identifier: trimmedNonEmptyString.optional(),
|
|
99342
|
+
content: exports_external.string(),
|
|
99343
|
+
selectable: exports_external.boolean()
|
|
99344
|
+
}).strict()),
|
|
99345
|
+
maxChoices: nonnegativeInteger
|
|
99346
|
+
}).strict(),
|
|
99347
|
+
exports_external.object({
|
|
99348
|
+
...playableInteractionBase,
|
|
99349
|
+
type: exports_external.literal("hotspot"),
|
|
99350
|
+
image: playableGraphicSchema,
|
|
99351
|
+
hotspots: exports_external.array(playableHotspotSchema),
|
|
99352
|
+
minChoices: nonnegativeInteger,
|
|
99353
|
+
maxChoices: nonnegativeInteger
|
|
99354
|
+
}).strict(),
|
|
99355
|
+
exports_external.object({
|
|
99356
|
+
...playableInteractionBase,
|
|
99357
|
+
type: exports_external.literal("gap-match"),
|
|
99358
|
+
content: exports_external.string(),
|
|
99359
|
+
contentNodes: playableContentNodesSchema,
|
|
99360
|
+
gapTexts: exports_external.array(playableGapTextSchema),
|
|
99361
|
+
gaps: exports_external.array(trimmedNonEmptyString),
|
|
99362
|
+
maxAssociations: nonnegativeInteger
|
|
99363
|
+
}).strict(),
|
|
99364
|
+
exports_external.object({
|
|
99365
|
+
...playableInteractionBase,
|
|
99366
|
+
type: exports_external.literal("graphic-gap-match"),
|
|
99367
|
+
image: playableGraphicSchema,
|
|
99368
|
+
gapImages: exports_external.array(playableGapImageSchema),
|
|
99369
|
+
hotspots: exports_external.array(playableHotspotSchema),
|
|
99370
|
+
maxAssociations: nonnegativeInteger
|
|
99371
|
+
}).strict(),
|
|
99372
|
+
exports_external.object({
|
|
99373
|
+
...playableInteractionBase,
|
|
99374
|
+
type: exports_external.literal("select-point"),
|
|
99375
|
+
image: playableGraphicSchema,
|
|
99376
|
+
minChoices: nonnegativeInteger,
|
|
99377
|
+
maxChoices: nonnegativeInteger
|
|
99378
|
+
}).strict(),
|
|
99379
|
+
exports_external.object({
|
|
99380
|
+
...playableInteractionBase,
|
|
99381
|
+
type: exports_external.literal("graphic-associate"),
|
|
99382
|
+
image: playableGraphicSchema,
|
|
99383
|
+
hotspots: exports_external.array(playableHotspotSchema),
|
|
99384
|
+
maxAssociations: nonnegativeInteger
|
|
99385
|
+
}).strict(),
|
|
99386
|
+
exports_external.object({
|
|
99387
|
+
...playableInteractionBase,
|
|
99388
|
+
type: exports_external.literal("position-object"),
|
|
99389
|
+
stage: playableGraphicSchema,
|
|
99390
|
+
object: playableGraphicSchema,
|
|
99391
|
+
maxPlacements: nonnegativeInteger
|
|
99392
|
+
}).strict()
|
|
99393
|
+
]);
|
|
99394
|
+
playableItemSchema = exports_external.object({
|
|
99395
|
+
identifier: trimmedNonEmptyString,
|
|
99396
|
+
title: exports_external.string(),
|
|
99397
|
+
prompt: exports_external.string(),
|
|
99398
|
+
promptContent: playableContentNodesSchema.optional(),
|
|
99399
|
+
maxScore: finiteNumber2,
|
|
99400
|
+
interactions: exports_external.array(playableInteractionSchema)
|
|
99401
|
+
}).strict();
|
|
99402
|
+
scoringRuleSchema = exports_external.object({
|
|
99403
|
+
responseIdentifier: trimmedNonEmptyString,
|
|
99404
|
+
cardinality: exports_external.enum(["single", "multiple", "ordered"]),
|
|
99405
|
+
baseType: exports_external.enum(["string", "identifier", "integer", "float", "directedPair"]),
|
|
99406
|
+
points: finiteNumber2,
|
|
99407
|
+
comparison: exports_external.discriminatedUnion("kind", [
|
|
99408
|
+
exports_external.object({ kind: exports_external.literal("match"), correctValues: exports_external.array(exports_external.string()) }),
|
|
99409
|
+
exports_external.object({ kind: exports_external.literal("numeric-equal"), correctValue: finiteNumber2 }),
|
|
99410
|
+
exports_external.object({
|
|
99411
|
+
kind: exports_external.literal("numeric-equal-rounded"),
|
|
99412
|
+
correctValue: finiteNumber2,
|
|
99413
|
+
roundingMode: exports_external.enum(["decimalPlaces", "significantFigures"]),
|
|
99414
|
+
figures: exports_external.number().int().nonnegative()
|
|
99415
|
+
})
|
|
99416
|
+
])
|
|
99417
|
+
});
|
|
99418
|
+
scoringItemSchema = exports_external.discriminatedUnion("strategy", [
|
|
99419
|
+
exports_external.object({
|
|
99420
|
+
strategy: exports_external.literal("qti"),
|
|
99421
|
+
itemIdentifier: trimmedNonEmptyString,
|
|
99422
|
+
maxScore: finiteNumber2,
|
|
99423
|
+
reason: exports_external.enum([
|
|
99424
|
+
"response-mapping",
|
|
99425
|
+
"area-mapping",
|
|
99426
|
+
"unsupported-base-type",
|
|
99427
|
+
"unsupported-cardinality",
|
|
99428
|
+
"invalid-correct-response",
|
|
99429
|
+
"no-scorable-response",
|
|
99430
|
+
"unsupported-processing-template",
|
|
99431
|
+
"unsupported-response-processing",
|
|
99432
|
+
"cross-response-processing",
|
|
99433
|
+
"inconsistent-max-score"
|
|
99434
|
+
])
|
|
99435
|
+
}),
|
|
99436
|
+
exports_external.object({
|
|
99437
|
+
strategy: exports_external.literal("platform"),
|
|
99438
|
+
itemIdentifier: trimmedNonEmptyString,
|
|
99439
|
+
maxScore: finiteNumber2,
|
|
99440
|
+
rules: exports_external.array(scoringRuleSchema)
|
|
99441
|
+
})
|
|
99442
|
+
]);
|
|
99443
|
+
uniqueScoringItemsSchema = exports_external.array(scoringItemSchema).refine((items) => new Set(items.map((item) => item.itemIdentifier)).size === items.length);
|
|
99444
|
+
diagnosticContextSchema = exports_external.object({
|
|
99445
|
+
routingRevision: trimmedNonEmptyString,
|
|
99446
|
+
routingManifest: exports_external.record(exports_external.unknown())
|
|
99447
|
+
});
|
|
99448
|
+
});
|
|
99449
|
+
|
|
99450
|
+
// ../api-core/src/utils/assessment-artifact-loader.util.ts
|
|
99451
|
+
class AssessmentArtifactRuntimeLoader {
|
|
99452
|
+
artifacts;
|
|
99453
|
+
playableCache;
|
|
99454
|
+
scoringCache;
|
|
99455
|
+
playableLoads = new Map;
|
|
99456
|
+
scoringLoads = new Map;
|
|
99457
|
+
constructor(artifacts, options) {
|
|
99458
|
+
this.artifacts = artifacts;
|
|
99459
|
+
this.playableCache = this.immutableCache(options.maxSize);
|
|
99460
|
+
this.scoringCache = this.immutableCache(options.maxSize);
|
|
99461
|
+
}
|
|
99462
|
+
loadPlayable(identity) {
|
|
99463
|
+
return this.load({
|
|
99464
|
+
artifactKind: "playable",
|
|
99465
|
+
identity,
|
|
99466
|
+
key: this.cacheKey(identity),
|
|
99467
|
+
cache: this.playableCache,
|
|
99468
|
+
loads: this.playableLoads,
|
|
99469
|
+
load: () => this.artifacts.loadPlayable(identity),
|
|
99470
|
+
assert: assertPlayableAssessmentArtifact
|
|
99471
|
+
});
|
|
99472
|
+
}
|
|
99473
|
+
async loadPlayableContainingItems(identity, itemIdentifiers) {
|
|
99474
|
+
const startedAt = Date.now();
|
|
99475
|
+
const assessment = await this.loadPlayable(identity);
|
|
99476
|
+
if (assessment) {
|
|
99477
|
+
try {
|
|
99478
|
+
assertPlayableArtifactContainsItems(assessment, itemIdentifiers);
|
|
99479
|
+
return assessment;
|
|
99480
|
+
} catch (error88) {
|
|
99481
|
+
this.recordLoad("playable", identity.kind, "validation_failure", startedAt, error88);
|
|
99482
|
+
}
|
|
99483
|
+
}
|
|
99484
|
+
this.recordFallback("playable", identity.kind);
|
|
99485
|
+
return null;
|
|
99486
|
+
}
|
|
99487
|
+
loadManagedPlayable(qtiTestIdentifier) {
|
|
99488
|
+
return this.load({
|
|
99489
|
+
artifactKind: "playable",
|
|
99490
|
+
key: `managed\x00${qtiTestIdentifier}`,
|
|
99491
|
+
cache: this.playableCache,
|
|
99492
|
+
loads: this.playableLoads,
|
|
99493
|
+
load: () => this.artifacts.loadManagedPlayable(qtiTestIdentifier),
|
|
99494
|
+
assert: assertPlayableAssessmentArtifact
|
|
99495
|
+
});
|
|
99496
|
+
}
|
|
99497
|
+
loadScoring(identity) {
|
|
99498
|
+
return this.load({
|
|
99499
|
+
artifactKind: "scoring",
|
|
99500
|
+
identity,
|
|
99501
|
+
key: this.cacheKey(identity),
|
|
99502
|
+
cache: this.scoringCache,
|
|
99503
|
+
loads: this.scoringLoads,
|
|
99504
|
+
load: () => this.artifacts.loadScoring(identity),
|
|
99505
|
+
assert: assertScoringAssessmentArtifact
|
|
99506
|
+
});
|
|
99507
|
+
}
|
|
99508
|
+
async loadMatchedScoring(assessment, identity) {
|
|
99509
|
+
const scoring = await this.loadScoring(identity);
|
|
99510
|
+
if (scoring) {
|
|
99511
|
+
try {
|
|
99512
|
+
assertScoringArtifactMatchesPlayable(assessment, scoring, identity);
|
|
99513
|
+
return scoring;
|
|
99514
|
+
} catch (error88) {
|
|
99515
|
+
this.recordLoad("scoring", identity.kind, "validation_failure", Date.now(), error88);
|
|
99516
|
+
}
|
|
99517
|
+
}
|
|
99518
|
+
this.recordFallback("scoring", identity.kind);
|
|
99519
|
+
return null;
|
|
99520
|
+
}
|
|
99521
|
+
recordFallback(artifactKind, identityKind) {
|
|
99522
|
+
addEvent("assessment.artifact_fallback", {
|
|
99523
|
+
"app.assessment.artifact_kind": artifactKind,
|
|
99524
|
+
"app.assessment.artifact_identity_kind": identityKind
|
|
99525
|
+
});
|
|
99526
|
+
}
|
|
99527
|
+
immutableCache(maxSize) {
|
|
99528
|
+
return new TimebackCache({ defaultTTL: Number.POSITIVE_INFINITY, maxSize });
|
|
99529
|
+
}
|
|
99530
|
+
cacheKey(identity) {
|
|
99531
|
+
return canonicalJson2(identity);
|
|
99532
|
+
}
|
|
99533
|
+
recordLoad(artifactKind, identityKind, outcome, startedAt, error88) {
|
|
99534
|
+
addEvent("assessment.artifact_load", {
|
|
99535
|
+
"app.assessment.artifact_kind": artifactKind,
|
|
99536
|
+
"app.assessment.artifact_identity_kind": identityKind,
|
|
99537
|
+
"app.assessment.artifact_outcome": outcome,
|
|
99538
|
+
"app.assessment.artifact_duration_ms": Date.now() - startedAt,
|
|
99539
|
+
...error88 ? { "exception.type": errorType(error88) } : {}
|
|
99540
|
+
});
|
|
99541
|
+
}
|
|
99542
|
+
async load(input) {
|
|
99543
|
+
const startedAt = Date.now();
|
|
99544
|
+
const identityKind = input.identity?.kind ?? "assessment";
|
|
99545
|
+
const record3 = (outcome, error88) => this.recordLoad(input.artifactKind, identityKind, outcome, startedAt, error88);
|
|
99546
|
+
const cached3 = input.cache.get(input.key);
|
|
99547
|
+
if (cached3) {
|
|
99548
|
+
record3("cache_hit");
|
|
99549
|
+
return cached3;
|
|
99550
|
+
}
|
|
99551
|
+
const inFlight = input.loads.get(input.key);
|
|
99552
|
+
if (inFlight) {
|
|
99553
|
+
record3("coalesced");
|
|
99554
|
+
return inFlight;
|
|
99555
|
+
}
|
|
99556
|
+
const pending = (async () => {
|
|
99557
|
+
try {
|
|
99558
|
+
const stored = await input.load();
|
|
99559
|
+
if (!stored) {
|
|
99560
|
+
record3("missing");
|
|
99561
|
+
return null;
|
|
99562
|
+
}
|
|
99563
|
+
input.assert(stored.payload, input.identity ?? stored.identity);
|
|
99564
|
+
input.cache.set(input.key, stored.payload);
|
|
99565
|
+
if (!input.identity) {
|
|
99566
|
+
input.cache.set(this.cacheKey(stored.identity), stored.payload);
|
|
99567
|
+
}
|
|
99568
|
+
record3("loaded");
|
|
99569
|
+
return stored.payload;
|
|
99570
|
+
} catch (error88) {
|
|
99571
|
+
record3(error88 instanceof ValidationError ? "validation_failure" : "load_failure", error88);
|
|
99572
|
+
return null;
|
|
99573
|
+
} finally {
|
|
99574
|
+
input.loads.delete(input.key);
|
|
99575
|
+
}
|
|
99576
|
+
})();
|
|
99577
|
+
input.loads.set(input.key, pending);
|
|
99578
|
+
return pending;
|
|
99579
|
+
}
|
|
99580
|
+
}
|
|
99581
|
+
var init_assessment_artifact_loader_util = __esm(async () => {
|
|
99582
|
+
init_spans();
|
|
99583
|
+
init_errors2();
|
|
99584
|
+
init_assessment_artifact_runtime_util();
|
|
99585
|
+
await init_dist5();
|
|
99586
|
+
});
|
|
99587
|
+
|
|
98448
99588
|
// ../data/src/locks.ts
|
|
98449
99589
|
function createInMemoryDatabaseLock() {
|
|
98450
99590
|
const tails = new Map;
|
|
@@ -98565,6 +99705,810 @@ var init_assessment_runtime_lock_util = __esm(() => {
|
|
|
98565
99705
|
};
|
|
98566
99706
|
});
|
|
98567
99707
|
|
|
99708
|
+
// ../api-core/src/utils/timeback-assessment-runtime.util.ts
|
|
99709
|
+
function assessmentPreparationContentionError(expectedResponseVersion, responseVersion, kind) {
|
|
99710
|
+
if (expectedResponseVersion === responseVersion) {
|
|
99711
|
+
return new ServiceUnavailableError(`The ${kind} response state changed repeatedly while this item was being prepared. Try again shortly.`, {
|
|
99712
|
+
retryable: true,
|
|
99713
|
+
reason: "PREPARATION_CONTENTION",
|
|
99714
|
+
expectedResponseVersion,
|
|
99715
|
+
responseVersion
|
|
99716
|
+
});
|
|
99717
|
+
}
|
|
99718
|
+
return AssessmentRuntimeError.from(assessmentResponseVersionConflict(expectedResponseVersion, responseVersion));
|
|
99719
|
+
}
|
|
99720
|
+
function assessmentTrackableCourse(integration) {
|
|
99721
|
+
if (!isTimebackGrade2(integration.grade) || !isTimebackSubject(integration.subject)) {
|
|
99722
|
+
return null;
|
|
99723
|
+
}
|
|
99724
|
+
return { grade: integration.grade, subject: integration.subject };
|
|
99725
|
+
}
|
|
99726
|
+
function buildAssessmentActivityData(input) {
|
|
99727
|
+
if (!input.course) {
|
|
99728
|
+
return;
|
|
99729
|
+
}
|
|
99730
|
+
return {
|
|
99731
|
+
activityId: input.activityId,
|
|
99732
|
+
activityName: input.activityName,
|
|
99733
|
+
...input.course,
|
|
99734
|
+
courseId: input.courseId
|
|
99735
|
+
};
|
|
99736
|
+
}
|
|
99737
|
+
function buildDiagnosticAssessmentSnapshot(input) {
|
|
99738
|
+
return {
|
|
99739
|
+
attemptId: input.attemptId,
|
|
99740
|
+
responseVersion: input.metadata.responseVersion,
|
|
99741
|
+
status: "in_progress",
|
|
99742
|
+
flow: "platform-routed-item-submit",
|
|
99743
|
+
...input.activityData ? { activityData: input.activityData } : {},
|
|
99744
|
+
assessment: assessmentPresentationForAttempt(input.assessment, input.attemptId),
|
|
99745
|
+
selection: {
|
|
99746
|
+
kind: "platform-routed-diagnostic",
|
|
99747
|
+
purpose: "diagnostic",
|
|
99748
|
+
definitionId: input.metadata.diagnostic.definitionId,
|
|
99749
|
+
diagnosticKey: input.metadata.diagnostic.diagnosticKey,
|
|
99750
|
+
routingRevision: input.metadata.diagnostic.routingRevision
|
|
99751
|
+
},
|
|
99752
|
+
routing: projectDiagnosticRoutingSnapshot(input.metadata.diagnostic.routingRevision, input.state),
|
|
99753
|
+
completion: null
|
|
99754
|
+
};
|
|
99755
|
+
}
|
|
99756
|
+
function buildConventionalAssessmentSnapshot(input) {
|
|
99757
|
+
const metadata2 = input.metadata;
|
|
99758
|
+
let selection;
|
|
99759
|
+
if (metadata2.purpose === "review") {
|
|
99760
|
+
selection = {
|
|
99761
|
+
kind: "standards-review",
|
|
99762
|
+
purpose: metadata2.purpose,
|
|
99763
|
+
standards: [...metadata2.review.standards],
|
|
99764
|
+
candidateItemsPerStandard: metadata2.review.candidateItemsPerStandard,
|
|
99765
|
+
selections: [...metadata2.review.selections],
|
|
99766
|
+
fulfillment: metadata2.review.fulfillment
|
|
99767
|
+
};
|
|
99768
|
+
} else if (metadata2.purpose === "mastery") {
|
|
99769
|
+
selection = {
|
|
99770
|
+
kind: "standard-quiz",
|
|
99771
|
+
purpose: metadata2.purpose,
|
|
99772
|
+
standard: metadata2.mastery.standard
|
|
99773
|
+
};
|
|
99774
|
+
} else {
|
|
99775
|
+
selection = { kind: "fixed-test", purpose: metadata2.purpose };
|
|
99776
|
+
}
|
|
99777
|
+
return {
|
|
99778
|
+
attemptId: input.attemptId,
|
|
99779
|
+
responseVersion: metadata2.responseVersion,
|
|
99780
|
+
status: "in_progress",
|
|
99781
|
+
flow: assessmentFlowForPurpose(metadata2.purpose),
|
|
99782
|
+
...input.activityData ? { activityData: input.activityData } : {},
|
|
99783
|
+
assessment: assessmentPresentationForAttempt(input.assessment, input.attemptId),
|
|
99784
|
+
responses: metadata2.responses,
|
|
99785
|
+
itemSubmissions: metadata2.itemSubmissions,
|
|
99786
|
+
score: null,
|
|
99787
|
+
selection
|
|
99788
|
+
};
|
|
99789
|
+
}
|
|
99790
|
+
function buildAssessmentAwardRecord(input, mastery) {
|
|
99791
|
+
return {
|
|
99792
|
+
xpAwarded: input.xpAwarded,
|
|
99793
|
+
...input.masteredUnits !== undefined ? { masteredUnits: input.masteredUnits } : {},
|
|
99794
|
+
...input.masteredUnitsAbsolute !== undefined ? { masteredUnitsAbsolute: input.masteredUnitsAbsolute } : {},
|
|
99795
|
+
masteredUnitsApplied: mastery.masteredUnitsApplied,
|
|
99796
|
+
...mastery.pctCompleteApp !== undefined ? { pctCompleteApp: mastery.pctCompleteApp } : {},
|
|
99797
|
+
...mastery.warnings ? { warnings: mastery.warnings } : {}
|
|
99798
|
+
};
|
|
99799
|
+
}
|
|
99800
|
+
function stageAssessmentAttemptSupersession(attempt) {
|
|
99801
|
+
Object.assign(attempt.result, ASSESSMENT_ATTEMPT_SUPERSEDED);
|
|
99802
|
+
Object.assign(attempt.selection, ASSESSMENT_ATTEMPT_SUPERSEDED);
|
|
99803
|
+
}
|
|
99804
|
+
function shouldConfirmAssessmentVersionUnavailable(details) {
|
|
99805
|
+
return typeof details === "object" && details !== null && "expectedRevision" in details && typeof details.expectedRevision === "string" && !("currentRevision" in details);
|
|
99806
|
+
}
|
|
99807
|
+
function buildAssessmentAttemptSupersessionUpdate(result) {
|
|
99808
|
+
if (result.status !== "active" || !isAssessmentAttemptOpen({
|
|
99809
|
+
inProgress: result.inProgress ?? "",
|
|
99810
|
+
scoreStatus: result.scoreStatus
|
|
99811
|
+
})) {
|
|
99812
|
+
return null;
|
|
99813
|
+
}
|
|
99814
|
+
return {
|
|
99815
|
+
status: "active",
|
|
99816
|
+
assessmentLineItem: result.assessmentLineItem,
|
|
99817
|
+
student: result.student,
|
|
99818
|
+
score: result.score ?? 0,
|
|
99819
|
+
scoreDate: result.scoreDate,
|
|
99820
|
+
...ASSESSMENT_ATTEMPT_SUPERSEDED,
|
|
99821
|
+
metadata: result.metadata ?? {}
|
|
99822
|
+
};
|
|
99823
|
+
}
|
|
99824
|
+
async function resolveAvailableAssessmentAttempt(initialDecision, select3, resume) {
|
|
99825
|
+
let decision = initialDecision;
|
|
99826
|
+
while (decision?.kind === "resume") {
|
|
99827
|
+
const snapshot = await resume(decision);
|
|
99828
|
+
if (snapshot !== null) {
|
|
99829
|
+
return { kind: "resumed", snapshot };
|
|
99830
|
+
}
|
|
99831
|
+
decision = select3();
|
|
99832
|
+
}
|
|
99833
|
+
return { kind: "continue", decision };
|
|
99834
|
+
}
|
|
99835
|
+
function buildReviewItemResultMetadata(input) {
|
|
99836
|
+
const administration = itemAdministration(input.metadata.itemSubmissions, input.selection.itemIdentifier);
|
|
99837
|
+
if (!administration) {
|
|
99838
|
+
throw new Error(`Cannot create a review result for unadministered candidate ${input.selection.itemIdentifier}`);
|
|
99839
|
+
}
|
|
99840
|
+
return {
|
|
99841
|
+
version: PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_VERSION,
|
|
99842
|
+
parentAttemptId: input.parentAttemptId,
|
|
99843
|
+
activityId: input.metadata.activityId,
|
|
99844
|
+
integrationId: input.metadata.integrationId,
|
|
99845
|
+
enrollmentId: input.metadata.enrollmentId,
|
|
99846
|
+
bankRevision: input.metadata.review.bankRevision,
|
|
99847
|
+
itemIdentifier: input.selection.itemIdentifier,
|
|
99848
|
+
standard: input.selection.standard,
|
|
99849
|
+
deliveredAt: administration.submittedAt,
|
|
99850
|
+
responseVersion: input.responseVersion ?? input.metadata.responseVersion,
|
|
99851
|
+
responses: input.metadata.responses[input.selection.itemIdentifier] ?? {},
|
|
99852
|
+
...input.submissionId ? { submissionId: input.submissionId } : {},
|
|
99853
|
+
...input.isCorrect === undefined ? {} : { isCorrect: input.isCorrect }
|
|
99854
|
+
};
|
|
99855
|
+
}
|
|
99856
|
+
function buildOpenReviewChildResult(input) {
|
|
99857
|
+
return {
|
|
99858
|
+
status: "active",
|
|
99859
|
+
assessmentLineItem: { sourcedId: input.childLineItemId },
|
|
99860
|
+
student: input.student,
|
|
99861
|
+
score: 0,
|
|
99862
|
+
scoreDate: input.metadata.deliveredAt,
|
|
99863
|
+
...ASSESSMENT_ATTEMPT_OPEN,
|
|
99864
|
+
metadata: {
|
|
99865
|
+
[PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY]: input.metadata
|
|
99866
|
+
}
|
|
99867
|
+
};
|
|
99868
|
+
}
|
|
99869
|
+
function diagnosticQuestionLineItemId(parentLineItemId, itemIdentifier) {
|
|
99870
|
+
return deterministicUUID(["playcademy:diagnostic-question-line-item:v1", parentLineItemId, itemIdentifier].join("\x00"));
|
|
99871
|
+
}
|
|
99872
|
+
function diagnosticQuestionResultId(parentAttemptId, childLineItemId) {
|
|
99873
|
+
return deterministicUUID(["playcademy:diagnostic-question-result:v1", parentAttemptId, childLineItemId].join("\x00"));
|
|
99874
|
+
}
|
|
99875
|
+
function diagnosticAdministration(input) {
|
|
99876
|
+
const ledgerIndex = input.metadata.diagnostic.ledger.findIndex((entry) => entry.routingNodeKey === input.ledgerEntry.routingNodeKey);
|
|
99877
|
+
const persistedEntry = input.metadata.diagnostic.ledger[ledgerIndex];
|
|
99878
|
+
const administration = input.metadata.itemSubmissions[ledgerIndex];
|
|
99879
|
+
if (ledgerIndex === -1 || !persistedEntry || !administration || persistedEntry.stageKey !== input.ledgerEntry.stageKey || persistedEntry.trackKey !== input.ledgerEntry.trackKey || persistedEntry.itemIdentifier !== input.ledgerEntry.itemIdentifier || persistedEntry.isCorrect !== input.ledgerEntry.isCorrect || administration.itemIdentifier !== input.ledgerEntry.itemIdentifier || administration.isCorrect !== input.ledgerEntry.isCorrect) {
|
|
99880
|
+
throw new Error(`Cannot create a diagnostic result for uncommitted routing node ${input.ledgerEntry.routingNodeKey}`);
|
|
99881
|
+
}
|
|
99882
|
+
return administration;
|
|
99883
|
+
}
|
|
99884
|
+
function buildDiagnosticItemResultMetadata(input) {
|
|
99885
|
+
const administration = diagnosticAdministration(input);
|
|
99886
|
+
return {
|
|
99887
|
+
version: PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_VERSION,
|
|
99888
|
+
parentAttemptId: input.parentAttemptId,
|
|
99889
|
+
activityId: input.metadata.activityId,
|
|
99890
|
+
integrationId: input.metadata.integrationId,
|
|
99891
|
+
enrollmentId: input.metadata.enrollmentId,
|
|
99892
|
+
definitionId: input.metadata.diagnostic.definitionId,
|
|
99893
|
+
diagnosticKey: input.metadata.diagnostic.diagnosticKey,
|
|
99894
|
+
routingRevision: input.metadata.diagnostic.routingRevision,
|
|
99895
|
+
contentRevision: input.metadata.selectedTest.contentRevision,
|
|
99896
|
+
stageKey: input.ledgerEntry.stageKey,
|
|
99897
|
+
trackKey: input.ledgerEntry.trackKey,
|
|
99898
|
+
routingNodeKey: input.ledgerEntry.routingNodeKey,
|
|
99899
|
+
itemIdentifier: input.ledgerEntry.itemIdentifier,
|
|
99900
|
+
deliveredAt: administration.submittedAt,
|
|
99901
|
+
responseVersion: administration.responseVersion,
|
|
99902
|
+
responses: input.metadata.responses[input.ledgerEntry.itemIdentifier] ?? {},
|
|
99903
|
+
submissionId: administration.submissionId,
|
|
99904
|
+
isCorrect: input.ledgerEntry.isCorrect
|
|
99905
|
+
};
|
|
99906
|
+
}
|
|
99907
|
+
function resolveDiagnosticChildResult(input) {
|
|
99908
|
+
if (!input.childResult) {
|
|
99909
|
+
return { action: "create" };
|
|
99910
|
+
}
|
|
99911
|
+
const childMetadata = playcademyDiagnosticAssessmentItemResultMetadata(input.childResult.metadata);
|
|
99912
|
+
const expectedMetadata = buildDiagnosticItemResultMetadata({
|
|
99913
|
+
metadata: input.parentMetadata,
|
|
99914
|
+
ledgerEntry: input.ledgerEntry,
|
|
99915
|
+
parentAttemptId: input.parentResult.sourcedId
|
|
99916
|
+
});
|
|
99917
|
+
if (input.childResult.assessmentLineItem.sourcedId !== input.childLineItemId || input.childResult.student.sourcedId !== input.parentResult.student.sourcedId || !childMetadata || canonicalJson2(childMetadata) !== canonicalJson2(expectedMetadata)) {
|
|
99918
|
+
return { action: "restore", reason: "incompatible" };
|
|
99919
|
+
}
|
|
99920
|
+
return input.childResult.status === "active" ? { action: "reuse", metadata: expectedMetadata } : { action: "restore", reason: "inactive" };
|
|
99921
|
+
}
|
|
99922
|
+
function buildFinalizedDiagnosticChildResult(input) {
|
|
99923
|
+
const administration = diagnosticAdministration(input);
|
|
99924
|
+
const metadata2 = buildDiagnosticItemResultMetadata(input);
|
|
99925
|
+
return {
|
|
99926
|
+
resultUpdate: {
|
|
99927
|
+
status: "active",
|
|
99928
|
+
assessmentLineItem: { sourcedId: input.childLineItemId },
|
|
99929
|
+
student: input.student,
|
|
99930
|
+
score: administration.score.earned,
|
|
99931
|
+
scoreDate: administration.submittedAt,
|
|
99932
|
+
...ASSESSMENT_ATTEMPT_COMPLETED,
|
|
99933
|
+
metadata: {
|
|
99934
|
+
[PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY]: metadata2
|
|
99935
|
+
}
|
|
99936
|
+
}
|
|
99937
|
+
};
|
|
99938
|
+
}
|
|
99939
|
+
function buildFinalizedReviewChildResult(input) {
|
|
99940
|
+
const metadata2 = buildReviewItemResultMetadata({
|
|
99941
|
+
metadata: input.metadata,
|
|
99942
|
+
selection: input.selection,
|
|
99943
|
+
parentAttemptId: input.parentAttemptId,
|
|
99944
|
+
submissionId: input.submissionId,
|
|
99945
|
+
isCorrect: input.itemResult.isCorrect
|
|
99946
|
+
});
|
|
99947
|
+
return {
|
|
99948
|
+
resultUpdate: {
|
|
99949
|
+
status: "active",
|
|
99950
|
+
assessmentLineItem: { sourcedId: input.childLineItemId },
|
|
99951
|
+
student: input.student,
|
|
99952
|
+
score: input.itemResult.score.earned,
|
|
99953
|
+
scoreDate: input.timestamp,
|
|
99954
|
+
...ASSESSMENT_ATTEMPT_COMPLETED,
|
|
99955
|
+
metadata: {
|
|
99956
|
+
[PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY]: metadata2
|
|
99957
|
+
}
|
|
99958
|
+
},
|
|
99959
|
+
outcome: {
|
|
99960
|
+
itemIdentifier: input.selection.itemIdentifier,
|
|
99961
|
+
standard: input.selection.standard,
|
|
99962
|
+
answered: Object.keys(input.metadata.responses[input.selection.itemIdentifier] ?? {}).length > 0,
|
|
99963
|
+
score: input.itemResult.score,
|
|
99964
|
+
isCorrect: input.itemResult.isCorrect
|
|
99965
|
+
}
|
|
99966
|
+
};
|
|
99967
|
+
}
|
|
99968
|
+
function resolveReviewChildResult(input) {
|
|
99969
|
+
if (!input.childResult) {
|
|
99970
|
+
return { action: "create" };
|
|
99971
|
+
}
|
|
99972
|
+
const childMetadata = playcademyAssessmentItemResultMetadata(input.childResult.metadata);
|
|
99973
|
+
const expectedResponses = input.parentMetadata.responses[input.selection.itemIdentifier] ?? {};
|
|
99974
|
+
const administration = itemAdministration(input.parentMetadata.itemSubmissions, input.selection.itemIdentifier);
|
|
99975
|
+
if (input.childResult.assessmentLineItem.sourcedId !== input.childLineItemId || input.childResult.student.sourcedId !== input.parentResult.student.sourcedId || !childMetadata || childMetadata.parentAttemptId !== input.parentResult.sourcedId || childMetadata.activityId !== input.parentMetadata.activityId || childMetadata.integrationId !== input.parentMetadata.integrationId || childMetadata.enrollmentId !== input.parentMetadata.enrollmentId || childMetadata.bankRevision !== input.parentMetadata.review.bankRevision || childMetadata.itemIdentifier !== input.selection.itemIdentifier || childMetadata.standard.framework !== input.selection.standard.framework || childMetadata.standard.identifier !== input.selection.standard.identifier || childMetadata.deliveredAt !== administration?.submittedAt || childMetadata.responseVersion > input.parentMetadata.responseVersion || canonicalJson2(childMetadata.responses) !== canonicalJson2(expectedResponses)) {
|
|
99976
|
+
return { action: "restore", reason: "incompatible" };
|
|
99977
|
+
}
|
|
99978
|
+
return input.childResult.status === "active" ? { action: "reuse", metadata: childMetadata } : { action: "restore", reason: "inactive" };
|
|
99979
|
+
}
|
|
99980
|
+
function buildAssessmentSubmitResult(input) {
|
|
99981
|
+
if (!input.metadata.score || !input.metadata.submissionId) {
|
|
99982
|
+
return null;
|
|
99983
|
+
}
|
|
99984
|
+
if (input.metadata.purpose === "diagnostic" && input.metadata.diagnostic) {
|
|
99985
|
+
const tracks = input.metadata.completion?.diagnosticTracks;
|
|
99986
|
+
return tracks ? {
|
|
99987
|
+
purpose: input.metadata.purpose,
|
|
99988
|
+
attemptId: input.attemptId,
|
|
99989
|
+
submissionId: input.metadata.submissionId,
|
|
99990
|
+
diagnosticKey: input.metadata.diagnostic.diagnosticKey,
|
|
99991
|
+
responseVersion: input.metadata.responseVersion,
|
|
99992
|
+
status: "awaiting_award",
|
|
99993
|
+
tracks
|
|
99994
|
+
} : null;
|
|
99995
|
+
}
|
|
99996
|
+
const base = {
|
|
99997
|
+
attemptId: input.attemptId,
|
|
99998
|
+
submissionId: input.metadata.submissionId,
|
|
99999
|
+
responseVersion: input.metadata.responseVersion,
|
|
100000
|
+
status: "awaiting_award",
|
|
100001
|
+
responses: input.metadata.responses,
|
|
100002
|
+
score: input.metadata.score
|
|
100003
|
+
};
|
|
100004
|
+
if (input.metadata.purpose === "review") {
|
|
100005
|
+
const itemOutcomes = input.metadata.completion?.itemOutcomes;
|
|
100006
|
+
return itemOutcomes ? { ...base, purpose: input.metadata.purpose, itemOutcomes } : null;
|
|
100007
|
+
}
|
|
100008
|
+
return { ...base, purpose: input.metadata.purpose };
|
|
100009
|
+
}
|
|
100010
|
+
function assessmentAwardFromResult(result, metadata2) {
|
|
100011
|
+
if (metadata2.award) {
|
|
100012
|
+
return metadata2.award;
|
|
100013
|
+
}
|
|
100014
|
+
const xp = result.metadata?.xp;
|
|
100015
|
+
return typeof xp === "number" && Number.isFinite(xp) && xp >= 0 ? { xpAwarded: xp, masteredUnitsApplied: 0 } : null;
|
|
100016
|
+
}
|
|
100017
|
+
function buildCompletedAssessmentSubmitResult(input) {
|
|
100018
|
+
const attemptId = input.result.sourcedId;
|
|
100019
|
+
const award = assessmentAwardFromResult(input.result, input.metadata);
|
|
100020
|
+
if (award === null) {
|
|
100021
|
+
throw new GoneError("This completed assessment is missing its recorded XP.", {
|
|
100022
|
+
attemptId
|
|
100023
|
+
});
|
|
100024
|
+
}
|
|
100025
|
+
const submitted = buildAssessmentSubmitResult({ attemptId, metadata: input.metadata });
|
|
100026
|
+
if (!submitted) {
|
|
100027
|
+
throw new GoneError("This completed assessment has incomplete persisted results.", {
|
|
100028
|
+
attemptId
|
|
100029
|
+
});
|
|
100030
|
+
}
|
|
100031
|
+
return {
|
|
100032
|
+
...submitted,
|
|
100033
|
+
status: "completed",
|
|
100034
|
+
xpAwarded: award.xpAwarded,
|
|
100035
|
+
masteredUnitsApplied: award.masteredUnitsApplied,
|
|
100036
|
+
...award.pctCompleteApp !== undefined ? { pctCompleteApp: award.pctCompleteApp } : {},
|
|
100037
|
+
...award.warnings ? { warnings: award.warnings } : {}
|
|
100038
|
+
};
|
|
100039
|
+
}
|
|
100040
|
+
function assessmentAttemptId(input) {
|
|
100041
|
+
return deterministicUUID([
|
|
100042
|
+
"playcademy:assessment-attempt",
|
|
100043
|
+
input.gameId,
|
|
100044
|
+
input.studentId,
|
|
100045
|
+
input.courseId,
|
|
100046
|
+
input.activityId,
|
|
100047
|
+
input.purpose,
|
|
100048
|
+
String(input.attemptNumber)
|
|
100049
|
+
].join("\x00"));
|
|
100050
|
+
}
|
|
100051
|
+
async function findLatestAssessmentResult(results, match) {
|
|
100052
|
+
let latest = null;
|
|
100053
|
+
let latestTimestamp = 0;
|
|
100054
|
+
for await (const result of results) {
|
|
100055
|
+
const resultTimestamp = assessmentAttemptTimestamp(result.scoreDate);
|
|
100056
|
+
if (latest && resultTimestamp < latestTimestamp) {
|
|
100057
|
+
break;
|
|
100058
|
+
}
|
|
100059
|
+
const matched = match(result);
|
|
100060
|
+
if (matched !== null && (!latest || compareNewestCompletedAssessmentAttempt({ attemptId: result.sourcedId, scoreDate: result.scoreDate }, {
|
|
100061
|
+
attemptId: latest.result.sourcedId,
|
|
100062
|
+
scoreDate: latest.result.scoreDate
|
|
100063
|
+
}) < 0)) {
|
|
100064
|
+
latest = { result, match: matched };
|
|
100065
|
+
latestTimestamp = resultTimestamp;
|
|
100066
|
+
}
|
|
100067
|
+
}
|
|
100068
|
+
return latest;
|
|
100069
|
+
}
|
|
100070
|
+
function playableChoice(choice) {
|
|
100071
|
+
return {
|
|
100072
|
+
identifier: choice.identifier,
|
|
100073
|
+
content: choice.content,
|
|
100074
|
+
...choice.contentNodes ? { contentNodes: choice.contentNodes } : {}
|
|
100075
|
+
};
|
|
100076
|
+
}
|
|
100077
|
+
function playableAssociableChoice(choice) {
|
|
100078
|
+
const matchMax = qtiAttributeCapacity(choice.attributes, "match-max", 0);
|
|
100079
|
+
if (matchMax === null) {
|
|
100080
|
+
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", "The selected assessment contains an invalid Match capacity.");
|
|
100081
|
+
}
|
|
100082
|
+
return {
|
|
100083
|
+
...playableChoice(choice),
|
|
100084
|
+
matchMax
|
|
100085
|
+
};
|
|
100086
|
+
}
|
|
100087
|
+
function requireChoices2(choices) {
|
|
100088
|
+
if (choices.length === 0 || choices.some((candidate) => !candidate.identifier)) {
|
|
100089
|
+
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", "The selected assessment contains an interaction Playcademy cannot render.");
|
|
100090
|
+
}
|
|
100091
|
+
return choices;
|
|
100092
|
+
}
|
|
100093
|
+
function isPlayableAssessmentShape(value) {
|
|
100094
|
+
return PLAYABLE_SHAPE_SET.has(value);
|
|
100095
|
+
}
|
|
100096
|
+
function playableGraphic(graphic) {
|
|
100097
|
+
return {
|
|
100098
|
+
src: graphic.src,
|
|
100099
|
+
...graphic.width === undefined ? {} : { width: graphic.width },
|
|
100100
|
+
...graphic.height === undefined ? {} : { height: graphic.height },
|
|
100101
|
+
...graphic.description ? { description: graphic.description } : {}
|
|
100102
|
+
};
|
|
100103
|
+
}
|
|
100104
|
+
function requireGraphic(questionIdentifier, graphic) {
|
|
100105
|
+
if (!graphic) {
|
|
100106
|
+
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${questionIdentifier} is missing a required graphic.`);
|
|
100107
|
+
}
|
|
100108
|
+
return playableGraphic(graphic);
|
|
100109
|
+
}
|
|
100110
|
+
function requireHotspots(questionIdentifier, hotspots) {
|
|
100111
|
+
if (hotspots.length === 0) {
|
|
100112
|
+
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${questionIdentifier} declares no graphic regions.`);
|
|
100113
|
+
}
|
|
100114
|
+
return hotspots.map((hotspot) => ({
|
|
100115
|
+
identifier: hotspot.identifier,
|
|
100116
|
+
shape: isPlayableAssessmentShape(hotspot.shape) ? hotspot.shape : "default",
|
|
100117
|
+
coords: [...hotspot.coords],
|
|
100118
|
+
...hotspot.matchMax === undefined ? {} : { matchMax: hotspot.matchMax },
|
|
100119
|
+
...hotspot.description ? { description: hotspot.description } : {}
|
|
100120
|
+
}));
|
|
100121
|
+
}
|
|
100122
|
+
function parseInteraction2(questionIdentifier, interaction) {
|
|
100123
|
+
const { type } = interaction;
|
|
100124
|
+
const responseIdentifier = interaction.attributes["response-identifier"];
|
|
100125
|
+
if (!responseIdentifier) {
|
|
100126
|
+
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${questionIdentifier} has no response identifier.`);
|
|
100127
|
+
}
|
|
100128
|
+
const prompt = interaction.embeddedPrompt ? { prompt: interaction.embeddedPrompt } : {};
|
|
100129
|
+
if (type === "choice") {
|
|
100130
|
+
const choices = requireChoices2(interaction.choices.map(playableChoice));
|
|
100131
|
+
return {
|
|
100132
|
+
type,
|
|
100133
|
+
responseIdentifier,
|
|
100134
|
+
...prompt,
|
|
100135
|
+
choices,
|
|
100136
|
+
minChoices: qtiAttributeNumber(interaction.attributes, "min-choices", 1),
|
|
100137
|
+
maxChoices: qtiAttributeMaximum(interaction.attributes, "max-choices", 1, choices.length)
|
|
100138
|
+
};
|
|
100139
|
+
}
|
|
100140
|
+
if (type === "inline-choice") {
|
|
100141
|
+
return {
|
|
100142
|
+
type,
|
|
100143
|
+
responseIdentifier,
|
|
100144
|
+
...prompt,
|
|
100145
|
+
choices: requireChoices2(interaction.choices.map(playableChoice))
|
|
100146
|
+
};
|
|
100147
|
+
}
|
|
100148
|
+
if (type === "text-entry") {
|
|
100149
|
+
const expectedLength = qtiAttributeNumber(interaction.attributes, "expected-length", 0);
|
|
100150
|
+
return {
|
|
100151
|
+
type,
|
|
100152
|
+
responseIdentifier,
|
|
100153
|
+
...prompt,
|
|
100154
|
+
...Number.isInteger(expectedLength) && expectedLength > 0 ? { expectedLength } : {},
|
|
100155
|
+
...interaction.attributes.placeholder ? { placeholder: interaction.attributes.placeholder } : {}
|
|
100156
|
+
};
|
|
100157
|
+
}
|
|
100158
|
+
if (type === "order") {
|
|
100159
|
+
return {
|
|
100160
|
+
type,
|
|
100161
|
+
responseIdentifier,
|
|
100162
|
+
...prompt,
|
|
100163
|
+
choices: requireChoices2(interaction.choices.map(playableChoice)),
|
|
100164
|
+
shuffle: qtiAttributeBoolean(interaction.attributes, "shuffle")
|
|
100165
|
+
};
|
|
100166
|
+
}
|
|
100167
|
+
if (type === "match") {
|
|
100168
|
+
const sets = interaction.choiceSets;
|
|
100169
|
+
if (sets.length !== 2) {
|
|
100170
|
+
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", "Match interactions require exactly two choice sets.");
|
|
100171
|
+
}
|
|
100172
|
+
const sourceChoices = requireChoices2(sets[0].map(playableAssociableChoice));
|
|
100173
|
+
const targetChoices = requireChoices2(sets[1].map(playableAssociableChoice));
|
|
100174
|
+
return {
|
|
100175
|
+
type,
|
|
100176
|
+
responseIdentifier,
|
|
100177
|
+
...prompt,
|
|
100178
|
+
sourceChoices,
|
|
100179
|
+
targetChoices,
|
|
100180
|
+
maxAssociations: qtiAttributeMaximum(interaction.attributes, "max-associations", 1, sourceChoices.length * targetChoices.length),
|
|
100181
|
+
shuffle: qtiAttributeBoolean(interaction.attributes, "shuffle")
|
|
100182
|
+
};
|
|
100183
|
+
}
|
|
100184
|
+
if (type === "hotspot") {
|
|
100185
|
+
const image = requireGraphic(questionIdentifier, interaction.graphic);
|
|
100186
|
+
const hotspots = requireHotspots(questionIdentifier, interaction.hotspots);
|
|
100187
|
+
return {
|
|
100188
|
+
type,
|
|
100189
|
+
responseIdentifier,
|
|
100190
|
+
...prompt,
|
|
100191
|
+
image,
|
|
100192
|
+
hotspots,
|
|
100193
|
+
minChoices: qtiAttributeNumber(interaction.attributes, "min-choices", 1),
|
|
100194
|
+
maxChoices: qtiAttributeMaximum(interaction.attributes, "max-choices", 1, hotspots.length)
|
|
100195
|
+
};
|
|
100196
|
+
}
|
|
100197
|
+
if (type === "gap-match") {
|
|
100198
|
+
if (interaction.gapTexts.length === 0 || interaction.gaps.length === 0 || interaction.textWithGaps === undefined || !interaction.gapContent || !qtiGapContentMatchesGaps(interaction.gapContent, interaction.gaps)) {
|
|
100199
|
+
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${questionIdentifier} has an incomplete gap-match interaction.`);
|
|
100200
|
+
}
|
|
100201
|
+
return {
|
|
100202
|
+
type,
|
|
100203
|
+
responseIdentifier,
|
|
100204
|
+
...prompt,
|
|
100205
|
+
content: interaction.textWithGaps,
|
|
100206
|
+
contentNodes: interaction.gapContent,
|
|
100207
|
+
gapTexts: interaction.gapTexts.map((gapText) => ({
|
|
100208
|
+
identifier: gapText.identifier,
|
|
100209
|
+
content: gapText.content,
|
|
100210
|
+
matchMax: gapText.matchMax
|
|
100211
|
+
})),
|
|
100212
|
+
gaps: [...interaction.gaps],
|
|
100213
|
+
maxAssociations: qtiAttributeNumber(interaction.attributes, "max-associations", 0)
|
|
100214
|
+
};
|
|
100215
|
+
}
|
|
100216
|
+
if (type === "graphic-gap-match") {
|
|
100217
|
+
const image = requireGraphic(questionIdentifier, interaction.graphic);
|
|
100218
|
+
const hotspots = requireHotspots(questionIdentifier, interaction.hotspots);
|
|
100219
|
+
if (interaction.gapImages.length === 0) {
|
|
100220
|
+
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${questionIdentifier} has no draggable gap images.`);
|
|
100221
|
+
}
|
|
100222
|
+
return {
|
|
100223
|
+
type,
|
|
100224
|
+
responseIdentifier,
|
|
100225
|
+
...prompt,
|
|
100226
|
+
image,
|
|
100227
|
+
gapImages: interaction.gapImages.map((gapImage) => ({
|
|
100228
|
+
identifier: gapImage.identifier,
|
|
100229
|
+
...gapImage.graphic ? { image: playableGraphic(gapImage.graphic) } : {},
|
|
100230
|
+
matchMax: gapImage.matchMax,
|
|
100231
|
+
...gapImage.description ? { description: gapImage.description } : {}
|
|
100232
|
+
})),
|
|
100233
|
+
hotspots,
|
|
100234
|
+
maxAssociations: qtiAttributeNumber(interaction.attributes, "max-associations", 0)
|
|
100235
|
+
};
|
|
100236
|
+
}
|
|
100237
|
+
if (type === "select-point") {
|
|
100238
|
+
return {
|
|
100239
|
+
type,
|
|
100240
|
+
responseIdentifier,
|
|
100241
|
+
...prompt,
|
|
100242
|
+
image: requireGraphic(questionIdentifier, interaction.graphic),
|
|
100243
|
+
minChoices: qtiAttributeNumber(interaction.attributes, "min-choices", 0),
|
|
100244
|
+
maxChoices: qtiAttributeNumber(interaction.attributes, "max-choices", 0)
|
|
100245
|
+
};
|
|
100246
|
+
}
|
|
100247
|
+
if (type === "graphic-associate") {
|
|
100248
|
+
return {
|
|
100249
|
+
type,
|
|
100250
|
+
responseIdentifier,
|
|
100251
|
+
...prompt,
|
|
100252
|
+
image: requireGraphic(questionIdentifier, interaction.graphic),
|
|
100253
|
+
hotspots: requireHotspots(questionIdentifier, interaction.hotspots),
|
|
100254
|
+
maxAssociations: qtiAttributeNumber(interaction.attributes, "max-associations", 1)
|
|
100255
|
+
};
|
|
100256
|
+
}
|
|
100257
|
+
if (type === "position-object") {
|
|
100258
|
+
return {
|
|
100259
|
+
type,
|
|
100260
|
+
responseIdentifier,
|
|
100261
|
+
...prompt,
|
|
100262
|
+
stage: requireGraphic(questionIdentifier, interaction.graphic),
|
|
100263
|
+
object: requireGraphic(questionIdentifier, interaction.objectGraphic),
|
|
100264
|
+
maxPlacements: qtiAttributeNumber(interaction.attributes, "max-choices", 1)
|
|
100265
|
+
};
|
|
100266
|
+
}
|
|
100267
|
+
if (type === "hottext") {
|
|
100268
|
+
const segments = interaction.textSegments;
|
|
100269
|
+
if (!segments.some((segment) => segment.selectable) || segments.some((segment) => segment.selectable && (!segment.identifier || !segment.content))) {
|
|
100270
|
+
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", "Hottext interactions require identified selectable content.");
|
|
100271
|
+
}
|
|
100272
|
+
return {
|
|
100273
|
+
type,
|
|
100274
|
+
responseIdentifier,
|
|
100275
|
+
...prompt,
|
|
100276
|
+
segments,
|
|
100277
|
+
maxChoices: qtiAttributeMaximum(interaction.attributes, "max-choices", 1, segments.filter((segment) => segment.selectable).length)
|
|
100278
|
+
};
|
|
100279
|
+
}
|
|
100280
|
+
const unsupported = type;
|
|
100281
|
+
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${questionIdentifier} uses unsupported interaction ${unsupported}.`);
|
|
100282
|
+
}
|
|
100283
|
+
function normalizedQtiItem(question) {
|
|
100284
|
+
if (!question.rawXml) {
|
|
100285
|
+
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${question.identifier} has no authoritative QTI XML.`);
|
|
100286
|
+
}
|
|
100287
|
+
try {
|
|
100288
|
+
return parseQtiItemXml(question.rawXml);
|
|
100289
|
+
} catch (error88) {
|
|
100290
|
+
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${question.identifier} contains invalid QTI XML.`, { reason: errorMessage(error88) });
|
|
100291
|
+
}
|
|
100292
|
+
}
|
|
100293
|
+
function parsePlayableItem(question) {
|
|
100294
|
+
const parsed = normalizedQtiItem(question);
|
|
100295
|
+
const interactions = playableQtiInteractions(parsed);
|
|
100296
|
+
if (!interactions) {
|
|
100297
|
+
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${question.identifier} must contain only supported interactions with unique response identifiers.`);
|
|
100298
|
+
}
|
|
100299
|
+
return {
|
|
100300
|
+
identifier: question.identifier,
|
|
100301
|
+
title: question.title,
|
|
100302
|
+
prompt: parsed.bodyTextWithBlanks,
|
|
100303
|
+
...parsed.bodyContent ? { promptContent: parsed.bodyContent } : {},
|
|
100304
|
+
maxScore: qtiItemMaxScore(parsed),
|
|
100305
|
+
interactions: interactions.map((interaction) => parseInteraction2(question.identifier, interaction))
|
|
100306
|
+
};
|
|
100307
|
+
}
|
|
100308
|
+
function assessmentContentRevision(test, questions) {
|
|
100309
|
+
return sha256Hex(canonicalJson2({
|
|
100310
|
+
identifier: test.identifier,
|
|
100311
|
+
title: test.title,
|
|
100312
|
+
qtiVersion: test.qtiVersion,
|
|
100313
|
+
parts: test["qti-test-part"],
|
|
100314
|
+
questions: questions.questions.map(({ reference, question }) => ({
|
|
100315
|
+
identifier: reference.identifier,
|
|
100316
|
+
rawXml: question.rawXml
|
|
100317
|
+
}))
|
|
100318
|
+
}));
|
|
100319
|
+
}
|
|
100320
|
+
async function buildPlayableAssessment(test, questions) {
|
|
100321
|
+
return {
|
|
100322
|
+
identifier: test.identifier,
|
|
100323
|
+
contractVersion: PLAYCADEMY_ASSESSMENT_CONTRACT_VERSION,
|
|
100324
|
+
contentRevision: await assessmentContentRevision(test, questions),
|
|
100325
|
+
title: test.title,
|
|
100326
|
+
items: questions.questions.map(({ question }) => parsePlayableItem(question))
|
|
100327
|
+
};
|
|
100328
|
+
}
|
|
100329
|
+
function assessmentFixtureScoringKeys(assessment, questions) {
|
|
100330
|
+
const questionByIdentifier = new Map(questions.questions.map(({ question }) => [question.identifier, question]));
|
|
100331
|
+
const correctResponses = {};
|
|
100332
|
+
const responseAreas = {};
|
|
100333
|
+
for (const item of assessment.items) {
|
|
100334
|
+
const question = questionByIdentifier.get(item.identifier);
|
|
100335
|
+
const normalizedDeclarations = question ? normalizedQtiItem(question).responseDeclarations : [];
|
|
100336
|
+
const itemResponses = {};
|
|
100337
|
+
for (const interaction of item.interactions) {
|
|
100338
|
+
const responseIdentifier = interaction.responseIdentifier;
|
|
100339
|
+
const declaration = question?.responseDeclarations?.find((candidate) => candidate.identifier === responseIdentifier);
|
|
100340
|
+
const declaredValues = declaration?.correctResponse?.value ?? [];
|
|
100341
|
+
const normalizedDeclaration = normalizedDeclarations.find((candidate) => candidate.identifier === responseIdentifier);
|
|
100342
|
+
const values = declaredValues.length > 0 ? declaredValues : normalizedDeclaration?.correctValues ?? [];
|
|
100343
|
+
const areas = interaction.type === "select-point" || interaction.type === "position-object" ? normalizedDeclaration?.areaMapping?.entries.filter((entry) => entry.value > 0 && isPlayableAssessmentShape(entry.shape)) : undefined;
|
|
100344
|
+
if (areas?.length) {
|
|
100345
|
+
(responseAreas[item.identifier] ??= {})[responseIdentifier] = areas.map((entry) => ({
|
|
100346
|
+
shape: entry.shape,
|
|
100347
|
+
coords: [...entry.coords],
|
|
100348
|
+
value: entry.value
|
|
100349
|
+
}));
|
|
100350
|
+
}
|
|
100351
|
+
if (values.length > 0) {
|
|
100352
|
+
const cardinality = declaration?.cardinality ?? normalizedDeclaration?.cardinality;
|
|
100353
|
+
itemResponses[responseIdentifier] = cardinality === "single" && values.length === 1 ? values[0] : values;
|
|
100354
|
+
} else if (!areas?.length) {
|
|
100355
|
+
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${item.identifier} has no fixture scoring key.`);
|
|
100356
|
+
}
|
|
100357
|
+
}
|
|
100358
|
+
correctResponses[item.identifier] = itemResponses;
|
|
100359
|
+
}
|
|
100360
|
+
return {
|
|
100361
|
+
correctResponses,
|
|
100362
|
+
...Object.keys(responseAreas).length > 0 ? { responseAreas } : {}
|
|
100363
|
+
};
|
|
100364
|
+
}
|
|
100365
|
+
function prepareDiagnosticAssessmentResponses(assessment, current, input) {
|
|
100366
|
+
const update2 = { [input.itemIdentifier]: input.responses };
|
|
100367
|
+
validateAssessmentResponseUpdate(assessment, update2);
|
|
100368
|
+
const responses = applyAssessmentResponseUpdate(current, update2);
|
|
100369
|
+
const item = assessment.items.find((candidate) => candidate.identifier === input.itemIdentifier);
|
|
100370
|
+
const itemResponses = responses[input.itemIdentifier];
|
|
100371
|
+
const missingResponse = item?.interactions.find((interaction) => itemResponses?.[interaction.responseIdentifier] === undefined);
|
|
100372
|
+
if (!item || !itemResponses || missingResponse) {
|
|
100373
|
+
throw new AssessmentRuntimeError("INVALID_RESPONSE", `Diagnostic item ${input.itemIdentifier} requires a response for every interaction.`, {
|
|
100374
|
+
itemIdentifier: input.itemIdentifier,
|
|
100375
|
+
...missingResponse ? { responseIdentifier: missingResponse.responseIdentifier } : {}
|
|
100376
|
+
});
|
|
100377
|
+
}
|
|
100378
|
+
validateAssessmentResponses(assessment, { [input.itemIdentifier]: itemResponses });
|
|
100379
|
+
return responses;
|
|
100380
|
+
}
|
|
100381
|
+
function validateAssessmentResponseUpdate(assessment, update2) {
|
|
100382
|
+
const message = assessmentResponseUpdateValidationMessage(assessment, update2);
|
|
100383
|
+
if (message) {
|
|
100384
|
+
throw new AssessmentRuntimeError("INVALID_RESPONSE", message);
|
|
100385
|
+
}
|
|
100386
|
+
}
|
|
100387
|
+
function validateAssessmentResponses(assessment, responses) {
|
|
100388
|
+
const message = assessmentResponseValidationMessage(assessment, responses);
|
|
100389
|
+
if (message) {
|
|
100390
|
+
throw new AssessmentRuntimeError("INVALID_RESPONSE", message);
|
|
100391
|
+
}
|
|
100392
|
+
}
|
|
100393
|
+
function validateAssessmentDraftResponses(assessment, responses) {
|
|
100394
|
+
const message = assessmentDraftResponseValidationMessage(assessment, responses);
|
|
100395
|
+
if (message) {
|
|
100396
|
+
throw new AssessmentRuntimeError("INVALID_RESPONSE", message);
|
|
100397
|
+
}
|
|
100398
|
+
}
|
|
100399
|
+
function assessmentScoringRequests(assessment, responses) {
|
|
100400
|
+
return assessment.items.map((item) => ({
|
|
100401
|
+
itemIdentifier: item.identifier,
|
|
100402
|
+
maxScore: item.maxScore,
|
|
100403
|
+
requests: item.interactions.map((interaction) => ({
|
|
100404
|
+
identifier: interaction.responseIdentifier,
|
|
100405
|
+
response: responses[item.identifier]?.[interaction.responseIdentifier] ?? ""
|
|
100406
|
+
}))
|
|
100407
|
+
}));
|
|
100408
|
+
}
|
|
100409
|
+
function buildAssessmentResultMetadata(input) {
|
|
100410
|
+
return {
|
|
100411
|
+
[PLAYCADEMY_ASSESSMENT_RESULT_METADATA_KEY]: input.metadata,
|
|
100412
|
+
assessmentPurpose: input.metadata.purpose,
|
|
100413
|
+
...assessmentPurposeClassificationFields2(input.metadata.purpose, input)
|
|
100414
|
+
};
|
|
100415
|
+
}
|
|
100416
|
+
function mergeAssessmentRuntimeMetadata(resultMetadata, metadata2) {
|
|
100417
|
+
return {
|
|
100418
|
+
...resultMetadata,
|
|
100419
|
+
[PLAYCADEMY_ASSESSMENT_RESULT_METADATA_KEY]: metadata2
|
|
100420
|
+
};
|
|
100421
|
+
}
|
|
100422
|
+
function aggregateAssessmentScore(itemScores, possible) {
|
|
100423
|
+
const earned = itemScores.reduce((sum, itemScore) => sum + (Number.isFinite(itemScore) ? itemScore : 0), 0);
|
|
100424
|
+
return {
|
|
100425
|
+
earned,
|
|
100426
|
+
possible,
|
|
100427
|
+
normalized: possible === 0 ? 0 : Math.min(1, Math.max(0, earned / possible))
|
|
100428
|
+
};
|
|
100429
|
+
}
|
|
100430
|
+
function countCorrectQuestions(verdicts) {
|
|
100431
|
+
return verdicts.filter((verdict) => verdict === true).length;
|
|
100432
|
+
}
|
|
100433
|
+
function assessmentScoreFromPercentage(percentage, possible) {
|
|
100434
|
+
const normalized = Math.min(1, Math.max(0, (percentage ?? 0) / 100));
|
|
100435
|
+
const earned = Math.round(normalized * possible * 1e6) / 1e6;
|
|
100436
|
+
return { earned, possible, normalized };
|
|
100437
|
+
}
|
|
100438
|
+
function buildAssessmentResultSubmission(input) {
|
|
100439
|
+
const metadata2 = {
|
|
100440
|
+
...input.metadata,
|
|
100441
|
+
submissionId: input.submissionId,
|
|
100442
|
+
score: input.score,
|
|
100443
|
+
completion: {
|
|
100444
|
+
testName: input.testName,
|
|
100445
|
+
courseName: input.courseName,
|
|
100446
|
+
totalQuestions: input.totalQuestions,
|
|
100447
|
+
correctQuestions: input.correctQuestions,
|
|
100448
|
+
...input.session ? { session: input.session } : {},
|
|
100449
|
+
...input.itemOutcomes ? { itemOutcomes: input.itemOutcomes } : {},
|
|
100450
|
+
...input.diagnosticTracks ? { diagnosticTracks: input.diagnosticTracks } : {}
|
|
100451
|
+
},
|
|
100452
|
+
updatedAt: input.timestamp
|
|
100453
|
+
};
|
|
100454
|
+
return {
|
|
100455
|
+
metadata: metadata2,
|
|
100456
|
+
resultUpdate: {
|
|
100457
|
+
status: input.result.status,
|
|
100458
|
+
assessmentLineItem: input.result.assessmentLineItem,
|
|
100459
|
+
student: input.result.student,
|
|
100460
|
+
score: input.score.normalized * 100,
|
|
100461
|
+
scoreDate: input.timestamp,
|
|
100462
|
+
...ASSESSMENT_ATTEMPT_AWAITING_AWARD,
|
|
100463
|
+
metadata: mergeAssessmentRuntimeMetadata(input.result.metadata, metadata2)
|
|
100464
|
+
}
|
|
100465
|
+
};
|
|
100466
|
+
}
|
|
100467
|
+
function buildAssessmentResultAwardFinalization(input) {
|
|
100468
|
+
const completion = input.metadata.completion;
|
|
100469
|
+
if (!completion || completion.totalQuestions === undefined) {
|
|
100470
|
+
throw new Error("A scored assessment must retain its completion facts before finalization");
|
|
100471
|
+
}
|
|
100472
|
+
const metadata2 = {
|
|
100473
|
+
...input.metadata,
|
|
100474
|
+
finalizedAt: input.timestamp,
|
|
100475
|
+
updatedAt: input.timestamp,
|
|
100476
|
+
award: input.award
|
|
100477
|
+
};
|
|
100478
|
+
const masteryRequested = input.award.masteredUnits !== undefined || input.award.masteredUnitsAbsolute !== undefined;
|
|
100479
|
+
return {
|
|
100480
|
+
metadata: metadata2,
|
|
100481
|
+
resultUpdate: {
|
|
100482
|
+
status: input.result.status,
|
|
100483
|
+
assessmentLineItem: input.result.assessmentLineItem,
|
|
100484
|
+
student: input.result.student,
|
|
100485
|
+
score: input.result.score ?? 0,
|
|
100486
|
+
scoreDate: input.result.scoreDate,
|
|
100487
|
+
...ASSESSMENT_ATTEMPT_COMPLETED,
|
|
100488
|
+
metadata: {
|
|
100489
|
+
...mergeAssessmentRuntimeMetadata(input.result.metadata, metadata2),
|
|
100490
|
+
xp: input.award.xpAwarded,
|
|
100491
|
+
...masteryRequested ? { masteredUnits: input.award.masteredUnitsApplied } : {},
|
|
100492
|
+
totalQuestions: completion.totalQuestions,
|
|
100493
|
+
correctQuestions: completion.correctQuestions,
|
|
100494
|
+
appName: input.appName
|
|
100495
|
+
}
|
|
100496
|
+
}
|
|
100497
|
+
};
|
|
100498
|
+
}
|
|
100499
|
+
var PLAYABLE_SHAPE_SET;
|
|
100500
|
+
var init_timeback_assessment_runtime_util = __esm(() => {
|
|
100501
|
+
init_src();
|
|
100502
|
+
init_assessment_runtime2();
|
|
100503
|
+
init_qti();
|
|
100504
|
+
init_types2();
|
|
100505
|
+
init_utils6();
|
|
100506
|
+
init_timeback4();
|
|
100507
|
+
init_uuid();
|
|
100508
|
+
init_errors2();
|
|
100509
|
+
PLAYABLE_SHAPE_SET = new Set(PLAYABLE_ASSESSMENT_SHAPES);
|
|
100510
|
+
});
|
|
100511
|
+
|
|
98568
100512
|
// ../api-core/src/utils/timeback-qti-authoring.util.ts
|
|
98569
100513
|
function recordValue(value) {
|
|
98570
100514
|
return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
@@ -99506,810 +101450,6 @@ var init_timeback_qti_authoring_util = __esm(() => {
|
|
|
99506
101450
|
];
|
|
99507
101451
|
});
|
|
99508
101452
|
|
|
99509
|
-
// ../api-core/src/utils/timeback-assessment-runtime.util.ts
|
|
99510
|
-
function assessmentPreparationContentionError(expectedResponseVersion, responseVersion, kind) {
|
|
99511
|
-
if (expectedResponseVersion === responseVersion) {
|
|
99512
|
-
return new ServiceUnavailableError(`The ${kind} response state changed repeatedly while this item was being prepared. Try again shortly.`, {
|
|
99513
|
-
retryable: true,
|
|
99514
|
-
reason: "PREPARATION_CONTENTION",
|
|
99515
|
-
expectedResponseVersion,
|
|
99516
|
-
responseVersion
|
|
99517
|
-
});
|
|
99518
|
-
}
|
|
99519
|
-
return AssessmentRuntimeError.from(assessmentResponseVersionConflict(expectedResponseVersion, responseVersion));
|
|
99520
|
-
}
|
|
99521
|
-
function assessmentTrackableCourse(integration) {
|
|
99522
|
-
if (!isTimebackGrade2(integration.grade) || !isTimebackSubject(integration.subject)) {
|
|
99523
|
-
return null;
|
|
99524
|
-
}
|
|
99525
|
-
return { grade: integration.grade, subject: integration.subject };
|
|
99526
|
-
}
|
|
99527
|
-
function buildAssessmentActivityData(input) {
|
|
99528
|
-
if (!input.course) {
|
|
99529
|
-
return;
|
|
99530
|
-
}
|
|
99531
|
-
return {
|
|
99532
|
-
activityId: input.activityId,
|
|
99533
|
-
activityName: input.activityName,
|
|
99534
|
-
...input.course,
|
|
99535
|
-
courseId: input.courseId
|
|
99536
|
-
};
|
|
99537
|
-
}
|
|
99538
|
-
function buildDiagnosticAssessmentSnapshot(input) {
|
|
99539
|
-
return {
|
|
99540
|
-
attemptId: input.attemptId,
|
|
99541
|
-
responseVersion: input.metadata.responseVersion,
|
|
99542
|
-
status: "in_progress",
|
|
99543
|
-
flow: "platform-routed-item-submit",
|
|
99544
|
-
...input.activityData ? { activityData: input.activityData } : {},
|
|
99545
|
-
assessment: assessmentPresentationForAttempt(input.assessment, input.attemptId),
|
|
99546
|
-
selection: {
|
|
99547
|
-
kind: "platform-routed-diagnostic",
|
|
99548
|
-
purpose: "diagnostic",
|
|
99549
|
-
definitionId: input.metadata.diagnostic.definitionId,
|
|
99550
|
-
diagnosticKey: input.metadata.diagnostic.diagnosticKey,
|
|
99551
|
-
routingRevision: input.metadata.diagnostic.routingRevision
|
|
99552
|
-
},
|
|
99553
|
-
routing: projectDiagnosticRoutingSnapshot(input.metadata.diagnostic.routingRevision, input.state),
|
|
99554
|
-
completion: null
|
|
99555
|
-
};
|
|
99556
|
-
}
|
|
99557
|
-
function buildConventionalAssessmentSnapshot(input) {
|
|
99558
|
-
const metadata2 = input.metadata;
|
|
99559
|
-
let selection;
|
|
99560
|
-
if (metadata2.purpose === "review") {
|
|
99561
|
-
selection = {
|
|
99562
|
-
kind: "standards-review",
|
|
99563
|
-
purpose: metadata2.purpose,
|
|
99564
|
-
standards: [...metadata2.review.standards],
|
|
99565
|
-
candidateItemsPerStandard: metadata2.review.candidateItemsPerStandard,
|
|
99566
|
-
selections: [...metadata2.review.selections],
|
|
99567
|
-
fulfillment: metadata2.review.fulfillment
|
|
99568
|
-
};
|
|
99569
|
-
} else if (metadata2.purpose === "mastery") {
|
|
99570
|
-
selection = {
|
|
99571
|
-
kind: "standard-quiz",
|
|
99572
|
-
purpose: metadata2.purpose,
|
|
99573
|
-
standard: metadata2.mastery.standard
|
|
99574
|
-
};
|
|
99575
|
-
} else {
|
|
99576
|
-
selection = { kind: "fixed-test", purpose: metadata2.purpose };
|
|
99577
|
-
}
|
|
99578
|
-
return {
|
|
99579
|
-
attemptId: input.attemptId,
|
|
99580
|
-
responseVersion: metadata2.responseVersion,
|
|
99581
|
-
status: "in_progress",
|
|
99582
|
-
flow: assessmentFlowForPurpose(metadata2.purpose),
|
|
99583
|
-
...input.activityData ? { activityData: input.activityData } : {},
|
|
99584
|
-
assessment: assessmentPresentationForAttempt(input.assessment, input.attemptId),
|
|
99585
|
-
responses: metadata2.responses,
|
|
99586
|
-
itemSubmissions: metadata2.itemSubmissions,
|
|
99587
|
-
score: null,
|
|
99588
|
-
selection
|
|
99589
|
-
};
|
|
99590
|
-
}
|
|
99591
|
-
function buildAssessmentAwardRecord(input, mastery) {
|
|
99592
|
-
return {
|
|
99593
|
-
xpAwarded: input.xpAwarded,
|
|
99594
|
-
...input.masteredUnits !== undefined ? { masteredUnits: input.masteredUnits } : {},
|
|
99595
|
-
...input.masteredUnitsAbsolute !== undefined ? { masteredUnitsAbsolute: input.masteredUnitsAbsolute } : {},
|
|
99596
|
-
masteredUnitsApplied: mastery.masteredUnitsApplied,
|
|
99597
|
-
...mastery.pctCompleteApp !== undefined ? { pctCompleteApp: mastery.pctCompleteApp } : {},
|
|
99598
|
-
...mastery.warnings ? { warnings: mastery.warnings } : {}
|
|
99599
|
-
};
|
|
99600
|
-
}
|
|
99601
|
-
function stageAssessmentAttemptSupersession(attempt) {
|
|
99602
|
-
Object.assign(attempt.result, ASSESSMENT_ATTEMPT_SUPERSEDED);
|
|
99603
|
-
Object.assign(attempt.selection, ASSESSMENT_ATTEMPT_SUPERSEDED);
|
|
99604
|
-
}
|
|
99605
|
-
function shouldConfirmAssessmentVersionUnavailable(details) {
|
|
99606
|
-
return typeof details === "object" && details !== null && "expectedRevision" in details && typeof details.expectedRevision === "string" && !("currentRevision" in details);
|
|
99607
|
-
}
|
|
99608
|
-
function buildAssessmentAttemptSupersessionUpdate(result) {
|
|
99609
|
-
if (result.status !== "active" || !isAssessmentAttemptOpen({
|
|
99610
|
-
inProgress: result.inProgress ?? "",
|
|
99611
|
-
scoreStatus: result.scoreStatus
|
|
99612
|
-
})) {
|
|
99613
|
-
return null;
|
|
99614
|
-
}
|
|
99615
|
-
return {
|
|
99616
|
-
status: "active",
|
|
99617
|
-
assessmentLineItem: result.assessmentLineItem,
|
|
99618
|
-
student: result.student,
|
|
99619
|
-
score: result.score ?? 0,
|
|
99620
|
-
scoreDate: result.scoreDate,
|
|
99621
|
-
...ASSESSMENT_ATTEMPT_SUPERSEDED,
|
|
99622
|
-
metadata: result.metadata ?? {}
|
|
99623
|
-
};
|
|
99624
|
-
}
|
|
99625
|
-
async function resolveAvailableAssessmentAttempt(initialDecision, select3, resume) {
|
|
99626
|
-
let decision = initialDecision;
|
|
99627
|
-
while (decision?.kind === "resume") {
|
|
99628
|
-
const snapshot = await resume(decision);
|
|
99629
|
-
if (snapshot !== null) {
|
|
99630
|
-
return { kind: "resumed", snapshot };
|
|
99631
|
-
}
|
|
99632
|
-
decision = select3();
|
|
99633
|
-
}
|
|
99634
|
-
return { kind: "continue", decision };
|
|
99635
|
-
}
|
|
99636
|
-
function buildReviewItemResultMetadata(input) {
|
|
99637
|
-
const administration = itemAdministration(input.metadata.itemSubmissions, input.selection.itemIdentifier);
|
|
99638
|
-
if (!administration) {
|
|
99639
|
-
throw new Error(`Cannot create a review result for unadministered candidate ${input.selection.itemIdentifier}`);
|
|
99640
|
-
}
|
|
99641
|
-
return {
|
|
99642
|
-
version: PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_VERSION,
|
|
99643
|
-
parentAttemptId: input.parentAttemptId,
|
|
99644
|
-
activityId: input.metadata.activityId,
|
|
99645
|
-
integrationId: input.metadata.integrationId,
|
|
99646
|
-
enrollmentId: input.metadata.enrollmentId,
|
|
99647
|
-
bankRevision: input.metadata.review.bankRevision,
|
|
99648
|
-
itemIdentifier: input.selection.itemIdentifier,
|
|
99649
|
-
standard: input.selection.standard,
|
|
99650
|
-
deliveredAt: administration.submittedAt,
|
|
99651
|
-
responseVersion: input.responseVersion ?? input.metadata.responseVersion,
|
|
99652
|
-
responses: input.metadata.responses[input.selection.itemIdentifier] ?? {},
|
|
99653
|
-
...input.submissionId ? { submissionId: input.submissionId } : {},
|
|
99654
|
-
...input.isCorrect === undefined ? {} : { isCorrect: input.isCorrect }
|
|
99655
|
-
};
|
|
99656
|
-
}
|
|
99657
|
-
function buildOpenReviewChildResult(input) {
|
|
99658
|
-
return {
|
|
99659
|
-
status: "active",
|
|
99660
|
-
assessmentLineItem: { sourcedId: input.childLineItemId },
|
|
99661
|
-
student: input.student,
|
|
99662
|
-
score: 0,
|
|
99663
|
-
scoreDate: input.metadata.deliveredAt,
|
|
99664
|
-
...ASSESSMENT_ATTEMPT_OPEN,
|
|
99665
|
-
metadata: {
|
|
99666
|
-
[PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY]: input.metadata
|
|
99667
|
-
}
|
|
99668
|
-
};
|
|
99669
|
-
}
|
|
99670
|
-
function diagnosticQuestionLineItemId(parentLineItemId, itemIdentifier) {
|
|
99671
|
-
return deterministicUUID(["playcademy:diagnostic-question-line-item:v1", parentLineItemId, itemIdentifier].join("\x00"));
|
|
99672
|
-
}
|
|
99673
|
-
function diagnosticQuestionResultId(parentAttemptId, childLineItemId) {
|
|
99674
|
-
return deterministicUUID(["playcademy:diagnostic-question-result:v1", parentAttemptId, childLineItemId].join("\x00"));
|
|
99675
|
-
}
|
|
99676
|
-
function diagnosticAdministration(input) {
|
|
99677
|
-
const ledgerIndex = input.metadata.diagnostic.ledger.findIndex((entry) => entry.routingNodeKey === input.ledgerEntry.routingNodeKey);
|
|
99678
|
-
const persistedEntry = input.metadata.diagnostic.ledger[ledgerIndex];
|
|
99679
|
-
const administration = input.metadata.itemSubmissions[ledgerIndex];
|
|
99680
|
-
if (ledgerIndex === -1 || !persistedEntry || !administration || persistedEntry.stageKey !== input.ledgerEntry.stageKey || persistedEntry.trackKey !== input.ledgerEntry.trackKey || persistedEntry.itemIdentifier !== input.ledgerEntry.itemIdentifier || persistedEntry.isCorrect !== input.ledgerEntry.isCorrect || administration.itemIdentifier !== input.ledgerEntry.itemIdentifier || administration.isCorrect !== input.ledgerEntry.isCorrect) {
|
|
99681
|
-
throw new Error(`Cannot create a diagnostic result for uncommitted routing node ${input.ledgerEntry.routingNodeKey}`);
|
|
99682
|
-
}
|
|
99683
|
-
return administration;
|
|
99684
|
-
}
|
|
99685
|
-
function buildDiagnosticItemResultMetadata(input) {
|
|
99686
|
-
const administration = diagnosticAdministration(input);
|
|
99687
|
-
return {
|
|
99688
|
-
version: PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_VERSION,
|
|
99689
|
-
parentAttemptId: input.parentAttemptId,
|
|
99690
|
-
activityId: input.metadata.activityId,
|
|
99691
|
-
integrationId: input.metadata.integrationId,
|
|
99692
|
-
enrollmentId: input.metadata.enrollmentId,
|
|
99693
|
-
definitionId: input.metadata.diagnostic.definitionId,
|
|
99694
|
-
diagnosticKey: input.metadata.diagnostic.diagnosticKey,
|
|
99695
|
-
routingRevision: input.metadata.diagnostic.routingRevision,
|
|
99696
|
-
contentRevision: input.metadata.selectedTest.contentRevision,
|
|
99697
|
-
stageKey: input.ledgerEntry.stageKey,
|
|
99698
|
-
trackKey: input.ledgerEntry.trackKey,
|
|
99699
|
-
routingNodeKey: input.ledgerEntry.routingNodeKey,
|
|
99700
|
-
itemIdentifier: input.ledgerEntry.itemIdentifier,
|
|
99701
|
-
deliveredAt: administration.submittedAt,
|
|
99702
|
-
responseVersion: administration.responseVersion,
|
|
99703
|
-
responses: input.metadata.responses[input.ledgerEntry.itemIdentifier] ?? {},
|
|
99704
|
-
submissionId: administration.submissionId,
|
|
99705
|
-
isCorrect: input.ledgerEntry.isCorrect
|
|
99706
|
-
};
|
|
99707
|
-
}
|
|
99708
|
-
function resolveDiagnosticChildResult(input) {
|
|
99709
|
-
if (!input.childResult) {
|
|
99710
|
-
return { action: "create" };
|
|
99711
|
-
}
|
|
99712
|
-
const childMetadata = playcademyDiagnosticAssessmentItemResultMetadata(input.childResult.metadata);
|
|
99713
|
-
const expectedMetadata = buildDiagnosticItemResultMetadata({
|
|
99714
|
-
metadata: input.parentMetadata,
|
|
99715
|
-
ledgerEntry: input.ledgerEntry,
|
|
99716
|
-
parentAttemptId: input.parentResult.sourcedId
|
|
99717
|
-
});
|
|
99718
|
-
if (input.childResult.assessmentLineItem.sourcedId !== input.childLineItemId || input.childResult.student.sourcedId !== input.parentResult.student.sourcedId || !childMetadata || canonicalJson2(childMetadata) !== canonicalJson2(expectedMetadata)) {
|
|
99719
|
-
return { action: "restore", reason: "incompatible" };
|
|
99720
|
-
}
|
|
99721
|
-
return input.childResult.status === "active" ? { action: "reuse", metadata: expectedMetadata } : { action: "restore", reason: "inactive" };
|
|
99722
|
-
}
|
|
99723
|
-
function buildFinalizedDiagnosticChildResult(input) {
|
|
99724
|
-
const administration = diagnosticAdministration(input);
|
|
99725
|
-
const metadata2 = buildDiagnosticItemResultMetadata(input);
|
|
99726
|
-
return {
|
|
99727
|
-
resultUpdate: {
|
|
99728
|
-
status: "active",
|
|
99729
|
-
assessmentLineItem: { sourcedId: input.childLineItemId },
|
|
99730
|
-
student: input.student,
|
|
99731
|
-
score: administration.score.earned,
|
|
99732
|
-
scoreDate: administration.submittedAt,
|
|
99733
|
-
...ASSESSMENT_ATTEMPT_COMPLETED,
|
|
99734
|
-
metadata: {
|
|
99735
|
-
[PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY]: metadata2
|
|
99736
|
-
}
|
|
99737
|
-
}
|
|
99738
|
-
};
|
|
99739
|
-
}
|
|
99740
|
-
function buildFinalizedReviewChildResult(input) {
|
|
99741
|
-
const metadata2 = buildReviewItemResultMetadata({
|
|
99742
|
-
metadata: input.metadata,
|
|
99743
|
-
selection: input.selection,
|
|
99744
|
-
parentAttemptId: input.parentAttemptId,
|
|
99745
|
-
submissionId: input.submissionId,
|
|
99746
|
-
isCorrect: input.itemResult.isCorrect
|
|
99747
|
-
});
|
|
99748
|
-
return {
|
|
99749
|
-
resultUpdate: {
|
|
99750
|
-
status: "active",
|
|
99751
|
-
assessmentLineItem: { sourcedId: input.childLineItemId },
|
|
99752
|
-
student: input.student,
|
|
99753
|
-
score: input.itemResult.score.earned,
|
|
99754
|
-
scoreDate: input.timestamp,
|
|
99755
|
-
...ASSESSMENT_ATTEMPT_COMPLETED,
|
|
99756
|
-
metadata: {
|
|
99757
|
-
[PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY]: metadata2
|
|
99758
|
-
}
|
|
99759
|
-
},
|
|
99760
|
-
outcome: {
|
|
99761
|
-
itemIdentifier: input.selection.itemIdentifier,
|
|
99762
|
-
standard: input.selection.standard,
|
|
99763
|
-
answered: Object.keys(input.metadata.responses[input.selection.itemIdentifier] ?? {}).length > 0,
|
|
99764
|
-
score: input.itemResult.score,
|
|
99765
|
-
isCorrect: input.itemResult.isCorrect
|
|
99766
|
-
}
|
|
99767
|
-
};
|
|
99768
|
-
}
|
|
99769
|
-
function resolveReviewChildResult(input) {
|
|
99770
|
-
if (!input.childResult) {
|
|
99771
|
-
return { action: "create" };
|
|
99772
|
-
}
|
|
99773
|
-
const childMetadata = playcademyAssessmentItemResultMetadata(input.childResult.metadata);
|
|
99774
|
-
const expectedResponses = input.parentMetadata.responses[input.selection.itemIdentifier] ?? {};
|
|
99775
|
-
const administration = itemAdministration(input.parentMetadata.itemSubmissions, input.selection.itemIdentifier);
|
|
99776
|
-
if (input.childResult.assessmentLineItem.sourcedId !== input.childLineItemId || input.childResult.student.sourcedId !== input.parentResult.student.sourcedId || !childMetadata || childMetadata.parentAttemptId !== input.parentResult.sourcedId || childMetadata.activityId !== input.parentMetadata.activityId || childMetadata.integrationId !== input.parentMetadata.integrationId || childMetadata.enrollmentId !== input.parentMetadata.enrollmentId || childMetadata.bankRevision !== input.parentMetadata.review.bankRevision || childMetadata.itemIdentifier !== input.selection.itemIdentifier || childMetadata.standard.framework !== input.selection.standard.framework || childMetadata.standard.identifier !== input.selection.standard.identifier || childMetadata.deliveredAt !== administration?.submittedAt || childMetadata.responseVersion > input.parentMetadata.responseVersion || canonicalJson2(childMetadata.responses) !== canonicalJson2(expectedResponses)) {
|
|
99777
|
-
return { action: "restore", reason: "incompatible" };
|
|
99778
|
-
}
|
|
99779
|
-
return input.childResult.status === "active" ? { action: "reuse", metadata: childMetadata } : { action: "restore", reason: "inactive" };
|
|
99780
|
-
}
|
|
99781
|
-
function buildAssessmentSubmitResult(input) {
|
|
99782
|
-
if (!input.metadata.score || !input.metadata.submissionId) {
|
|
99783
|
-
return null;
|
|
99784
|
-
}
|
|
99785
|
-
if (input.metadata.purpose === "diagnostic" && input.metadata.diagnostic) {
|
|
99786
|
-
const tracks = input.metadata.completion?.diagnosticTracks;
|
|
99787
|
-
return tracks ? {
|
|
99788
|
-
purpose: input.metadata.purpose,
|
|
99789
|
-
attemptId: input.attemptId,
|
|
99790
|
-
submissionId: input.metadata.submissionId,
|
|
99791
|
-
diagnosticKey: input.metadata.diagnostic.diagnosticKey,
|
|
99792
|
-
responseVersion: input.metadata.responseVersion,
|
|
99793
|
-
status: "awaiting_award",
|
|
99794
|
-
tracks
|
|
99795
|
-
} : null;
|
|
99796
|
-
}
|
|
99797
|
-
const base = {
|
|
99798
|
-
attemptId: input.attemptId,
|
|
99799
|
-
submissionId: input.metadata.submissionId,
|
|
99800
|
-
responseVersion: input.metadata.responseVersion,
|
|
99801
|
-
status: "awaiting_award",
|
|
99802
|
-
responses: input.metadata.responses,
|
|
99803
|
-
score: input.metadata.score
|
|
99804
|
-
};
|
|
99805
|
-
if (input.metadata.purpose === "review") {
|
|
99806
|
-
const itemOutcomes = input.metadata.completion?.itemOutcomes;
|
|
99807
|
-
return itemOutcomes ? { ...base, purpose: input.metadata.purpose, itemOutcomes } : null;
|
|
99808
|
-
}
|
|
99809
|
-
return { ...base, purpose: input.metadata.purpose };
|
|
99810
|
-
}
|
|
99811
|
-
function assessmentAwardFromResult(result, metadata2) {
|
|
99812
|
-
if (metadata2.award) {
|
|
99813
|
-
return metadata2.award;
|
|
99814
|
-
}
|
|
99815
|
-
const xp = result.metadata?.xp;
|
|
99816
|
-
return typeof xp === "number" && Number.isFinite(xp) && xp >= 0 ? { xpAwarded: xp, masteredUnitsApplied: 0 } : null;
|
|
99817
|
-
}
|
|
99818
|
-
function buildCompletedAssessmentSubmitResult(input) {
|
|
99819
|
-
const attemptId = input.result.sourcedId;
|
|
99820
|
-
const award = assessmentAwardFromResult(input.result, input.metadata);
|
|
99821
|
-
if (award === null) {
|
|
99822
|
-
throw new GoneError("This completed assessment is missing its recorded XP.", {
|
|
99823
|
-
attemptId
|
|
99824
|
-
});
|
|
99825
|
-
}
|
|
99826
|
-
const submitted = buildAssessmentSubmitResult({ attemptId, metadata: input.metadata });
|
|
99827
|
-
if (!submitted) {
|
|
99828
|
-
throw new GoneError("This completed assessment has incomplete persisted results.", {
|
|
99829
|
-
attemptId
|
|
99830
|
-
});
|
|
99831
|
-
}
|
|
99832
|
-
return {
|
|
99833
|
-
...submitted,
|
|
99834
|
-
status: "completed",
|
|
99835
|
-
xpAwarded: award.xpAwarded,
|
|
99836
|
-
masteredUnitsApplied: award.masteredUnitsApplied,
|
|
99837
|
-
...award.pctCompleteApp !== undefined ? { pctCompleteApp: award.pctCompleteApp } : {},
|
|
99838
|
-
...award.warnings ? { warnings: award.warnings } : {}
|
|
99839
|
-
};
|
|
99840
|
-
}
|
|
99841
|
-
function assessmentAttemptId(input) {
|
|
99842
|
-
return deterministicUUID([
|
|
99843
|
-
"playcademy:assessment-attempt",
|
|
99844
|
-
input.gameId,
|
|
99845
|
-
input.studentId,
|
|
99846
|
-
input.courseId,
|
|
99847
|
-
input.activityId,
|
|
99848
|
-
input.purpose,
|
|
99849
|
-
String(input.attemptNumber)
|
|
99850
|
-
].join("\x00"));
|
|
99851
|
-
}
|
|
99852
|
-
async function findLatestAssessmentResult(results, match) {
|
|
99853
|
-
let latest = null;
|
|
99854
|
-
let latestTimestamp = 0;
|
|
99855
|
-
for await (const result of results) {
|
|
99856
|
-
const resultTimestamp = assessmentAttemptTimestamp(result.scoreDate);
|
|
99857
|
-
if (latest && resultTimestamp < latestTimestamp) {
|
|
99858
|
-
break;
|
|
99859
|
-
}
|
|
99860
|
-
const matched = match(result);
|
|
99861
|
-
if (matched !== null && (!latest || compareNewestCompletedAssessmentAttempt({ attemptId: result.sourcedId, scoreDate: result.scoreDate }, {
|
|
99862
|
-
attemptId: latest.result.sourcedId,
|
|
99863
|
-
scoreDate: latest.result.scoreDate
|
|
99864
|
-
}) < 0)) {
|
|
99865
|
-
latest = { result, match: matched };
|
|
99866
|
-
latestTimestamp = resultTimestamp;
|
|
99867
|
-
}
|
|
99868
|
-
}
|
|
99869
|
-
return latest;
|
|
99870
|
-
}
|
|
99871
|
-
function playableChoice(choice) {
|
|
99872
|
-
return {
|
|
99873
|
-
identifier: choice.identifier,
|
|
99874
|
-
content: choice.content,
|
|
99875
|
-
...choice.contentNodes ? { contentNodes: choice.contentNodes } : {}
|
|
99876
|
-
};
|
|
99877
|
-
}
|
|
99878
|
-
function playableAssociableChoice(choice) {
|
|
99879
|
-
const matchMax = qtiAttributeCapacity(choice.attributes, "match-max", 0);
|
|
99880
|
-
if (matchMax === null) {
|
|
99881
|
-
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", "The selected assessment contains an invalid Match capacity.");
|
|
99882
|
-
}
|
|
99883
|
-
return {
|
|
99884
|
-
...playableChoice(choice),
|
|
99885
|
-
matchMax
|
|
99886
|
-
};
|
|
99887
|
-
}
|
|
99888
|
-
function requireChoices2(choices) {
|
|
99889
|
-
if (choices.length === 0 || choices.some((candidate) => !candidate.identifier)) {
|
|
99890
|
-
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", "The selected assessment contains an interaction Playcademy cannot render.");
|
|
99891
|
-
}
|
|
99892
|
-
return choices;
|
|
99893
|
-
}
|
|
99894
|
-
function isPlayableAssessmentShape(value) {
|
|
99895
|
-
return PLAYABLE_SHAPE_SET.has(value);
|
|
99896
|
-
}
|
|
99897
|
-
function playableGraphic(graphic) {
|
|
99898
|
-
return {
|
|
99899
|
-
src: graphic.src,
|
|
99900
|
-
...graphic.width === undefined ? {} : { width: graphic.width },
|
|
99901
|
-
...graphic.height === undefined ? {} : { height: graphic.height },
|
|
99902
|
-
...graphic.description ? { description: graphic.description } : {}
|
|
99903
|
-
};
|
|
99904
|
-
}
|
|
99905
|
-
function requireGraphic(questionIdentifier, graphic) {
|
|
99906
|
-
if (!graphic) {
|
|
99907
|
-
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${questionIdentifier} is missing a required graphic.`);
|
|
99908
|
-
}
|
|
99909
|
-
return playableGraphic(graphic);
|
|
99910
|
-
}
|
|
99911
|
-
function requireHotspots(questionIdentifier, hotspots) {
|
|
99912
|
-
if (hotspots.length === 0) {
|
|
99913
|
-
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${questionIdentifier} declares no graphic regions.`);
|
|
99914
|
-
}
|
|
99915
|
-
return hotspots.map((hotspot) => ({
|
|
99916
|
-
identifier: hotspot.identifier,
|
|
99917
|
-
shape: isPlayableAssessmentShape(hotspot.shape) ? hotspot.shape : "default",
|
|
99918
|
-
coords: [...hotspot.coords],
|
|
99919
|
-
...hotspot.matchMax === undefined ? {} : { matchMax: hotspot.matchMax },
|
|
99920
|
-
...hotspot.description ? { description: hotspot.description } : {}
|
|
99921
|
-
}));
|
|
99922
|
-
}
|
|
99923
|
-
function parseInteraction2(questionIdentifier, interaction) {
|
|
99924
|
-
const { type } = interaction;
|
|
99925
|
-
const responseIdentifier = interaction.attributes["response-identifier"];
|
|
99926
|
-
if (!responseIdentifier) {
|
|
99927
|
-
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${questionIdentifier} has no response identifier.`);
|
|
99928
|
-
}
|
|
99929
|
-
const prompt = interaction.embeddedPrompt ? { prompt: interaction.embeddedPrompt } : {};
|
|
99930
|
-
if (type === "choice") {
|
|
99931
|
-
const choices = requireChoices2(interaction.choices.map(playableChoice));
|
|
99932
|
-
return {
|
|
99933
|
-
type,
|
|
99934
|
-
responseIdentifier,
|
|
99935
|
-
...prompt,
|
|
99936
|
-
choices,
|
|
99937
|
-
minChoices: qtiAttributeNumber(interaction.attributes, "min-choices", 1),
|
|
99938
|
-
maxChoices: qtiAttributeMaximum(interaction.attributes, "max-choices", 1, choices.length)
|
|
99939
|
-
};
|
|
99940
|
-
}
|
|
99941
|
-
if (type === "inline-choice") {
|
|
99942
|
-
return {
|
|
99943
|
-
type,
|
|
99944
|
-
responseIdentifier,
|
|
99945
|
-
...prompt,
|
|
99946
|
-
choices: requireChoices2(interaction.choices.map(playableChoice))
|
|
99947
|
-
};
|
|
99948
|
-
}
|
|
99949
|
-
if (type === "text-entry") {
|
|
99950
|
-
const expectedLength = qtiAttributeNumber(interaction.attributes, "expected-length", 0);
|
|
99951
|
-
return {
|
|
99952
|
-
type,
|
|
99953
|
-
responseIdentifier,
|
|
99954
|
-
...prompt,
|
|
99955
|
-
...Number.isInteger(expectedLength) && expectedLength > 0 ? { expectedLength } : {},
|
|
99956
|
-
...interaction.attributes.placeholder ? { placeholder: interaction.attributes.placeholder } : {}
|
|
99957
|
-
};
|
|
99958
|
-
}
|
|
99959
|
-
if (type === "order") {
|
|
99960
|
-
return {
|
|
99961
|
-
type,
|
|
99962
|
-
responseIdentifier,
|
|
99963
|
-
...prompt,
|
|
99964
|
-
choices: requireChoices2(interaction.choices.map(playableChoice)),
|
|
99965
|
-
shuffle: qtiAttributeBoolean(interaction.attributes, "shuffle")
|
|
99966
|
-
};
|
|
99967
|
-
}
|
|
99968
|
-
if (type === "match") {
|
|
99969
|
-
const sets = interaction.choiceSets;
|
|
99970
|
-
if (sets.length !== 2) {
|
|
99971
|
-
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", "Match interactions require exactly two choice sets.");
|
|
99972
|
-
}
|
|
99973
|
-
const sourceChoices = requireChoices2(sets[0].map(playableAssociableChoice));
|
|
99974
|
-
const targetChoices = requireChoices2(sets[1].map(playableAssociableChoice));
|
|
99975
|
-
return {
|
|
99976
|
-
type,
|
|
99977
|
-
responseIdentifier,
|
|
99978
|
-
...prompt,
|
|
99979
|
-
sourceChoices,
|
|
99980
|
-
targetChoices,
|
|
99981
|
-
maxAssociations: qtiAttributeMaximum(interaction.attributes, "max-associations", 1, sourceChoices.length * targetChoices.length),
|
|
99982
|
-
shuffle: qtiAttributeBoolean(interaction.attributes, "shuffle")
|
|
99983
|
-
};
|
|
99984
|
-
}
|
|
99985
|
-
if (type === "hotspot") {
|
|
99986
|
-
const image = requireGraphic(questionIdentifier, interaction.graphic);
|
|
99987
|
-
const hotspots = requireHotspots(questionIdentifier, interaction.hotspots);
|
|
99988
|
-
return {
|
|
99989
|
-
type,
|
|
99990
|
-
responseIdentifier,
|
|
99991
|
-
...prompt,
|
|
99992
|
-
image,
|
|
99993
|
-
hotspots,
|
|
99994
|
-
minChoices: qtiAttributeNumber(interaction.attributes, "min-choices", 1),
|
|
99995
|
-
maxChoices: qtiAttributeMaximum(interaction.attributes, "max-choices", 1, hotspots.length)
|
|
99996
|
-
};
|
|
99997
|
-
}
|
|
99998
|
-
if (type === "gap-match") {
|
|
99999
|
-
if (interaction.gapTexts.length === 0 || interaction.gaps.length === 0 || interaction.textWithGaps === undefined || !interaction.gapContent || !qtiGapContentMatchesGaps(interaction.gapContent, interaction.gaps)) {
|
|
100000
|
-
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${questionIdentifier} has an incomplete gap-match interaction.`);
|
|
100001
|
-
}
|
|
100002
|
-
return {
|
|
100003
|
-
type,
|
|
100004
|
-
responseIdentifier,
|
|
100005
|
-
...prompt,
|
|
100006
|
-
content: interaction.textWithGaps,
|
|
100007
|
-
contentNodes: interaction.gapContent,
|
|
100008
|
-
gapTexts: interaction.gapTexts.map((gapText) => ({
|
|
100009
|
-
identifier: gapText.identifier,
|
|
100010
|
-
content: gapText.content,
|
|
100011
|
-
matchMax: gapText.matchMax
|
|
100012
|
-
})),
|
|
100013
|
-
gaps: [...interaction.gaps],
|
|
100014
|
-
maxAssociations: qtiAttributeNumber(interaction.attributes, "max-associations", 0)
|
|
100015
|
-
};
|
|
100016
|
-
}
|
|
100017
|
-
if (type === "graphic-gap-match") {
|
|
100018
|
-
const image = requireGraphic(questionIdentifier, interaction.graphic);
|
|
100019
|
-
const hotspots = requireHotspots(questionIdentifier, interaction.hotspots);
|
|
100020
|
-
if (interaction.gapImages.length === 0) {
|
|
100021
|
-
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${questionIdentifier} has no draggable gap images.`);
|
|
100022
|
-
}
|
|
100023
|
-
return {
|
|
100024
|
-
type,
|
|
100025
|
-
responseIdentifier,
|
|
100026
|
-
...prompt,
|
|
100027
|
-
image,
|
|
100028
|
-
gapImages: interaction.gapImages.map((gapImage) => ({
|
|
100029
|
-
identifier: gapImage.identifier,
|
|
100030
|
-
...gapImage.graphic ? { image: playableGraphic(gapImage.graphic) } : {},
|
|
100031
|
-
matchMax: gapImage.matchMax,
|
|
100032
|
-
...gapImage.description ? { description: gapImage.description } : {}
|
|
100033
|
-
})),
|
|
100034
|
-
hotspots,
|
|
100035
|
-
maxAssociations: qtiAttributeNumber(interaction.attributes, "max-associations", 0)
|
|
100036
|
-
};
|
|
100037
|
-
}
|
|
100038
|
-
if (type === "select-point") {
|
|
100039
|
-
return {
|
|
100040
|
-
type,
|
|
100041
|
-
responseIdentifier,
|
|
100042
|
-
...prompt,
|
|
100043
|
-
image: requireGraphic(questionIdentifier, interaction.graphic),
|
|
100044
|
-
minChoices: qtiAttributeNumber(interaction.attributes, "min-choices", 0),
|
|
100045
|
-
maxChoices: qtiAttributeNumber(interaction.attributes, "max-choices", 0)
|
|
100046
|
-
};
|
|
100047
|
-
}
|
|
100048
|
-
if (type === "graphic-associate") {
|
|
100049
|
-
return {
|
|
100050
|
-
type,
|
|
100051
|
-
responseIdentifier,
|
|
100052
|
-
...prompt,
|
|
100053
|
-
image: requireGraphic(questionIdentifier, interaction.graphic),
|
|
100054
|
-
hotspots: requireHotspots(questionIdentifier, interaction.hotspots),
|
|
100055
|
-
maxAssociations: qtiAttributeNumber(interaction.attributes, "max-associations", 1)
|
|
100056
|
-
};
|
|
100057
|
-
}
|
|
100058
|
-
if (type === "position-object") {
|
|
100059
|
-
return {
|
|
100060
|
-
type,
|
|
100061
|
-
responseIdentifier,
|
|
100062
|
-
...prompt,
|
|
100063
|
-
stage: requireGraphic(questionIdentifier, interaction.graphic),
|
|
100064
|
-
object: requireGraphic(questionIdentifier, interaction.objectGraphic),
|
|
100065
|
-
maxPlacements: qtiAttributeNumber(interaction.attributes, "max-choices", 1)
|
|
100066
|
-
};
|
|
100067
|
-
}
|
|
100068
|
-
if (type === "hottext") {
|
|
100069
|
-
const segments = interaction.textSegments;
|
|
100070
|
-
if (!segments.some((segment) => segment.selectable) || segments.some((segment) => segment.selectable && (!segment.identifier || !segment.content))) {
|
|
100071
|
-
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", "Hottext interactions require identified selectable content.");
|
|
100072
|
-
}
|
|
100073
|
-
return {
|
|
100074
|
-
type,
|
|
100075
|
-
responseIdentifier,
|
|
100076
|
-
...prompt,
|
|
100077
|
-
segments,
|
|
100078
|
-
maxChoices: qtiAttributeMaximum(interaction.attributes, "max-choices", 1, segments.filter((segment) => segment.selectable).length)
|
|
100079
|
-
};
|
|
100080
|
-
}
|
|
100081
|
-
const unsupported = type;
|
|
100082
|
-
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${questionIdentifier} uses unsupported interaction ${unsupported}.`);
|
|
100083
|
-
}
|
|
100084
|
-
function normalizedQtiItem(question) {
|
|
100085
|
-
if (!question.rawXml) {
|
|
100086
|
-
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${question.identifier} has no authoritative QTI XML.`);
|
|
100087
|
-
}
|
|
100088
|
-
try {
|
|
100089
|
-
return parseQtiItemXml(question.rawXml);
|
|
100090
|
-
} catch (error88) {
|
|
100091
|
-
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${question.identifier} contains invalid QTI XML.`, { reason: errorMessage(error88) });
|
|
100092
|
-
}
|
|
100093
|
-
}
|
|
100094
|
-
function parsePlayableItem(question) {
|
|
100095
|
-
const parsed = normalizedQtiItem(question);
|
|
100096
|
-
const interactions = playableQtiInteractions(parsed);
|
|
100097
|
-
if (!interactions) {
|
|
100098
|
-
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${question.identifier} must contain only supported interactions with unique response identifiers.`);
|
|
100099
|
-
}
|
|
100100
|
-
return {
|
|
100101
|
-
identifier: question.identifier,
|
|
100102
|
-
title: question.title,
|
|
100103
|
-
prompt: parsed.bodyTextWithBlanks,
|
|
100104
|
-
...parsed.bodyContent ? { promptContent: parsed.bodyContent } : {},
|
|
100105
|
-
maxScore: qtiItemMaxScore(parsed),
|
|
100106
|
-
interactions: interactions.map((interaction) => parseInteraction2(question.identifier, interaction))
|
|
100107
|
-
};
|
|
100108
|
-
}
|
|
100109
|
-
function assessmentContentRevision(test, questions) {
|
|
100110
|
-
return sha256Hex(canonicalJson2({
|
|
100111
|
-
identifier: test.identifier,
|
|
100112
|
-
title: test.title,
|
|
100113
|
-
qtiVersion: test.qtiVersion,
|
|
100114
|
-
parts: test["qti-test-part"],
|
|
100115
|
-
questions: questions.questions.map(({ reference, question }) => ({
|
|
100116
|
-
identifier: reference.identifier,
|
|
100117
|
-
rawXml: question.rawXml
|
|
100118
|
-
}))
|
|
100119
|
-
}));
|
|
100120
|
-
}
|
|
100121
|
-
async function buildPlayableAssessment(test, questions) {
|
|
100122
|
-
return {
|
|
100123
|
-
identifier: test.identifier,
|
|
100124
|
-
contractVersion: PLAYCADEMY_ASSESSMENT_CONTRACT_VERSION,
|
|
100125
|
-
contentRevision: await assessmentContentRevision(test, questions),
|
|
100126
|
-
title: test.title,
|
|
100127
|
-
items: questions.questions.map(({ question }) => parsePlayableItem(question))
|
|
100128
|
-
};
|
|
100129
|
-
}
|
|
100130
|
-
function assessmentFixtureScoringKeys(assessment, questions) {
|
|
100131
|
-
const questionByIdentifier = new Map(questions.questions.map(({ question }) => [question.identifier, question]));
|
|
100132
|
-
const correctResponses = {};
|
|
100133
|
-
const responseAreas = {};
|
|
100134
|
-
for (const item of assessment.items) {
|
|
100135
|
-
const question = questionByIdentifier.get(item.identifier);
|
|
100136
|
-
const normalizedDeclarations = question ? normalizedQtiItem(question).responseDeclarations : [];
|
|
100137
|
-
const itemResponses = {};
|
|
100138
|
-
for (const interaction of item.interactions) {
|
|
100139
|
-
const responseIdentifier = interaction.responseIdentifier;
|
|
100140
|
-
const declaration = question?.responseDeclarations?.find((candidate) => candidate.identifier === responseIdentifier);
|
|
100141
|
-
const declaredValues = declaration?.correctResponse?.value ?? [];
|
|
100142
|
-
const normalizedDeclaration = normalizedDeclarations.find((candidate) => candidate.identifier === responseIdentifier);
|
|
100143
|
-
const values = declaredValues.length > 0 ? declaredValues : normalizedDeclaration?.correctValues ?? [];
|
|
100144
|
-
const areas = interaction.type === "select-point" || interaction.type === "position-object" ? normalizedDeclaration?.areaMapping?.entries.filter((entry) => entry.value > 0 && isPlayableAssessmentShape(entry.shape)) : undefined;
|
|
100145
|
-
if (areas?.length) {
|
|
100146
|
-
(responseAreas[item.identifier] ??= {})[responseIdentifier] = areas.map((entry) => ({
|
|
100147
|
-
shape: entry.shape,
|
|
100148
|
-
coords: [...entry.coords],
|
|
100149
|
-
value: entry.value
|
|
100150
|
-
}));
|
|
100151
|
-
}
|
|
100152
|
-
if (values.length > 0) {
|
|
100153
|
-
const cardinality = declaration?.cardinality ?? normalizedDeclaration?.cardinality;
|
|
100154
|
-
itemResponses[responseIdentifier] = cardinality === "single" && values.length === 1 ? values[0] : values;
|
|
100155
|
-
} else if (!areas?.length) {
|
|
100156
|
-
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${item.identifier} has no fixture scoring key.`);
|
|
100157
|
-
}
|
|
100158
|
-
}
|
|
100159
|
-
correctResponses[item.identifier] = itemResponses;
|
|
100160
|
-
}
|
|
100161
|
-
return {
|
|
100162
|
-
correctResponses,
|
|
100163
|
-
...Object.keys(responseAreas).length > 0 ? { responseAreas } : {}
|
|
100164
|
-
};
|
|
100165
|
-
}
|
|
100166
|
-
function prepareDiagnosticAssessmentResponses(assessment, current, input) {
|
|
100167
|
-
const update2 = { [input.itemIdentifier]: input.responses };
|
|
100168
|
-
validateAssessmentResponseUpdate(assessment, update2);
|
|
100169
|
-
const responses = applyAssessmentResponseUpdate(current, update2);
|
|
100170
|
-
const item = assessment.items.find((candidate) => candidate.identifier === input.itemIdentifier);
|
|
100171
|
-
const itemResponses = responses[input.itemIdentifier];
|
|
100172
|
-
const missingResponse = item?.interactions.find((interaction) => itemResponses?.[interaction.responseIdentifier] === undefined);
|
|
100173
|
-
if (!item || !itemResponses || missingResponse) {
|
|
100174
|
-
throw new AssessmentRuntimeError("INVALID_RESPONSE", `Diagnostic item ${input.itemIdentifier} requires a response for every interaction.`, {
|
|
100175
|
-
itemIdentifier: input.itemIdentifier,
|
|
100176
|
-
...missingResponse ? { responseIdentifier: missingResponse.responseIdentifier } : {}
|
|
100177
|
-
});
|
|
100178
|
-
}
|
|
100179
|
-
validateAssessmentResponses(assessment, { [input.itemIdentifier]: itemResponses });
|
|
100180
|
-
return responses;
|
|
100181
|
-
}
|
|
100182
|
-
function validateAssessmentResponseUpdate(assessment, update2) {
|
|
100183
|
-
const message = assessmentResponseUpdateValidationMessage(assessment, update2);
|
|
100184
|
-
if (message) {
|
|
100185
|
-
throw new AssessmentRuntimeError("INVALID_RESPONSE", message);
|
|
100186
|
-
}
|
|
100187
|
-
}
|
|
100188
|
-
function validateAssessmentResponses(assessment, responses) {
|
|
100189
|
-
const message = assessmentResponseValidationMessage(assessment, responses);
|
|
100190
|
-
if (message) {
|
|
100191
|
-
throw new AssessmentRuntimeError("INVALID_RESPONSE", message);
|
|
100192
|
-
}
|
|
100193
|
-
}
|
|
100194
|
-
function validateAssessmentDraftResponses(assessment, responses) {
|
|
100195
|
-
const message = assessmentDraftResponseValidationMessage(assessment, responses);
|
|
100196
|
-
if (message) {
|
|
100197
|
-
throw new AssessmentRuntimeError("INVALID_RESPONSE", message);
|
|
100198
|
-
}
|
|
100199
|
-
}
|
|
100200
|
-
function assessmentScoringRequests(assessment, responses) {
|
|
100201
|
-
return assessment.items.map((item) => ({
|
|
100202
|
-
itemIdentifier: item.identifier,
|
|
100203
|
-
maxScore: item.maxScore,
|
|
100204
|
-
requests: item.interactions.map((interaction) => ({
|
|
100205
|
-
identifier: interaction.responseIdentifier,
|
|
100206
|
-
response: responses[item.identifier]?.[interaction.responseIdentifier] ?? ""
|
|
100207
|
-
}))
|
|
100208
|
-
}));
|
|
100209
|
-
}
|
|
100210
|
-
function buildAssessmentResultMetadata(input) {
|
|
100211
|
-
return {
|
|
100212
|
-
[PLAYCADEMY_ASSESSMENT_RESULT_METADATA_KEY]: input.metadata,
|
|
100213
|
-
assessmentPurpose: input.metadata.purpose,
|
|
100214
|
-
...assessmentPurposeClassificationFields2(input.metadata.purpose, input)
|
|
100215
|
-
};
|
|
100216
|
-
}
|
|
100217
|
-
function mergeAssessmentRuntimeMetadata(resultMetadata, metadata2) {
|
|
100218
|
-
return {
|
|
100219
|
-
...resultMetadata,
|
|
100220
|
-
[PLAYCADEMY_ASSESSMENT_RESULT_METADATA_KEY]: metadata2
|
|
100221
|
-
};
|
|
100222
|
-
}
|
|
100223
|
-
function aggregateAssessmentScore(itemScores, possible) {
|
|
100224
|
-
const earned = itemScores.reduce((sum, itemScore) => sum + (Number.isFinite(itemScore) ? itemScore : 0), 0);
|
|
100225
|
-
return {
|
|
100226
|
-
earned,
|
|
100227
|
-
possible,
|
|
100228
|
-
normalized: possible === 0 ? 0 : Math.min(1, Math.max(0, earned / possible))
|
|
100229
|
-
};
|
|
100230
|
-
}
|
|
100231
|
-
function countCorrectQuestions(verdicts) {
|
|
100232
|
-
return verdicts.filter((verdict) => verdict === true).length;
|
|
100233
|
-
}
|
|
100234
|
-
function assessmentScoreFromPercentage(percentage, possible) {
|
|
100235
|
-
const normalized = Math.min(1, Math.max(0, (percentage ?? 0) / 100));
|
|
100236
|
-
const earned = Math.round(normalized * possible * 1e6) / 1e6;
|
|
100237
|
-
return { earned, possible, normalized };
|
|
100238
|
-
}
|
|
100239
|
-
function buildAssessmentResultSubmission(input) {
|
|
100240
|
-
const metadata2 = {
|
|
100241
|
-
...input.metadata,
|
|
100242
|
-
submissionId: input.submissionId,
|
|
100243
|
-
score: input.score,
|
|
100244
|
-
completion: {
|
|
100245
|
-
testName: input.testName,
|
|
100246
|
-
courseName: input.courseName,
|
|
100247
|
-
totalQuestions: input.totalQuestions,
|
|
100248
|
-
correctQuestions: input.correctQuestions,
|
|
100249
|
-
...input.session ? { session: input.session } : {},
|
|
100250
|
-
...input.itemOutcomes ? { itemOutcomes: input.itemOutcomes } : {},
|
|
100251
|
-
...input.diagnosticTracks ? { diagnosticTracks: input.diagnosticTracks } : {}
|
|
100252
|
-
},
|
|
100253
|
-
updatedAt: input.timestamp
|
|
100254
|
-
};
|
|
100255
|
-
return {
|
|
100256
|
-
metadata: metadata2,
|
|
100257
|
-
resultUpdate: {
|
|
100258
|
-
status: input.result.status,
|
|
100259
|
-
assessmentLineItem: input.result.assessmentLineItem,
|
|
100260
|
-
student: input.result.student,
|
|
100261
|
-
score: input.score.normalized * 100,
|
|
100262
|
-
scoreDate: input.timestamp,
|
|
100263
|
-
...ASSESSMENT_ATTEMPT_AWAITING_AWARD,
|
|
100264
|
-
metadata: mergeAssessmentRuntimeMetadata(input.result.metadata, metadata2)
|
|
100265
|
-
}
|
|
100266
|
-
};
|
|
100267
|
-
}
|
|
100268
|
-
function buildAssessmentResultAwardFinalization(input) {
|
|
100269
|
-
const completion = input.metadata.completion;
|
|
100270
|
-
if (!completion || completion.totalQuestions === undefined) {
|
|
100271
|
-
throw new Error("A scored assessment must retain its completion facts before finalization");
|
|
100272
|
-
}
|
|
100273
|
-
const metadata2 = {
|
|
100274
|
-
...input.metadata,
|
|
100275
|
-
finalizedAt: input.timestamp,
|
|
100276
|
-
updatedAt: input.timestamp,
|
|
100277
|
-
award: input.award
|
|
100278
|
-
};
|
|
100279
|
-
const masteryRequested = input.award.masteredUnits !== undefined || input.award.masteredUnitsAbsolute !== undefined;
|
|
100280
|
-
return {
|
|
100281
|
-
metadata: metadata2,
|
|
100282
|
-
resultUpdate: {
|
|
100283
|
-
status: input.result.status,
|
|
100284
|
-
assessmentLineItem: input.result.assessmentLineItem,
|
|
100285
|
-
student: input.result.student,
|
|
100286
|
-
score: input.result.score ?? 0,
|
|
100287
|
-
scoreDate: input.result.scoreDate,
|
|
100288
|
-
...ASSESSMENT_ATTEMPT_COMPLETED,
|
|
100289
|
-
metadata: {
|
|
100290
|
-
...mergeAssessmentRuntimeMetadata(input.result.metadata, metadata2),
|
|
100291
|
-
xp: input.award.xpAwarded,
|
|
100292
|
-
...masteryRequested ? { masteredUnits: input.award.masteredUnitsApplied } : {},
|
|
100293
|
-
totalQuestions: completion.totalQuestions,
|
|
100294
|
-
correctQuestions: completion.correctQuestions,
|
|
100295
|
-
appName: input.appName
|
|
100296
|
-
}
|
|
100297
|
-
}
|
|
100298
|
-
};
|
|
100299
|
-
}
|
|
100300
|
-
var PLAYABLE_SHAPE_SET;
|
|
100301
|
-
var init_timeback_assessment_runtime_util = __esm(() => {
|
|
100302
|
-
init_src();
|
|
100303
|
-
init_assessment_runtime2();
|
|
100304
|
-
init_qti();
|
|
100305
|
-
init_types2();
|
|
100306
|
-
init_utils6();
|
|
100307
|
-
init_timeback4();
|
|
100308
|
-
init_uuid();
|
|
100309
|
-
init_errors2();
|
|
100310
|
-
PLAYABLE_SHAPE_SET = new Set(PLAYABLE_ASSESSMENT_SHAPES);
|
|
100311
|
-
});
|
|
100312
|
-
|
|
100313
101453
|
// ../api-core/src/utils/timeback-review-mapping.util.ts
|
|
100314
101454
|
function reviewBankTestItemIdentifiers(test) {
|
|
100315
101455
|
return qtiTestParts(test).flatMap((part) => part["qti-assessment-section"].flatMap((section) => (section["qti-assessment-item-ref"] ?? []).map((reference) => reference.identifier)));
|
|
@@ -100563,7 +101703,7 @@ async function prepareReviewMappingUpdate(test, questions) {
|
|
|
100563
101703
|
}
|
|
100564
101704
|
};
|
|
100565
101705
|
}
|
|
100566
|
-
var PLAYCADEMY_REVIEW_BANK_SINK_METADATA_KEY = "playcademyReviewBankSink", PLAYCADEMY_REVIEW_BANK_SINK_VERSION = 1, StoredSinkWarningSchema, ReviewBankSinkMarkerSchema;
|
|
101706
|
+
var PLAYCADEMY_REVIEW_BANK_TITLE = "Review Bank", PLAYCADEMY_REVIEW_BANK_SINK_METADATA_KEY = "playcademyReviewBankSink", PLAYCADEMY_REVIEW_BANK_SINK_VERSION = 1, StoredSinkWarningSchema, ReviewBankSinkMarkerSchema;
|
|
100567
101707
|
var init_timeback_review_mapping_util = __esm(() => {
|
|
100568
101708
|
init_esm();
|
|
100569
101709
|
init_assessment_runtime2();
|
|
@@ -100606,6 +101746,259 @@ var init_timeback_review_mapping_util = __esm(() => {
|
|
|
100606
101746
|
});
|
|
100607
101747
|
});
|
|
100608
101748
|
|
|
101749
|
+
// ../api-core/src/utils/timeback-assessment-artifacts.util.ts
|
|
101750
|
+
async function storeAndVerifyCompiledAssessmentArtifacts(input) {
|
|
101751
|
+
await input.store(input.compiled);
|
|
101752
|
+
const assessment = await input.loadPlayable(input.compiled.identity);
|
|
101753
|
+
const scoring = assessment ? await input.loadMatchedScoring(assessment, input.compiled.identity) : null;
|
|
101754
|
+
if (!assessment || !scoring) {
|
|
101755
|
+
throw new ServiceUnavailableError(`Prepared ${input.label} could not be verified.`);
|
|
101756
|
+
}
|
|
101757
|
+
return assessment;
|
|
101758
|
+
}
|
|
101759
|
+
function assertSourceIdentity(source) {
|
|
101760
|
+
if (source.test.identifier !== source.questions.assessmentTest) {
|
|
101761
|
+
throw new ValidationError(`QTI questions for ${source.questions.assessmentTest} do not describe ${source.test.identifier}`);
|
|
101762
|
+
}
|
|
101763
|
+
}
|
|
101764
|
+
function sourceItemIdentifiers(source) {
|
|
101765
|
+
return source.questions.questions.map(({ question }) => question.identifier);
|
|
101766
|
+
}
|
|
101767
|
+
function compileScoringArtifact(questions) {
|
|
101768
|
+
try {
|
|
101769
|
+
return compileQtiScoringArtifact(questions);
|
|
101770
|
+
} catch (error88) {
|
|
101771
|
+
if (error88 instanceof QtiScoringArtifactValidationError) {
|
|
101772
|
+
throw new ValidationError(error88.message);
|
|
101773
|
+
}
|
|
101774
|
+
throw error88;
|
|
101775
|
+
}
|
|
101776
|
+
}
|
|
101777
|
+
function qtiFallbackItemCount(compiled) {
|
|
101778
|
+
return compiled.scoringPayload.items.filter((item) => item.strategy === "qti").length;
|
|
101779
|
+
}
|
|
101780
|
+
async function compileManagedAssessmentArtifacts(input) {
|
|
101781
|
+
assertSourceIdentity(input.source);
|
|
101782
|
+
const playablePayload = await buildPlayableAssessment(input.source.test, input.source.questions);
|
|
101783
|
+
const scoring = compileScoringArtifact(input.source.questions);
|
|
101784
|
+
const diagnostic = input.diagnosticRoutingManifest ? {
|
|
101785
|
+
routingRevision: await diagnosticRoutingRevision(input.diagnosticRoutingManifest),
|
|
101786
|
+
routingManifest: input.diagnosticRoutingManifest
|
|
101787
|
+
} : undefined;
|
|
101788
|
+
return {
|
|
101789
|
+
identity: {
|
|
101790
|
+
kind: "assessment",
|
|
101791
|
+
qtiTestIdentifier: input.source.test.identifier,
|
|
101792
|
+
contentRevision: playablePayload.contentRevision
|
|
101793
|
+
},
|
|
101794
|
+
playablePayload,
|
|
101795
|
+
scoringPayload: {
|
|
101796
|
+
...scoring,
|
|
101797
|
+
...diagnostic ? { diagnostic } : {}
|
|
101798
|
+
}
|
|
101799
|
+
};
|
|
101800
|
+
}
|
|
101801
|
+
async function compileReviewBankArtifacts(input) {
|
|
101802
|
+
assertSourceIdentity(input.source);
|
|
101803
|
+
if (input.source.test.identifier !== input.bank.bankIdentifier) {
|
|
101804
|
+
throw new ValidationError(`Review bank ${input.bank.bankIdentifier} does not describe ${input.source.test.identifier}`);
|
|
101805
|
+
}
|
|
101806
|
+
const itemIdentifiers = sourceItemIdentifiers(input.source);
|
|
101807
|
+
const indexedItemIdentifiers = input.bank.items.map((item) => item.itemIdentifier);
|
|
101808
|
+
if (itemIdentifiers.length !== indexedItemIdentifiers.length || itemIdentifiers.some((identifier, index2) => identifier !== indexedItemIdentifiers[index2])) {
|
|
101809
|
+
throw new ValidationError("Review artifact content does not match its revisioned index");
|
|
101810
|
+
}
|
|
101811
|
+
const compiledPlayable = await buildPlayableAssessment({ ...input.source.test, title: PLAYCADEMY_REVIEW_BANK_TITLE }, input.source.questions);
|
|
101812
|
+
const playablePayload = {
|
|
101813
|
+
...compiledPlayable,
|
|
101814
|
+
contentRevision: input.bank.sourceContentRevision
|
|
101815
|
+
};
|
|
101816
|
+
return {
|
|
101817
|
+
identity: {
|
|
101818
|
+
kind: "review-bank",
|
|
101819
|
+
qtiTestIdentifier: input.bank.bankIdentifier,
|
|
101820
|
+
sourceContentRevision: input.bank.sourceContentRevision,
|
|
101821
|
+
bankRevision: input.bank.bankRevision
|
|
101822
|
+
},
|
|
101823
|
+
playablePayload,
|
|
101824
|
+
scoringPayload: {
|
|
101825
|
+
...compileScoringArtifact(input.source.questions),
|
|
101826
|
+
reviewBank: {
|
|
101827
|
+
sourceContentRevision: input.bank.sourceContentRevision,
|
|
101828
|
+
bankRevision: input.bank.bankRevision,
|
|
101829
|
+
items: input.bank.items
|
|
101830
|
+
}
|
|
101831
|
+
}
|
|
101832
|
+
};
|
|
101833
|
+
}
|
|
101834
|
+
var init_timeback_assessment_artifacts_util = __esm(() => {
|
|
101835
|
+
init_assessment_runtime2();
|
|
101836
|
+
init_qti();
|
|
101837
|
+
init_errors2();
|
|
101838
|
+
init_timeback_assessment_runtime_util();
|
|
101839
|
+
init_timeback_review_mapping_util();
|
|
101840
|
+
});
|
|
101841
|
+
|
|
101842
|
+
// ../api-core/src/utils/timeback-assessment-preparation.util.ts
|
|
101843
|
+
class AssessmentPreparationFlights {
|
|
101844
|
+
limit;
|
|
101845
|
+
pending = new Map;
|
|
101846
|
+
constructor(limit) {
|
|
101847
|
+
if (!Number.isInteger(limit) || limit <= 0) {
|
|
101848
|
+
throw new Error("Assessment preparation flight limit must be a positive integer");
|
|
101849
|
+
}
|
|
101850
|
+
this.limit = limit;
|
|
101851
|
+
}
|
|
101852
|
+
claim(key, operation) {
|
|
101853
|
+
const existing = this.pending.get(key);
|
|
101854
|
+
if (existing) {
|
|
101855
|
+
return { status: "coalesced", promise: existing };
|
|
101856
|
+
}
|
|
101857
|
+
if (this.pending.size >= this.limit) {
|
|
101858
|
+
return { status: "at_capacity" };
|
|
101859
|
+
}
|
|
101860
|
+
const pending = Promise.resolve().then(operation).finally(() => {
|
|
101861
|
+
if (this.pending.get(key) === pending) {
|
|
101862
|
+
this.pending.delete(key);
|
|
101863
|
+
}
|
|
101864
|
+
});
|
|
101865
|
+
this.pending.set(key, pending);
|
|
101866
|
+
return { status: "started", promise: pending };
|
|
101867
|
+
}
|
|
101868
|
+
}
|
|
101869
|
+
function assessmentPreparationRequestHash(input) {
|
|
101870
|
+
return sha256Hex(canonicalJson2(input));
|
|
101871
|
+
}
|
|
101872
|
+
function assessmentPreparationFlightKey(input) {
|
|
101873
|
+
return canonicalJson2(input);
|
|
101874
|
+
}
|
|
101875
|
+
async function assessmentPreparationReceiptMatches(plan, request) {
|
|
101876
|
+
return plan.gameId === request.gameId && plan.studentId === request.studentId && plan.activityId === request.input.activityId && plan.purpose === request.input.purpose && plan.requestHash === await assessmentPreparationRequestHash(request.input);
|
|
101877
|
+
}
|
|
101878
|
+
function resolveAssessmentPreparationSelection(decision, candidateCourseCount) {
|
|
101879
|
+
if (decision?.kind === "resume") {
|
|
101880
|
+
return {
|
|
101881
|
+
status: "blocked",
|
|
101882
|
+
result: { status: "canonical_required", reason: "resumable_attempt" }
|
|
101883
|
+
};
|
|
101884
|
+
}
|
|
101885
|
+
if (candidateCourseCount !== 1) {
|
|
101886
|
+
return {
|
|
101887
|
+
status: "blocked",
|
|
101888
|
+
result: { status: "unprepared", reason: "prerequisite_unavailable" }
|
|
101889
|
+
};
|
|
101890
|
+
}
|
|
101891
|
+
return decision ? { status: "ready", decision } : { status: "unavailable" };
|
|
101892
|
+
}
|
|
101893
|
+
function preparedAssessmentIntegration(plan, candidates) {
|
|
101894
|
+
if (candidates.rows.length !== 1) {
|
|
101895
|
+
return null;
|
|
101896
|
+
}
|
|
101897
|
+
const integration = candidates.rows[0];
|
|
101898
|
+
const enrollment = candidates.enrollmentByCourse.get(integration.courseId);
|
|
101899
|
+
if (integration.id !== plan.integrationId || integration.courseId !== plan.courseId || enrollment?.id !== plan.enrollmentId) {
|
|
101900
|
+
return null;
|
|
101901
|
+
}
|
|
101902
|
+
return integration;
|
|
101903
|
+
}
|
|
101904
|
+
function preparedAssessmentTestIsLive(plan, tests) {
|
|
101905
|
+
if (plan.purpose === "review" && tests.length !== 1) {
|
|
101906
|
+
return false;
|
|
101907
|
+
}
|
|
101908
|
+
return tests.some((test) => test.assessmentKey === plan.selectedTest.assessmentKey && test.qtiTestIdentifier === plan.selectedTest.identifier && (plan.purpose !== "diagnostic" || test.id === plan.diagnostic.definitionId));
|
|
101909
|
+
}
|
|
101910
|
+
function preparedAssessmentArtifactIdentity(plan) {
|
|
101911
|
+
return plan.purpose === "review" ? {
|
|
101912
|
+
kind: "review-bank",
|
|
101913
|
+
qtiTestIdentifier: plan.selectedTest.identifier,
|
|
101914
|
+
sourceContentRevision: plan.selectedTest.contentRevision,
|
|
101915
|
+
bankRevision: plan.review.bankRevision
|
|
101916
|
+
} : {
|
|
101917
|
+
kind: "assessment",
|
|
101918
|
+
qtiTestIdentifier: plan.selectedTest.identifier,
|
|
101919
|
+
contentRevision: plan.selectedTest.contentRevision
|
|
101920
|
+
};
|
|
101921
|
+
}
|
|
101922
|
+
function preparedAssessmentScoringIsCompatible(plan, scoring) {
|
|
101923
|
+
return Boolean(scoring && (plan.purpose !== "diagnostic" || scoring.diagnostic?.routingRevision === plan.diagnostic.routingRevision));
|
|
101924
|
+
}
|
|
101925
|
+
function preparedReviewBankIndex(plan) {
|
|
101926
|
+
return {
|
|
101927
|
+
bankIdentifier: plan.selectedTest.identifier,
|
|
101928
|
+
sourceContentRevision: plan.selectedTest.contentRevision,
|
|
101929
|
+
bankRevision: plan.review.bankRevision,
|
|
101930
|
+
items: plan.review.selections.map((selection) => ({
|
|
101931
|
+
itemIdentifier: selection.itemIdentifier,
|
|
101932
|
+
standards: [selection.standard]
|
|
101933
|
+
}))
|
|
101934
|
+
};
|
|
101935
|
+
}
|
|
101936
|
+
function nextAssessmentAttemptNumber(attempts) {
|
|
101937
|
+
return Math.max(0, ...[...attempts].map((entry) => entry.metadata.attemptNumber)) + 1;
|
|
101938
|
+
}
|
|
101939
|
+
function buildPreparedAssessmentMetadata(plan, timestamp6, testName) {
|
|
101940
|
+
const base = {
|
|
101941
|
+
version: PLAYCADEMY_ASSESSMENT_RESULT_METADATA_VERSION,
|
|
101942
|
+
activityId: plan.activityId,
|
|
101943
|
+
courseId: plan.courseId,
|
|
101944
|
+
integrationId: plan.integrationId,
|
|
101945
|
+
enrollmentId: plan.enrollmentId,
|
|
101946
|
+
selectedTest: plan.selectedTest,
|
|
101947
|
+
attemptNumber: plan.attemptNumber,
|
|
101948
|
+
responseVersion: 0,
|
|
101949
|
+
responses: {},
|
|
101950
|
+
itemSubmissions: [],
|
|
101951
|
+
startedAt: timestamp6,
|
|
101952
|
+
updatedAt: timestamp6
|
|
101953
|
+
};
|
|
101954
|
+
if (plan.purpose === "mastery") {
|
|
101955
|
+
return {
|
|
101956
|
+
...base,
|
|
101957
|
+
purpose: plan.purpose,
|
|
101958
|
+
mastery: {
|
|
101959
|
+
requestFingerprint: masteryRequestFingerprint(plan.mastery),
|
|
101960
|
+
standard: plan.mastery.standard
|
|
101961
|
+
}
|
|
101962
|
+
};
|
|
101963
|
+
}
|
|
101964
|
+
if (plan.purpose === "diagnostic") {
|
|
101965
|
+
return {
|
|
101966
|
+
...base,
|
|
101967
|
+
purpose: plan.purpose,
|
|
101968
|
+
diagnostic: {
|
|
101969
|
+
definitionId: plan.diagnostic.definitionId,
|
|
101970
|
+
diagnosticKey: plan.diagnostic.diagnosticKey,
|
|
101971
|
+
testName,
|
|
101972
|
+
routingRevision: plan.diagnostic.routingRevision,
|
|
101973
|
+
ledger: [],
|
|
101974
|
+
state: plan.diagnostic.initialState
|
|
101975
|
+
}
|
|
101976
|
+
};
|
|
101977
|
+
}
|
|
101978
|
+
if (plan.purpose === "review") {
|
|
101979
|
+
return {
|
|
101980
|
+
...base,
|
|
101981
|
+
purpose: plan.purpose,
|
|
101982
|
+
review: {
|
|
101983
|
+
requestFingerprint: reviewRequestFingerprint({
|
|
101984
|
+
standards: plan.review.standards,
|
|
101985
|
+
candidateItemsPerStandard: plan.review.candidateItemsPerStandard
|
|
101986
|
+
}, DEFAULT_REVIEW_SELECTION_POLICY),
|
|
101987
|
+
bankRevision: plan.review.bankRevision,
|
|
101988
|
+
standards: plan.review.standards,
|
|
101989
|
+
candidateItemsPerStandard: plan.review.candidateItemsPerStandard,
|
|
101990
|
+
selections: plan.review.selections,
|
|
101991
|
+
fulfillment: plan.review.fulfillment
|
|
101992
|
+
}
|
|
101993
|
+
};
|
|
101994
|
+
}
|
|
101995
|
+
return { ...base, purpose: plan.purpose };
|
|
101996
|
+
}
|
|
101997
|
+
var init_timeback_assessment_preparation_util = __esm(() => {
|
|
101998
|
+
init_src();
|
|
101999
|
+
init_assessment_runtime2();
|
|
102000
|
+
});
|
|
102001
|
+
|
|
100609
102002
|
// ../api-core/src/utils/timeback-assessment-publication.util.ts
|
|
100610
102003
|
function assessmentKeyFromManagedQtiIdentifier(identifier) {
|
|
100611
102004
|
const assessmentKey = /^playcademy-test\.(.+)\.[a-f0-9]{64}$/.exec(identifier)?.[1];
|
|
@@ -100727,7 +102120,7 @@ async function prepareManagedAssessmentPublication(input) {
|
|
|
100727
102120
|
sourceItemIdentifier: question.identifier,
|
|
100728
102121
|
qtiItemIdentifier,
|
|
100729
102122
|
contentHash,
|
|
100730
|
-
xml: canonicalManagedAssetUrls(rewriteQtiItemIdentity(declarationNeutralQtiXml(question.rawXml), qtiItemIdentifier, title)),
|
|
102123
|
+
xml: canonicalManagedAssetUrls(rewriteQtiItemIdentity(declarationNeutralQtiXml(question.rawXml).trim(), qtiItemIdentifier, title)),
|
|
100731
102124
|
metadata: {
|
|
100732
102125
|
...metadata2,
|
|
100733
102126
|
ownerSystem: PLAYCADEMY_QTI_OWNER_SYSTEM,
|
|
@@ -100816,7 +102209,11 @@ async function prepareManagedAssessmentPublication(input) {
|
|
|
100816
102209
|
qtiTestIdentifier,
|
|
100817
102210
|
testInput,
|
|
100818
102211
|
items,
|
|
100819
|
-
diagnosticRoutingManifest: compiledRouting
|
|
102212
|
+
diagnosticRoutingManifest: compiledRouting,
|
|
102213
|
+
artifactSource: {
|
|
102214
|
+
test: testInput,
|
|
102215
|
+
questions: physicalQuestions
|
|
102216
|
+
}
|
|
100820
102217
|
};
|
|
100821
102218
|
}
|
|
100822
102219
|
function assertManagedAssessmentPublication(test, expected) {
|
|
@@ -100924,9 +102321,6 @@ function buildAssessmentAssociationUpdates(row, input) {
|
|
|
100924
102321
|
updates.standardFramework = null;
|
|
100925
102322
|
updates.standardIdentifier = null;
|
|
100926
102323
|
}
|
|
100927
|
-
if (row.status === "live" && input.purpose !== row.purpose) {
|
|
100928
|
-
updates.sortOrder = null;
|
|
100929
|
-
}
|
|
100930
102324
|
}
|
|
100931
102325
|
if (input.standard !== undefined) {
|
|
100932
102326
|
updates.standardFramework = input.standard.framework;
|
|
@@ -100934,9 +102328,6 @@ function buildAssessmentAssociationUpdates(row, input) {
|
|
|
100934
102328
|
}
|
|
100935
102329
|
if (input.status !== undefined) {
|
|
100936
102330
|
updates.status = input.status;
|
|
100937
|
-
if (input.status === "archived") {
|
|
100938
|
-
updates.sortOrder = null;
|
|
100939
|
-
}
|
|
100940
102331
|
}
|
|
100941
102332
|
return updates;
|
|
100942
102333
|
}
|
|
@@ -100945,25 +102336,9 @@ function assessmentStandardForRow(row) {
|
|
|
100945
102336
|
}
|
|
100946
102337
|
function validateUniqueAssessmentIdentifiers(testIdentifiers) {
|
|
100947
102338
|
if (new Set(testIdentifiers).size !== testIdentifiers.length) {
|
|
100948
|
-
throw new ValidationError("Assessment
|
|
102339
|
+
throw new ValidationError("Assessment manifests must contain unique QTI test identifiers");
|
|
100949
102340
|
}
|
|
100950
102341
|
}
|
|
100951
|
-
function assertAssessmentOrderUpdateSucceeded(updatedRow) {
|
|
100952
|
-
if (!updatedRow) {
|
|
100953
|
-
throw new ValidationError("Assessment order changed while it was being saved. Refresh and try again.");
|
|
100954
|
-
}
|
|
100955
|
-
return updatedRow;
|
|
100956
|
-
}
|
|
100957
|
-
function lockOrderAssessmentRows(rows) {
|
|
100958
|
-
return rows.toSorted((left, right) => left.id.localeCompare(right.id));
|
|
100959
|
-
}
|
|
100960
|
-
function orderLiveAssessmentRows(liveRows, purpose, testIdentifiers) {
|
|
100961
|
-
const rowsByIdentifier = new Map(liveRows.map((row) => [row.qtiTestIdentifier, row]));
|
|
100962
|
-
if (liveRows.length !== testIdentifiers.length || testIdentifiers.some((identifier) => !rowsByIdentifier.has(identifier))) {
|
|
100963
|
-
throw new ValidationError(`Assessment order must include every live ${purpose} assessment exactly once`);
|
|
100964
|
-
}
|
|
100965
|
-
return testIdentifiers.map((identifier) => rowsByIdentifier.get(identifier));
|
|
100966
|
-
}
|
|
100967
102342
|
var init_timeback_assessment_rules_util = __esm(() => {
|
|
100968
102343
|
init_errors2();
|
|
100969
102344
|
});
|
|
@@ -101036,7 +102411,7 @@ var init_timeback_qti_hydration_util = __esm(() => {
|
|
|
101036
102411
|
});
|
|
101037
102412
|
|
|
101038
102413
|
// ../api-core/src/services/timeback-assessment-runtime.service.ts
|
|
101039
|
-
var TimebackAssessmentRuntimeService;
|
|
102414
|
+
var AssessmentPreparationPrerequisiteError, TimebackAssessmentRuntimeService;
|
|
101040
102415
|
var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
101041
102416
|
init_drizzle_orm();
|
|
101042
102417
|
init_src();
|
|
@@ -101048,17 +102423,30 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
101048
102423
|
init_types2();
|
|
101049
102424
|
init_uuid();
|
|
101050
102425
|
init_errors2();
|
|
102426
|
+
init_assessment_artifact_runtime_util();
|
|
101051
102427
|
init_assessment_runtime_lock_util();
|
|
102428
|
+
init_timeback_assessment_artifacts_util();
|
|
102429
|
+
init_timeback_assessment_preparation_util();
|
|
101052
102430
|
init_timeback_assessment_publication_util();
|
|
101053
102431
|
init_timeback_assessment_rules_util();
|
|
101054
102432
|
init_timeback_assessment_runtime_util();
|
|
101055
102433
|
init_timeback_qti_hydration_util();
|
|
101056
102434
|
await __promiseAll([
|
|
101057
102435
|
init_dist5(),
|
|
101058
|
-
init_errors8()
|
|
102436
|
+
init_errors8(),
|
|
102437
|
+
init_assessment_artifact_loader_util()
|
|
101059
102438
|
]);
|
|
102439
|
+
AssessmentPreparationPrerequisiteError = class AssessmentPreparationPrerequisiteError extends Error {
|
|
102440
|
+
original;
|
|
102441
|
+
constructor(original) {
|
|
102442
|
+
super("Assessment preparation prerequisite provisioning failed");
|
|
102443
|
+
this.name = "AssessmentPreparationPrerequisiteError";
|
|
102444
|
+
this.original = original;
|
|
102445
|
+
}
|
|
102446
|
+
};
|
|
101060
102447
|
TimebackAssessmentRuntimeService = class TimebackAssessmentRuntimeService {
|
|
101061
102448
|
static ASSESSMENT_CACHE_LIMIT = 32;
|
|
102449
|
+
static PREPARATION_IN_FLIGHT_LIMIT = 32;
|
|
101062
102450
|
static ASSESSMENT_CACHE_TTL_MS = 60000;
|
|
101063
102451
|
static EXPORT_CONCURRENCY = 4;
|
|
101064
102452
|
static ITEM_SUBMISSION_MAX_PREPARATION_ATTEMPTS = 3;
|
|
@@ -101078,16 +102466,65 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
101078
102466
|
defaultTTL: TimebackAssessmentRuntimeService.ASSESSMENT_CACHE_TTL_MS,
|
|
101079
102467
|
maxSize: TimebackAssessmentRuntimeService.ASSESSMENT_CACHE_LIMIT
|
|
101080
102468
|
});
|
|
102469
|
+
artifactLoader;
|
|
102470
|
+
preparations = new AssessmentPreparationFlights(TimebackAssessmentRuntimeService.PREPARATION_IN_FLIGHT_LIMIT);
|
|
101081
102471
|
constructor(deps) {
|
|
101082
102472
|
this.deps = deps;
|
|
102473
|
+
this.artifactLoader = new AssessmentArtifactRuntimeLoader(deps.assessmentArtifacts, {
|
|
102474
|
+
maxSize: TimebackAssessmentRuntimeService.ASSESSMENT_CACHE_LIMIT
|
|
102475
|
+
});
|
|
101083
102476
|
}
|
|
101084
|
-
async
|
|
102477
|
+
async prepare(params) {
|
|
102478
|
+
await this.deps.validateDeveloperAccess(params.user, params.gameId);
|
|
102479
|
+
const startedAt = Date.now();
|
|
102480
|
+
const requestHash = await assessmentPreparationRequestHash(params.input);
|
|
102481
|
+
const key = assessmentPreparationFlightKey({
|
|
102482
|
+
gameId: params.gameId,
|
|
102483
|
+
studentId: params.studentId,
|
|
102484
|
+
requestHash
|
|
102485
|
+
});
|
|
102486
|
+
const flight = this.preparations.claim(key, () => this.prepareNewAttempt(params, requestHash).catch((error88) => {
|
|
102487
|
+
if (error88 instanceof ValidationError || error88 instanceof AssessmentRuntimeError) {
|
|
102488
|
+
throw error88;
|
|
102489
|
+
}
|
|
102490
|
+
this.recordPreparation("unprepared", params.input.purpose, startedAt, {
|
|
102491
|
+
reason: "prerequisite_unavailable",
|
|
102492
|
+
error: error88
|
|
102493
|
+
});
|
|
102494
|
+
return {
|
|
102495
|
+
status: "unprepared",
|
|
102496
|
+
reason: "prerequisite_unavailable"
|
|
102497
|
+
};
|
|
102498
|
+
}));
|
|
102499
|
+
if (flight.status === "coalesced") {
|
|
102500
|
+
addEvent("assessment.preparation_coalesced", {
|
|
102501
|
+
"app.assessment.purpose": params.input.purpose
|
|
102502
|
+
});
|
|
102503
|
+
}
|
|
102504
|
+
if (flight.status === "at_capacity") {
|
|
102505
|
+
this.recordPreparation("unprepared", params.input.purpose, Date.now(), {
|
|
102506
|
+
reason: "local_capacity"
|
|
102507
|
+
});
|
|
102508
|
+
return { status: "unprepared", reason: "prerequisite_unavailable" };
|
|
102509
|
+
}
|
|
102510
|
+
return flight.promise;
|
|
102511
|
+
}
|
|
102512
|
+
async start(params) {
|
|
102513
|
+
await this.deps.validateDeveloperAccess(params.user, params.gameId);
|
|
102514
|
+
if (params.preparationReceipt) {
|
|
102515
|
+
const prepared = await this.tryPreparedStart(params, params.preparationReceipt);
|
|
102516
|
+
if (prepared) {
|
|
102517
|
+
return prepared;
|
|
102518
|
+
}
|
|
102519
|
+
}
|
|
102520
|
+
return this.startCanonical(params);
|
|
102521
|
+
}
|
|
102522
|
+
async startCanonical({
|
|
101085
102523
|
gameId,
|
|
101086
102524
|
studentId,
|
|
101087
102525
|
input,
|
|
101088
102526
|
user
|
|
101089
102527
|
}) {
|
|
101090
|
-
await this.deps.validateDeveloperAccess(user, gameId);
|
|
101091
102528
|
if (input.purpose === "diagnostic") {
|
|
101092
102529
|
return this.startDiagnostic({ gameId, studentId, input, user });
|
|
101093
102530
|
}
|
|
@@ -101196,6 +102633,285 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
101196
102633
|
return this.snapshot(result, metadata2, assessment, context2.integration);
|
|
101197
102634
|
});
|
|
101198
102635
|
}
|
|
102636
|
+
async prepareNewAttempt(params, requestHash) {
|
|
102637
|
+
const startedAt = Date.now();
|
|
102638
|
+
const [candidates, results] = await Promise.all([
|
|
102639
|
+
this.candidateIntegrations(params.gameId, params.studentId, params.input),
|
|
102640
|
+
this.studentAssessmentResults(params.studentId)
|
|
102641
|
+
]);
|
|
102642
|
+
const context2 = await this.selectIntegration(candidates, params.studentId, params.input, results);
|
|
102643
|
+
const attemptScope = {
|
|
102644
|
+
studentId: params.studentId,
|
|
102645
|
+
activityId: params.input.activityId,
|
|
102646
|
+
purpose: params.input.purpose,
|
|
102647
|
+
courseId: context2.integration.courseId,
|
|
102648
|
+
integrationId: context2.integration.id,
|
|
102649
|
+
enrollmentId: context2.enrollment.id
|
|
102650
|
+
};
|
|
102651
|
+
const tests = await this.liveTests(context2.integration.id, params.input.purpose, this.deps.db, params.input.purpose === "mastery" ? params.input.standard : undefined, params.input.purpose === "diagnostic" ? params.input.diagnosticKey : undefined);
|
|
102652
|
+
const listing = this.attemptListing(results, attemptScope);
|
|
102653
|
+
const decision = selectRuntimeAssessment(tests, [
|
|
102654
|
+
...this.attemptsMatchingRequest(listing.attempts, this.startRequestFingerprint(params.input)).values()
|
|
102655
|
+
].map((entry) => entry.selection));
|
|
102656
|
+
const selection = resolveAssessmentPreparationSelection(decision, candidates.rows.length);
|
|
102657
|
+
if (selection.status === "blocked") {
|
|
102658
|
+
this.recordPreparation(selection.result.status, params.input.purpose, startedAt, {
|
|
102659
|
+
reason: selection.result.status === "canonical_required" ? selection.result.reason : "multiple_candidate_courses"
|
|
102660
|
+
});
|
|
102661
|
+
return selection.result;
|
|
102662
|
+
}
|
|
102663
|
+
if (selection.status === "unavailable") {
|
|
102664
|
+
throw AssessmentRuntimeError.from(assessmentNoEligibleTests(params.input.purpose));
|
|
102665
|
+
}
|
|
102666
|
+
const preparation = {
|
|
102667
|
+
context: context2,
|
|
102668
|
+
tests,
|
|
102669
|
+
listing,
|
|
102670
|
+
decision: selection.decision
|
|
102671
|
+
};
|
|
102672
|
+
let prepared;
|
|
102673
|
+
try {
|
|
102674
|
+
prepared = await this.buildPreparationPlan(params, requestHash, preparation);
|
|
102675
|
+
} catch (error88) {
|
|
102676
|
+
if (error88 instanceof ValidationError || error88 instanceof AssessmentRuntimeError) {
|
|
102677
|
+
throw error88;
|
|
102678
|
+
}
|
|
102679
|
+
const prerequisiteFailure = error88 instanceof AssessmentPreparationPrerequisiteError;
|
|
102680
|
+
const reason = prerequisiteFailure ? "prerequisite_unavailable" : "artifact_unavailable";
|
|
102681
|
+
this.recordPreparation("unprepared", params.input.purpose, startedAt, {
|
|
102682
|
+
reason,
|
|
102683
|
+
error: prerequisiteFailure ? error88.original : error88
|
|
102684
|
+
});
|
|
102685
|
+
return { status: "unprepared", reason };
|
|
102686
|
+
}
|
|
102687
|
+
const minted = await this.deps.mintAssessmentPreparationReceipt(prepared.plan);
|
|
102688
|
+
this.recordPreparation("prepared", params.input.purpose, startedAt, {
|
|
102689
|
+
artifactSource: prepared.artifactSource
|
|
102690
|
+
});
|
|
102691
|
+
return { status: "prepared", ...minted };
|
|
102692
|
+
}
|
|
102693
|
+
async buildPreparationPlan(params, requestHash, preparation) {
|
|
102694
|
+
if (params.input.purpose === "review") {
|
|
102695
|
+
return this.buildReviewPreparationPlan({ ...params, input: params.input }, requestHash, preparation);
|
|
102696
|
+
}
|
|
102697
|
+
const { context: context2, decision, listing } = preparation;
|
|
102698
|
+
let artifact;
|
|
102699
|
+
let diagnostic;
|
|
102700
|
+
if (params.input.purpose === "diagnostic") {
|
|
102701
|
+
const definition = await this.requireDiagnosticDefinition(decision.test.id, context2.integration.id, this.deps.db, true);
|
|
102702
|
+
artifact = await this.loadOrCompileManagedArtifact(definition.qtiTestIdentifier, definition.diagnosticRoutingManifest);
|
|
102703
|
+
diagnostic = {
|
|
102704
|
+
definition,
|
|
102705
|
+
routing: await this.initializeHostedDiagnostic(definition, artifact.assessment)
|
|
102706
|
+
};
|
|
102707
|
+
} else {
|
|
102708
|
+
artifact = await this.loadOrCompileManagedArtifact(decision.test.qtiTestIdentifier);
|
|
102709
|
+
}
|
|
102710
|
+
const plan = await this.provisionPreparationPrerequisites(() => this.finishManagedPreparationPlan({
|
|
102711
|
+
params,
|
|
102712
|
+
requestHash,
|
|
102713
|
+
context: context2,
|
|
102714
|
+
listing,
|
|
102715
|
+
decision,
|
|
102716
|
+
assessment: artifact.assessment,
|
|
102717
|
+
diagnostic
|
|
102718
|
+
}));
|
|
102719
|
+
return { plan, artifactSource: artifact.source };
|
|
102720
|
+
}
|
|
102721
|
+
async provisionPreparationPrerequisites(operation) {
|
|
102722
|
+
try {
|
|
102723
|
+
return await operation();
|
|
102724
|
+
} catch (error88) {
|
|
102725
|
+
throw new AssessmentPreparationPrerequisiteError(error88);
|
|
102726
|
+
}
|
|
102727
|
+
}
|
|
102728
|
+
async finishManagedPreparationPlan(input) {
|
|
102729
|
+
const { params, context: context2, listing, decision, assessment, diagnostic } = input;
|
|
102730
|
+
const diagnosticIdentity = diagnostic && {
|
|
102731
|
+
definitionId: diagnostic.definition.id,
|
|
102732
|
+
diagnosticKey: diagnostic.definition.diagnosticKey,
|
|
102733
|
+
routingRevision: diagnostic.routing.routingRevision
|
|
102734
|
+
};
|
|
102735
|
+
const classId = await this.resolveClassId(context2.integration.courseId, context2.enrollment, params.studentId);
|
|
102736
|
+
const parentLineItemId = await this.ensureLineItem({
|
|
102737
|
+
classId,
|
|
102738
|
+
integration: context2.integration,
|
|
102739
|
+
purpose: params.input.purpose,
|
|
102740
|
+
assessment,
|
|
102741
|
+
...diagnosticIdentity ? { diagnostic: diagnosticIdentity } : {}
|
|
102742
|
+
});
|
|
102743
|
+
const attempt = await this.claimAttemptId({
|
|
102744
|
+
gameId: params.gameId,
|
|
102745
|
+
studentId: params.studentId,
|
|
102746
|
+
courseId: context2.integration.courseId,
|
|
102747
|
+
activityId: params.input.activityId,
|
|
102748
|
+
purpose: params.input.purpose
|
|
102749
|
+
}, nextAssessmentAttemptNumber(listing.attempts.values()));
|
|
102750
|
+
const common2 = {
|
|
102751
|
+
version: 1,
|
|
102752
|
+
gameId: params.gameId,
|
|
102753
|
+
studentId: params.studentId,
|
|
102754
|
+
activityId: params.input.activityId,
|
|
102755
|
+
requestHash: input.requestHash,
|
|
102756
|
+
preparedAt: Date.now(),
|
|
102757
|
+
integrationId: context2.integration.id,
|
|
102758
|
+
enrollmentId: context2.enrollment.id,
|
|
102759
|
+
courseId: context2.integration.courseId,
|
|
102760
|
+
...attempt,
|
|
102761
|
+
parentLineItemId,
|
|
102762
|
+
selectedTest: {
|
|
102763
|
+
assessmentKey: decision.test.assessmentKey,
|
|
102764
|
+
identifier: assessment.identifier,
|
|
102765
|
+
contentRevision: assessment.contentRevision
|
|
102766
|
+
}
|
|
102767
|
+
};
|
|
102768
|
+
if (params.input.purpose === "mastery") {
|
|
102769
|
+
return {
|
|
102770
|
+
...common2,
|
|
102771
|
+
purpose: params.input.purpose,
|
|
102772
|
+
mastery: { standard: normalizeMasteryStandard(params.input.standard) }
|
|
102773
|
+
};
|
|
102774
|
+
}
|
|
102775
|
+
if (params.input.purpose === "diagnostic") {
|
|
102776
|
+
if (!diagnostic || !diagnosticIdentity) {
|
|
102777
|
+
throw new Error("Diagnostic preparation is missing its routing plan");
|
|
102778
|
+
}
|
|
102779
|
+
return {
|
|
102780
|
+
...common2,
|
|
102781
|
+
purpose: params.input.purpose,
|
|
102782
|
+
diagnostic: { ...diagnosticIdentity, initialState: diagnostic.routing.state }
|
|
102783
|
+
};
|
|
102784
|
+
}
|
|
102785
|
+
if (params.input.purpose !== "end_of_course") {
|
|
102786
|
+
throw new Error(`Unsupported prepared assessment purpose ${params.input.purpose}`);
|
|
102787
|
+
}
|
|
102788
|
+
return { ...common2, purpose: "end_of_course" };
|
|
102789
|
+
}
|
|
102790
|
+
async buildReviewPreparationPlan(params, requestHash, preparation) {
|
|
102791
|
+
const { context: context2, decision, listing, tests } = preparation;
|
|
102792
|
+
if (tests.length !== 1) {
|
|
102793
|
+
throw new ValidationError("Standards review requires exactly one live review-bank assessment.");
|
|
102794
|
+
}
|
|
102795
|
+
const normalized = normalizeReviewRequest(params.input);
|
|
102796
|
+
const catalog = await this.loadReviewBankCatalog(decision.test.qtiTestIdentifier);
|
|
102797
|
+
const selected = selectReviewItems({
|
|
102798
|
+
request: params.input,
|
|
102799
|
+
bank: catalog.bank,
|
|
102800
|
+
exposures: this.reviewExposures(listing.reviewChildren.values(), {
|
|
102801
|
+
bankRevision: catalog.bank.bankRevision
|
|
102802
|
+
})
|
|
102803
|
+
});
|
|
102804
|
+
if (selected.selections.length === 0) {
|
|
102805
|
+
if (selected.fulfillment.status !== "partial") {
|
|
102806
|
+
throw new Error("A complete review selection cannot be empty");
|
|
102807
|
+
}
|
|
102808
|
+
throw AssessmentRuntimeError.from(assessmentReviewBankShortage(selected.fulfillment));
|
|
102809
|
+
}
|
|
102810
|
+
const artifact = await this.loadOrCompileReviewBankArtifact(catalog);
|
|
102811
|
+
const assessment = projectReviewAssessment(artifact.assessment, catalog.bank, selected.selections);
|
|
102812
|
+
const source = { assessment: artifact.assessment, bank: catalog.bank };
|
|
102813
|
+
const parentLineItemId = await this.provisionPreparationPrerequisites(() => this.ensureReviewBankLineItem({
|
|
102814
|
+
integration: context2.integration,
|
|
102815
|
+
activityId: params.input.activityId,
|
|
102816
|
+
source
|
|
102817
|
+
}));
|
|
102818
|
+
await this.provisionPreparationPrerequisites(() => runWithConcurrency(assessment.items, TimebackAssessmentRuntimeService.SCORING_CONCURRENCY, (item) => this.ensureReviewQuestionLineItem({
|
|
102819
|
+
parentLineItemId,
|
|
102820
|
+
integration: context2.integration,
|
|
102821
|
+
bank: catalog.bank,
|
|
102822
|
+
item
|
|
102823
|
+
})));
|
|
102824
|
+
const attempt = await this.provisionPreparationPrerequisites(() => this.claimAttemptId({
|
|
102825
|
+
gameId: params.gameId,
|
|
102826
|
+
studentId: params.studentId,
|
|
102827
|
+
courseId: context2.integration.courseId,
|
|
102828
|
+
activityId: params.input.activityId,
|
|
102829
|
+
purpose: params.input.purpose
|
|
102830
|
+
}, nextAssessmentAttemptNumber(listing.attempts.values())));
|
|
102831
|
+
const plan = {
|
|
102832
|
+
version: 1,
|
|
102833
|
+
gameId: params.gameId,
|
|
102834
|
+
studentId: params.studentId,
|
|
102835
|
+
activityId: params.input.activityId,
|
|
102836
|
+
purpose: params.input.purpose,
|
|
102837
|
+
requestHash,
|
|
102838
|
+
preparedAt: Date.now(),
|
|
102839
|
+
integrationId: context2.integration.id,
|
|
102840
|
+
enrollmentId: context2.enrollment.id,
|
|
102841
|
+
courseId: context2.integration.courseId,
|
|
102842
|
+
...attempt,
|
|
102843
|
+
parentLineItemId,
|
|
102844
|
+
selectedTest: {
|
|
102845
|
+
assessmentKey: decision.test.assessmentKey,
|
|
102846
|
+
identifier: artifact.assessment.identifier,
|
|
102847
|
+
contentRevision: artifact.assessment.contentRevision
|
|
102848
|
+
},
|
|
102849
|
+
review: {
|
|
102850
|
+
bankRevision: catalog.bank.bankRevision,
|
|
102851
|
+
standards: [...normalized.standards],
|
|
102852
|
+
candidateItemsPerStandard: normalized.candidateItemsPerStandard,
|
|
102853
|
+
selections: [...selected.selections],
|
|
102854
|
+
fulfillment: selected.fulfillment
|
|
102855
|
+
}
|
|
102856
|
+
};
|
|
102857
|
+
return { plan, artifactSource: artifact.source };
|
|
102858
|
+
}
|
|
102859
|
+
async tryPreparedStart(params, receipt) {
|
|
102860
|
+
const startedAt = Date.now();
|
|
102861
|
+
let plan;
|
|
102862
|
+
try {
|
|
102863
|
+
const verified = await this.deps.verifyAssessmentPreparationReceipt(receipt);
|
|
102864
|
+
if (!verified || !await assessmentPreparationReceiptMatches(verified, params)) {
|
|
102865
|
+
this.recordPreparedStart("fallback", params.input.purpose, startedAt, {
|
|
102866
|
+
reason: "receipt_mismatch"
|
|
102867
|
+
});
|
|
102868
|
+
return null;
|
|
102869
|
+
}
|
|
102870
|
+
plan = verified;
|
|
102871
|
+
const assessment = await this.loadPreparedPlanAssessment(verified);
|
|
102872
|
+
if (!assessment) {
|
|
102873
|
+
this.recordPreparedStart("fallback", params.input.purpose, startedAt, {
|
|
102874
|
+
reason: "artifact_unavailable",
|
|
102875
|
+
plan
|
|
102876
|
+
});
|
|
102877
|
+
return null;
|
|
102878
|
+
}
|
|
102879
|
+
const outcome = await this.deps.assessmentRuntimeLock(this.deps.db, [
|
|
102880
|
+
assessmentRuntimeStartLockKey({
|
|
102881
|
+
studentId: params.studentId,
|
|
102882
|
+
activityId: params.input.activityId,
|
|
102883
|
+
purpose: params.input.purpose,
|
|
102884
|
+
courseId: verified.courseId
|
|
102885
|
+
})
|
|
102886
|
+
], async (db2) => {
|
|
102887
|
+
const candidates = await this.candidateIntegrations(verified.gameId, verified.studentId, params.input, db2);
|
|
102888
|
+
const integration = preparedAssessmentIntegration(verified, candidates);
|
|
102889
|
+
if (!integration || !await this.preparedPlanTestIsLive(verified, db2)) {
|
|
102890
|
+
return null;
|
|
102891
|
+
}
|
|
102892
|
+
if (await this.attemptIdOccupied(verified.attemptId)) {
|
|
102893
|
+
return null;
|
|
102894
|
+
}
|
|
102895
|
+
return this.createPreparedAttempt(verified, assessment, integration);
|
|
102896
|
+
});
|
|
102897
|
+
if (!outcome) {
|
|
102898
|
+
this.recordPreparedStart("fallback", params.input.purpose, startedAt, {
|
|
102899
|
+
reason: "stale_plan",
|
|
102900
|
+
plan
|
|
102901
|
+
});
|
|
102902
|
+
return null;
|
|
102903
|
+
}
|
|
102904
|
+
this.recordPreparedStart("started", params.input.purpose, startedAt, { plan });
|
|
102905
|
+
return outcome;
|
|
102906
|
+
} catch (error88) {
|
|
102907
|
+
this.recordPreparedStart("fallback", params.input.purpose, startedAt, {
|
|
102908
|
+
reason: "prepared_start_failed",
|
|
102909
|
+
error: error88,
|
|
102910
|
+
...plan ? { plan } : {}
|
|
102911
|
+
});
|
|
102912
|
+
return null;
|
|
102913
|
+
}
|
|
102914
|
+
}
|
|
101199
102915
|
async startDiagnostic({
|
|
101200
102916
|
gameId,
|
|
101201
102917
|
studentId,
|
|
@@ -101850,7 +103566,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
101850
103566
|
let scoring = null;
|
|
101851
103567
|
if (loaded.state.status === "in-progress" && expected?.nodeKey === input.routingNodeKey && expected.itemIdentifier === input.itemIdentifier && attempt.metadata.responseVersion === input.expectedResponseVersion) {
|
|
101852
103568
|
const responses = prepareDiagnosticAssessmentResponses(assessment, attempt.metadata.responses, input);
|
|
101853
|
-
scoring = await this.scoreItem(assessment, responses, input.itemIdentifier);
|
|
103569
|
+
scoring = await this.scoreItem(assessment, responses, input.itemIdentifier, assessmentArtifactIdentityFromAttempt(attempt.metadata));
|
|
101854
103570
|
}
|
|
101855
103571
|
return {
|
|
101856
103572
|
responseVersion: attempt.metadata.responseVersion,
|
|
@@ -101861,7 +103577,8 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
101861
103577
|
scoring
|
|
101862
103578
|
};
|
|
101863
103579
|
}
|
|
101864
|
-
async scoreItems(assessment, responses, itemIdentifiers) {
|
|
103580
|
+
async scoreItems(assessment, responses, artifactIdentity, itemIdentifiers) {
|
|
103581
|
+
await this.artifactLoader.loadMatchedScoring(assessment, artifactIdentity);
|
|
101865
103582
|
const client2 = this.requireClient();
|
|
101866
103583
|
const scoringItems = assessmentScoringRequests(assessment, responses).filter((item) => !itemIdentifiers || itemIdentifiers.has(item.itemIdentifier));
|
|
101867
103584
|
const scoringTasks = scoringItems.flatMap((item, itemIndex) => item.requests.map((request) => ({
|
|
@@ -101891,8 +103608,8 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
101891
103608
|
};
|
|
101892
103609
|
});
|
|
101893
103610
|
}
|
|
101894
|
-
async scoreItem(assessment, responses, itemIdentifier) {
|
|
101895
|
-
const [scoring] = await this.scoreItems(assessment, responses, new Set([itemIdentifier]));
|
|
103611
|
+
async scoreItem(assessment, responses, itemIdentifier, artifactIdentity) {
|
|
103612
|
+
const [scoring] = await this.scoreItems(assessment, responses, artifactIdentity, new Set([itemIdentifier]));
|
|
101896
103613
|
return scoring;
|
|
101897
103614
|
}
|
|
101898
103615
|
async prepareItemSubmission(params, input) {
|
|
@@ -101920,7 +103637,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
101920
103637
|
itemSubmissions: attempt.metadata.itemSubmissions,
|
|
101921
103638
|
assessment
|
|
101922
103639
|
}, input);
|
|
101923
|
-
const scoring = transition.action === "commit" ? await this.scoreItem(assessment, transition.responses, input.itemIdentifier) : null;
|
|
103640
|
+
const scoring = transition.action === "commit" ? await this.scoreItem(assessment, transition.responses, input.itemIdentifier, assessmentArtifactIdentityFromAttempt(attempt.metadata)) : null;
|
|
101924
103641
|
return {
|
|
101925
103642
|
responseVersion: attempt.metadata.responseVersion,
|
|
101926
103643
|
submissionId: input.submissionId,
|
|
@@ -101933,7 +103650,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
101933
103650
|
const assessment = await this.loadAttemptAssessment(attempt.metadata);
|
|
101934
103651
|
validateAssessmentResponses(assessment, attempt.metadata.responses);
|
|
101935
103652
|
const administeredItems = attempt.metadata.purpose === "review" ? administeredItemIdentifiers(attempt.metadata.itemSubmissions) : undefined;
|
|
101936
|
-
const itemResults = await this.scoreItems(assessment, attempt.metadata.responses, administeredItems);
|
|
103653
|
+
const itemResults = await this.scoreItems(assessment, attempt.metadata.responses, assessmentArtifactIdentityFromAttempt(attempt.metadata), administeredItems);
|
|
101937
103654
|
return {
|
|
101938
103655
|
responseVersion: attempt.metadata.responseVersion,
|
|
101939
103656
|
assessment,
|
|
@@ -102425,23 +104142,24 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
102425
104142
|
liveTestIntegrationIds: integrationIdsWithLiveTests
|
|
102426
104143
|
};
|
|
102427
104144
|
}
|
|
102428
|
-
async selectIntegration(candidates, studentId, input) {
|
|
104145
|
+
async selectIntegration(candidates, studentId, input, results) {
|
|
102429
104146
|
const {
|
|
102430
104147
|
rows: candidateRows,
|
|
102431
104148
|
enrollmentByCourse,
|
|
102432
104149
|
liveTestIntegrationIds: integrationIdsWithLiveTests
|
|
102433
104150
|
} = candidates;
|
|
102434
104151
|
const needsOpenAnnotation = candidateRows.length > 1 || candidateRows.some((row) => !integrationIdsWithLiveTests.has(row.id));
|
|
102435
|
-
const
|
|
104152
|
+
const openAttemptScope = {
|
|
102436
104153
|
studentId,
|
|
102437
104154
|
activityId: input.activityId,
|
|
102438
104155
|
purpose: input.purpose,
|
|
102439
|
-
enrollmentIdByCourse: new Map(candidateRows.map((row) => [
|
|
102440
|
-
row.courseId,
|
|
102441
|
-
enrollmentByCourse.get(row.courseId).id
|
|
102442
|
-
])),
|
|
104156
|
+
enrollmentIdByCourse: new Map(candidateRows.map((row) => [row.courseId, enrollmentByCourse.get(row.courseId).id])),
|
|
102443
104157
|
requestFingerprint: this.startRequestFingerprint(input)
|
|
102444
|
-
}
|
|
104158
|
+
};
|
|
104159
|
+
let openAttemptCourseIds = new Set;
|
|
104160
|
+
if (needsOpenAnnotation) {
|
|
104161
|
+
openAttemptCourseIds = results ? this.openAttemptCourseIdsFromResults(results, openAttemptScope) : await this.openAttemptCourseIds(openAttemptScope);
|
|
104162
|
+
}
|
|
102445
104163
|
const annotated = candidateRows.map((row) => ({
|
|
102446
104164
|
...row,
|
|
102447
104165
|
hasLiveTests: integrationIdsWithLiveTests.has(row.id),
|
|
@@ -102471,20 +104189,28 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
102471
104189
|
id: row.id,
|
|
102472
104190
|
assessmentKey: row.assessmentKey,
|
|
102473
104191
|
qtiTestIdentifier: row.qtiTestIdentifier,
|
|
102474
|
-
sortOrder: row.sortOrder,
|
|
102475
|
-
updatedAt: row.updatedAt.toISOString(),
|
|
102476
104192
|
standard: assessmentStandardForRow(row)
|
|
102477
104193
|
})).filter((test) => !matchesMasteryStandard || matchesMasteryStandard(test.standard));
|
|
102478
104194
|
}
|
|
102479
104195
|
async openAttemptCourseIds(scope) {
|
|
102480
|
-
|
|
104196
|
+
return this.openAttemptCourseIdsFromResults(await this.studentAssessmentResults(scope.studentId), scope);
|
|
104197
|
+
}
|
|
104198
|
+
async studentAssessmentResults(studentId) {
|
|
104199
|
+
const stream = this.requireClient().api.oneroster.assessmentResults.stream({
|
|
102481
104200
|
where: {
|
|
102482
|
-
"student.sourcedId":
|
|
104201
|
+
"student.sourcedId": studentId,
|
|
102483
104202
|
status: ONEROSTER_STATUS2.active
|
|
102484
104203
|
}
|
|
102485
104204
|
});
|
|
104205
|
+
const results = [];
|
|
104206
|
+
for await (const result of stream) {
|
|
104207
|
+
results.push(result);
|
|
104208
|
+
}
|
|
104209
|
+
return results;
|
|
104210
|
+
}
|
|
104211
|
+
openAttemptCourseIdsFromResults(results, scope) {
|
|
102486
104212
|
const courseIds = new Set;
|
|
102487
|
-
for
|
|
104213
|
+
for (const result of results) {
|
|
102488
104214
|
const metadata2 = playcademyAssessmentResultMetadata(result.metadata);
|
|
102489
104215
|
if (metadata2 && metadata2.activityId === scope.activityId && metadata2.purpose === scope.purpose && this.attemptMatchesRequestFingerprint(metadata2, scope.requestFingerprint) && result.student.sourcedId === scope.studentId && metadata2.enrollmentId === scope.enrollmentIdByCourse.get(metadata2.courseId) && (this.isOpen(result) || this.isAwaitingAward(result))) {
|
|
102490
104216
|
courseIds.add(metadata2.courseId);
|
|
@@ -102493,15 +104219,12 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
102493
104219
|
return courseIds;
|
|
102494
104220
|
}
|
|
102495
104221
|
async listAttempts(scope) {
|
|
102496
|
-
|
|
102497
|
-
|
|
102498
|
-
|
|
102499
|
-
status: ONEROSTER_STATUS2.active
|
|
102500
|
-
}
|
|
102501
|
-
});
|
|
104222
|
+
return this.attemptListing(await this.studentAssessmentResults(scope.studentId), scope);
|
|
104223
|
+
}
|
|
104224
|
+
attemptListing(results, scope) {
|
|
102502
104225
|
const attempts = new Map;
|
|
102503
104226
|
const reviewChildren = new Map;
|
|
102504
|
-
for
|
|
104227
|
+
for (const result of results) {
|
|
102505
104228
|
const metadata2 = playcademyAssessmentResultMetadata(result.metadata);
|
|
102506
104229
|
const childMetadata = scope.purpose === "review" ? playcademyAssessmentItemResultMetadata(result.metadata) : null;
|
|
102507
104230
|
if (childMetadata && childMetadata.activityId === scope.activityId && childMetadata.integrationId === scope.integrationId && childMetadata.enrollmentId === scope.enrollmentId && result.student.sourcedId === scope.studentId) {
|
|
@@ -102713,6 +104436,57 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
102713
104436
|
...counts.selectedCount === undefined ? {} : { "app.assessment.selected_count": counts.selectedCount }
|
|
102714
104437
|
});
|
|
102715
104438
|
}
|
|
104439
|
+
async loadOrCompileManagedArtifact(identifier, diagnosticManifest) {
|
|
104440
|
+
const playable = await this.artifactLoader.loadManagedPlayable(identifier);
|
|
104441
|
+
if (playable) {
|
|
104442
|
+
const identity = {
|
|
104443
|
+
kind: "assessment",
|
|
104444
|
+
qtiTestIdentifier: identifier,
|
|
104445
|
+
contentRevision: playable.contentRevision
|
|
104446
|
+
};
|
|
104447
|
+
const scoring = await this.artifactLoader.loadMatchedScoring(playable, identity);
|
|
104448
|
+
const expectedRoutingRevision = diagnosticManifest ? await diagnosticRoutingRevision(diagnosticManifest) : undefined;
|
|
104449
|
+
if (scoring && (expectedRoutingRevision === undefined || scoring.diagnostic?.routingRevision === expectedRoutingRevision)) {
|
|
104450
|
+
return { assessment: playable, source: "existing" };
|
|
104451
|
+
}
|
|
104452
|
+
}
|
|
104453
|
+
const source = await loadHydratedQtiTest(this.requireClient(), identifier);
|
|
104454
|
+
const compiled = await compileManagedAssessmentArtifacts({
|
|
104455
|
+
source,
|
|
104456
|
+
diagnosticRoutingManifest: diagnosticManifest
|
|
104457
|
+
});
|
|
104458
|
+
return this.storeCompiledArtifact(compiled, `assessment artifacts for ${identifier}`);
|
|
104459
|
+
}
|
|
104460
|
+
async loadOrCompileReviewBankArtifact(catalog) {
|
|
104461
|
+
const identity = {
|
|
104462
|
+
kind: "review-bank",
|
|
104463
|
+
qtiTestIdentifier: catalog.bank.bankIdentifier,
|
|
104464
|
+
sourceContentRevision: catalog.bank.sourceContentRevision,
|
|
104465
|
+
bankRevision: catalog.bank.bankRevision
|
|
104466
|
+
};
|
|
104467
|
+
const playable = await this.artifactLoader.loadPlayable(identity);
|
|
104468
|
+
const scoring = playable ? await this.artifactLoader.loadMatchedScoring(playable, identity) : null;
|
|
104469
|
+
if (playable && scoring) {
|
|
104470
|
+
return { assessment: playable, source: "existing" };
|
|
104471
|
+
}
|
|
104472
|
+
const itemIdentifiers = catalog.bank.items.map((item) => item.itemIdentifier);
|
|
104473
|
+
const questions = await hydrateQtiTestQuestionSelection(this.requireClient(), catalog.references, itemIdentifiers);
|
|
104474
|
+
const compiled = await compileReviewBankArtifacts({
|
|
104475
|
+
source: { test: catalog.test, questions },
|
|
104476
|
+
bank: catalog.bank
|
|
104477
|
+
});
|
|
104478
|
+
return this.storeCompiledArtifact(compiled, `review-bank artifacts for ${catalog.bank.bankIdentifier}`);
|
|
104479
|
+
}
|
|
104480
|
+
async storeCompiledArtifact(compiled, label) {
|
|
104481
|
+
const assessment = await storeAndVerifyCompiledAssessmentArtifacts({
|
|
104482
|
+
compiled,
|
|
104483
|
+
label,
|
|
104484
|
+
store: (pair) => this.deps.assessmentArtifacts.storePair(pair),
|
|
104485
|
+
loadPlayable: (identity) => this.artifactLoader.loadPlayable(identity),
|
|
104486
|
+
loadMatchedScoring: (playable, identity) => this.artifactLoader.loadMatchedScoring(playable, identity)
|
|
104487
|
+
});
|
|
104488
|
+
return { assessment, source: "compiled" };
|
|
104489
|
+
}
|
|
102716
104490
|
async loadReviewBankCatalog(identifier, expectedSourceRevision) {
|
|
102717
104491
|
if (expectedSourceRevision) {
|
|
102718
104492
|
const cached3 = this.reviewBankCache.get(`${identifier}\x00${expectedSourceRevision}`);
|
|
@@ -102765,6 +104539,16 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
102765
104539
|
return assessment;
|
|
102766
104540
|
}
|
|
102767
104541
|
async hydrateReviewBankSelection(catalog, itemIdentifiers) {
|
|
104542
|
+
const identity = {
|
|
104543
|
+
kind: "review-bank",
|
|
104544
|
+
qtiTestIdentifier: catalog.bank.bankIdentifier,
|
|
104545
|
+
sourceContentRevision: catalog.bank.sourceContentRevision,
|
|
104546
|
+
bankRevision: catalog.bank.bankRevision
|
|
104547
|
+
};
|
|
104548
|
+
const artifact = await this.artifactLoader.loadPlayableContainingItems(identity, itemIdentifiers);
|
|
104549
|
+
if (artifact) {
|
|
104550
|
+
return { assessment: artifact, bank: catalog.bank };
|
|
104551
|
+
}
|
|
102768
104552
|
const assessment = await this.cachedReviewSelection({
|
|
102769
104553
|
identifier: catalog.test.identifier,
|
|
102770
104554
|
contentRevision: catalog.bank.sourceContentRevision,
|
|
@@ -102778,6 +104562,14 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
102778
104562
|
}
|
|
102779
104563
|
async loadPinnedReviewBankSource(metadata2) {
|
|
102780
104564
|
const itemIdentifiers = metadata2.review.selections.map((selection) => selection.itemIdentifier);
|
|
104565
|
+
const identity = assessmentArtifactIdentityFromAttempt(metadata2);
|
|
104566
|
+
const artifact = await this.artifactLoader.loadPlayableContainingItems(identity, itemIdentifiers);
|
|
104567
|
+
if (artifact) {
|
|
104568
|
+
return {
|
|
104569
|
+
assessment: artifact,
|
|
104570
|
+
bank: pinnedReviewBankFromAttempt(metadata2, artifact.identifier)
|
|
104571
|
+
};
|
|
104572
|
+
}
|
|
102781
104573
|
let assessment;
|
|
102782
104574
|
try {
|
|
102783
104575
|
assessment = await this.cachedReviewSelection({
|
|
@@ -102803,18 +104595,19 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
102803
104595
|
}
|
|
102804
104596
|
return {
|
|
102805
104597
|
assessment,
|
|
102806
|
-
bank:
|
|
102807
|
-
bankIdentifier: assessment.identifier,
|
|
102808
|
-
sourceContentRevision: metadata2.selectedTest.contentRevision,
|
|
102809
|
-
bankRevision: metadata2.review.bankRevision,
|
|
102810
|
-
items: metadata2.review.selections.map((selection) => ({
|
|
102811
|
-
itemIdentifier: selection.itemIdentifier,
|
|
102812
|
-
standards: [selection.standard]
|
|
102813
|
-
}))
|
|
102814
|
-
}
|
|
104598
|
+
bank: pinnedReviewBankFromAttempt(metadata2, assessment.identifier)
|
|
102815
104599
|
};
|
|
102816
104600
|
}
|
|
102817
104601
|
async loadAssessment(identifier, expectedRevision) {
|
|
104602
|
+
const artifact = expectedRevision ? await this.artifactLoader.loadPlayable({
|
|
104603
|
+
kind: "assessment",
|
|
104604
|
+
qtiTestIdentifier: identifier,
|
|
104605
|
+
contentRevision: expectedRevision
|
|
104606
|
+
}) : await this.artifactLoader.loadManagedPlayable(identifier);
|
|
104607
|
+
if (artifact) {
|
|
104608
|
+
return artifact;
|
|
104609
|
+
}
|
|
104610
|
+
this.artifactLoader.recordFallback("playable", "assessment");
|
|
102818
104611
|
if (expectedRevision) {
|
|
102819
104612
|
const cached3 = this.assessmentCache.get(`${identifier}\x00${expectedRevision}`);
|
|
102820
104613
|
if (cached3) {
|
|
@@ -103582,6 +105375,72 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
103582
105375
|
scoreFromResult(result, possible, metadata2) {
|
|
103583
105376
|
return metadata2.score ?? assessmentScoreFromPercentage(result.score, possible);
|
|
103584
105377
|
}
|
|
105378
|
+
recordPreparation(outcome, purpose, startedAt, details = {}) {
|
|
105379
|
+
addEvent("assessment.preparation", {
|
|
105380
|
+
"app.assessment.purpose": purpose,
|
|
105381
|
+
"app.assessment.preparation_outcome": outcome,
|
|
105382
|
+
"app.assessment.duration_ms": Date.now() - startedAt,
|
|
105383
|
+
...details.reason ? { "app.assessment.reason": details.reason } : {},
|
|
105384
|
+
...details.artifactSource ? { "app.assessment.artifact_source": details.artifactSource } : {},
|
|
105385
|
+
...details.error ? {
|
|
105386
|
+
"exception.type": errorType(details.error),
|
|
105387
|
+
"app.error.message": errorMessage(details.error)
|
|
105388
|
+
} : {}
|
|
105389
|
+
});
|
|
105390
|
+
}
|
|
105391
|
+
recordPreparedStart(outcome, purpose, startedAt, details = {}) {
|
|
105392
|
+
addEvent("assessment.prepared_start", {
|
|
105393
|
+
"app.assessment.purpose": purpose,
|
|
105394
|
+
"app.assessment.prepared_start_outcome": outcome,
|
|
105395
|
+
"app.assessment.duration_ms": Date.now() - startedAt,
|
|
105396
|
+
...details.reason ? { "app.assessment.reason": details.reason } : {},
|
|
105397
|
+
...details.plan ? {
|
|
105398
|
+
"app.assessment.preparation_age_ms": Math.max(0, Date.now() - details.plan.preparedAt)
|
|
105399
|
+
} : {},
|
|
105400
|
+
...details.error ? {
|
|
105401
|
+
"exception.type": errorType(details.error),
|
|
105402
|
+
"app.error.message": errorMessage(details.error)
|
|
105403
|
+
} : {}
|
|
105404
|
+
});
|
|
105405
|
+
}
|
|
105406
|
+
async preparedPlanTestIsLive(plan, db2) {
|
|
105407
|
+
const tests = await this.liveTests(plan.integrationId, plan.purpose, db2, plan.purpose === "mastery" ? plan.mastery.standard : undefined, plan.purpose === "diagnostic" ? plan.diagnostic.diagnosticKey : undefined);
|
|
105408
|
+
return preparedAssessmentTestIsLive(plan, tests);
|
|
105409
|
+
}
|
|
105410
|
+
async loadPreparedPlanAssessment(plan) {
|
|
105411
|
+
const identity = preparedAssessmentArtifactIdentity(plan);
|
|
105412
|
+
const playable = plan.purpose === "review" ? await this.artifactLoader.loadPlayableContainingItems(identity, plan.review.selections.map((selection) => selection.itemIdentifier)) : await this.artifactLoader.loadPlayable(identity);
|
|
105413
|
+
if (!playable) {
|
|
105414
|
+
return null;
|
|
105415
|
+
}
|
|
105416
|
+
const scoring = await this.artifactLoader.loadMatchedScoring(playable, identity);
|
|
105417
|
+
if (!preparedAssessmentScoringIsCompatible(plan, scoring)) {
|
|
105418
|
+
return null;
|
|
105419
|
+
}
|
|
105420
|
+
if (plan.purpose !== "review") {
|
|
105421
|
+
return playable;
|
|
105422
|
+
}
|
|
105423
|
+
return projectReviewAssessment(playable, preparedReviewBankIndex(plan), plan.review.selections);
|
|
105424
|
+
}
|
|
105425
|
+
async createPreparedAttempt(plan, assessment, integration) {
|
|
105426
|
+
const timestamp6 = new Date().toISOString();
|
|
105427
|
+
const metadata2 = buildPreparedAssessmentMetadata(plan, timestamp6, assessment.title);
|
|
105428
|
+
const result = await this.requireClient().api.oneroster.assessmentResults.upsert(plan.attemptId, {
|
|
105429
|
+
status: ONEROSTER_STATUS2.active,
|
|
105430
|
+
assessmentLineItem: { sourcedId: plan.parentLineItemId },
|
|
105431
|
+
student: { sourcedId: plan.studentId },
|
|
105432
|
+
score: 0,
|
|
105433
|
+
scoreDate: timestamp6,
|
|
105434
|
+
scoreStatus: ASSESSMENT_ATTEMPT_OPEN.scoreStatus,
|
|
105435
|
+
inProgress: ASSESSMENT_ATTEMPT_OPEN.inProgress,
|
|
105436
|
+
metadata: buildAssessmentResultMetadata({
|
|
105437
|
+
metadata: metadata2,
|
|
105438
|
+
testName: assessment.title,
|
|
105439
|
+
grade: integration.grade
|
|
105440
|
+
})
|
|
105441
|
+
});
|
|
105442
|
+
return this.isPlatformRoutedDiagnosticMetadata(metadata2) ? this.diagnosticSnapshot(result, metadata2, assessment, integration, metadata2.diagnostic.state) : this.snapshot(result, metadata2, assessment, integration);
|
|
105443
|
+
}
|
|
103585
105444
|
async claimAttemptId(scope, from) {
|
|
103586
105445
|
for (let attemptNumber = from;attemptNumber < from + 20; attemptNumber += 1) {
|
|
103587
105446
|
const attemptId = await assessmentAttemptId({ ...scope, attemptNumber });
|
|
@@ -103737,6 +105596,35 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
103737
105596
|
};
|
|
103738
105597
|
});
|
|
103739
105598
|
|
|
105599
|
+
// ../api-core/src/utils/assessment-artifact-fallback.util.ts
|
|
105600
|
+
function assessmentArtifactFailureFallback(error88) {
|
|
105601
|
+
if (error88 instanceof ValidationError || error88 instanceof ConflictError || error88 instanceof AssessmentRuntimeError) {
|
|
105602
|
+
throw error88;
|
|
105603
|
+
}
|
|
105604
|
+
return false;
|
|
105605
|
+
}
|
|
105606
|
+
function assessmentArtifactPublicationWarning(artifactsAvailable) {
|
|
105607
|
+
return artifactsAvailable ? undefined : ASSESSMENT_ARTIFACTS_UNAVAILABLE_WARNING;
|
|
105608
|
+
}
|
|
105609
|
+
function assessmentArtifactImportMessage(message, artifactsAvailable) {
|
|
105610
|
+
return artifactsAvailable ? message : `${message} Optimized ${ARTIFACTS_UNAVAILABLE_DETAIL}`;
|
|
105611
|
+
}
|
|
105612
|
+
function assessmentArtifactReviewBankWarnings(warnings, artifactsAvailable) {
|
|
105613
|
+
return artifactsAvailable ? warnings : [
|
|
105614
|
+
...warnings,
|
|
105615
|
+
{
|
|
105616
|
+
code: "ASSESSMENT_ARTIFACTS_UNAVAILABLE",
|
|
105617
|
+
message: REVIEW_BANK_ARTIFACTS_UNAVAILABLE_WARNING
|
|
105618
|
+
}
|
|
105619
|
+
];
|
|
105620
|
+
}
|
|
105621
|
+
var ARTIFACTS_UNAVAILABLE_DETAIL = "assessment artifacts are unavailable. Students will use the slower QTI fallback until compilation is repaired.", ASSESSMENT_ARTIFACTS_UNAVAILABLE_WARNING, REVIEW_BANK_ARTIFACTS_UNAVAILABLE_WARNING;
|
|
105622
|
+
var init_assessment_artifact_fallback_util = __esm(() => {
|
|
105623
|
+
init_errors2();
|
|
105624
|
+
ASSESSMENT_ARTIFACTS_UNAVAILABLE_WARNING = `Published successfully, but optimized ${ARTIFACTS_UNAVAILABLE_DETAIL}`;
|
|
105625
|
+
REVIEW_BANK_ARTIFACTS_UNAVAILABLE_WARNING = `Review bank synchronized successfully, but optimized ${ARTIFACTS_UNAVAILABLE_DETAIL}`;
|
|
105626
|
+
});
|
|
105627
|
+
|
|
103740
105628
|
// ../api-core/src/utils/timeback-assessment-diagnostic.util.ts
|
|
103741
105629
|
function diagnosticRoutingCapabilities(assessment, scoring) {
|
|
103742
105630
|
return assessment.items.map((item) => ({
|
|
@@ -104077,7 +105965,7 @@ class TimebackAssessmentsService {
|
|
|
104077
105965
|
assertManagedAssessmentIdentity(loaded.test, entry.assessmentKey);
|
|
104078
105966
|
assertAssessmentHasQuestions(loaded.questions.questions);
|
|
104079
105967
|
assertPlayableAssessmentImportQuestions(targetStatus, loaded.questions.questions.map(({ question }) => question));
|
|
104080
|
-
return { entry,
|
|
105968
|
+
return { entry, source: loaded };
|
|
104081
105969
|
} catch (error88) {
|
|
104082
105970
|
return { entry, error: error88 };
|
|
104083
105971
|
}
|
|
@@ -104090,7 +105978,6 @@ class TimebackAssessmentsService {
|
|
|
104090
105978
|
...test ? { title: test.title } : {},
|
|
104091
105979
|
purpose: entry.purpose,
|
|
104092
105980
|
...entry.standard ? { standard: entry.standard } : {},
|
|
104093
|
-
sourceSortOrder: entry.sortOrder,
|
|
104094
105981
|
...test && gameSlug ? { editable: isQtiTestOwnedByGame(test, gameSlug) } : {}
|
|
104095
105982
|
};
|
|
104096
105983
|
}
|
|
@@ -104098,7 +105985,7 @@ class TimebackAssessmentsService {
|
|
|
104098
105985
|
const pendingInserts = [];
|
|
104099
105986
|
for (const validation of input.validated) {
|
|
104100
105987
|
const { entry } = validation;
|
|
104101
|
-
const base = this.associationImportResultBase(entry, "
|
|
105988
|
+
const base = this.associationImportResultBase(entry, "source" in validation ? validation.source.test : undefined, input.gameSlug);
|
|
104102
105989
|
if ("error" in validation) {
|
|
104103
105990
|
input.results[entry.index] = {
|
|
104104
105991
|
...base,
|
|
@@ -104109,7 +105996,7 @@ class TimebackAssessmentsService {
|
|
|
104109
105996
|
const existingForKey = input.existingByKey.get(entry.assessmentKey);
|
|
104110
105997
|
const decision = planAssessmentAssociationImport(entry, existingForKey, input.existingByIdentifier.get(entry.qtiTestIdentifier));
|
|
104111
105998
|
if (decision.kind === "insert") {
|
|
104112
|
-
pendingInserts.push({ entry,
|
|
105999
|
+
pendingInserts.push({ entry, source: validation.source });
|
|
104113
106000
|
} else {
|
|
104114
106001
|
input.results[entry.index] = {
|
|
104115
106002
|
...base,
|
|
@@ -104141,16 +106028,19 @@ class TimebackAssessmentsService {
|
|
|
104141
106028
|
}
|
|
104142
106029
|
}
|
|
104143
106030
|
async insertImportedAssessments(input) {
|
|
104144
|
-
for (const { entry,
|
|
104145
|
-
const base = this.associationImportResultBase(entry, test, input.gameSlug);
|
|
106031
|
+
for (const { entry, source } of input.pending) {
|
|
106032
|
+
const base = this.associationImportResultBase(entry, source.test, input.gameSlug);
|
|
104146
106033
|
try {
|
|
106034
|
+
let artifactsAvailable = true;
|
|
106035
|
+
if (input.targetStatus === "live") {
|
|
106036
|
+
artifactsAvailable = await this.storeManagedArtifacts(source, managedAssessmentDiagnosticRoutingManifest(source.test, entry.assessmentKey));
|
|
106037
|
+
}
|
|
104147
106038
|
const [row] = await input.db.insert(gameTimebackAssessmentTests).values({
|
|
104148
106039
|
integrationId: input.integrationId,
|
|
104149
106040
|
assessmentKey: entry.assessmentKey,
|
|
104150
106041
|
qtiTestIdentifier: entry.qtiTestIdentifier,
|
|
104151
106042
|
purpose: entry.purpose,
|
|
104152
106043
|
status: input.targetStatus,
|
|
104153
|
-
sortOrder: entry.sortOrder,
|
|
104154
106044
|
standardFramework: entry.standard?.framework,
|
|
104155
106045
|
standardIdentifier: entry.standard?.identifier
|
|
104156
106046
|
}).onConflictDoNothing().returning();
|
|
@@ -104167,11 +106057,12 @@ class TimebackAssessmentsService {
|
|
|
104167
106057
|
throw new Error("Assessment association insert conflicted");
|
|
104168
106058
|
}
|
|
104169
106059
|
} else {
|
|
106060
|
+
const message = attachedAssessmentImportMessage(input.targetStatus, base.editable ?? false);
|
|
104170
106061
|
input.results[entry.index] = {
|
|
104171
106062
|
...base,
|
|
104172
106063
|
status: "created",
|
|
104173
106064
|
association: this.associationImportSummary(row),
|
|
104174
|
-
message:
|
|
106065
|
+
message: assessmentArtifactImportMessage(message, artifactsAvailable)
|
|
104175
106066
|
};
|
|
104176
106067
|
}
|
|
104177
106068
|
} catch (error88) {
|
|
@@ -104237,6 +106128,7 @@ class TimebackAssessmentsService {
|
|
|
104237
106128
|
assertQtiTestOwnedByGame(test, ownership.gameSlug);
|
|
104238
106129
|
await client2.qtiApi.assessmentTests.update(qtiTestIdentifier, buildQtiTestUpdateInput(test, input.title));
|
|
104239
106130
|
}
|
|
106131
|
+
let publicationWarning;
|
|
104240
106132
|
if (publishing) {
|
|
104241
106133
|
if (nextPurpose === "diagnostic" && !nextDiagnostic) {
|
|
104242
106134
|
throw new ValidationError("Platform-routed diagnostics require a diagnostic key and routing manifest before publication.");
|
|
@@ -104252,6 +106144,7 @@ class TimebackAssessmentsService {
|
|
|
104252
106144
|
if (publishing) {
|
|
104253
106145
|
const publication = await this.publishManagedAssessment(row, nextPurpose, nextDiagnostic?.routingManifest ?? null, loaded);
|
|
104254
106146
|
updates.qtiTestIdentifier = publication.qtiTestIdentifier;
|
|
106147
|
+
publicationWarning = publication.warning;
|
|
104255
106148
|
if (nextPurpose === "diagnostic") {
|
|
104256
106149
|
updates.diagnosticRoutingManifest = publication.diagnosticRoutingManifest;
|
|
104257
106150
|
}
|
|
@@ -104263,7 +106156,7 @@ class TimebackAssessmentsService {
|
|
|
104263
106156
|
updated = updatedRow ?? row;
|
|
104264
106157
|
}
|
|
104265
106158
|
setAttribute("app.assessment.operation", input.title !== undefined ? "update_test" : "update_association");
|
|
104266
|
-
return this.associationSummary(updated);
|
|
106159
|
+
return this.associationSummary(updated, publicationWarning);
|
|
104267
106160
|
});
|
|
104268
106161
|
} catch (error88) {
|
|
104269
106162
|
if (isUniqueViolation(error88)) {
|
|
@@ -104283,7 +106176,7 @@ class TimebackAssessmentsService {
|
|
|
104283
106176
|
if (plan.kind === "delete") {
|
|
104284
106177
|
await tx.delete(gameTimebackAssessmentTests).where(eq(gameTimebackAssessmentTests.id, row.id));
|
|
104285
106178
|
} else if (plan.kind === "archive") {
|
|
104286
|
-
await tx.update(gameTimebackAssessmentTests).set({ status: "archived",
|
|
106179
|
+
await tx.update(gameTimebackAssessmentTests).set({ status: "archived", updatedAt: new Date }).where(eq(gameTimebackAssessmentTests.id, row.id));
|
|
104287
106180
|
}
|
|
104288
106181
|
if (plan.kind !== "none") {
|
|
104289
106182
|
setAttribute("app.assessment.operation", plan.operation);
|
|
@@ -104291,27 +106184,6 @@ class TimebackAssessmentsService {
|
|
|
104291
106184
|
return { action: plan.action };
|
|
104292
106185
|
});
|
|
104293
106186
|
}
|
|
104294
|
-
async reorderAssessments(integrationId, purpose, testIdentifiers) {
|
|
104295
|
-
if (purpose === "review") {
|
|
104296
|
-
throw new ValidationError("The review bank is system-managed and cannot be reordered.");
|
|
104297
|
-
}
|
|
104298
|
-
await this.requireIntegration(integrationId);
|
|
104299
|
-
validateUniqueAssessmentIdentifiers(testIdentifiers);
|
|
104300
|
-
const updatedAt = new Date;
|
|
104301
|
-
await this.deps.db.transaction(async (tx) => {
|
|
104302
|
-
const liveRows = await tx.query.gameTimebackAssessmentTests.findMany({
|
|
104303
|
-
where: and(eq(gameTimebackAssessmentTests.integrationId, integrationId), eq(gameTimebackAssessmentTests.purpose, purpose), eq(gameTimebackAssessmentTests.status, "live"))
|
|
104304
|
-
});
|
|
104305
|
-
const orderedRows = orderLiveAssessmentRows(liveRows, purpose, testIdentifiers);
|
|
104306
|
-
const positionByRowId = new Map(orderedRows.map((row, index2) => [row.id, index2 + 1]));
|
|
104307
|
-
for (const row of lockOrderAssessmentRows(orderedRows)) {
|
|
104308
|
-
const testIdentifier = row.qtiTestIdentifier;
|
|
104309
|
-
const [updatedRow] = await tx.update(gameTimebackAssessmentTests).set({ sortOrder: positionByRowId.get(row.id), updatedAt }).where(and(eq(gameTimebackAssessmentTests.id, row.id), eq(gameTimebackAssessmentTests.integrationId, integrationId), eq(gameTimebackAssessmentTests.qtiTestIdentifier, testIdentifier), eq(gameTimebackAssessmentTests.purpose, purpose), eq(gameTimebackAssessmentTests.status, "live"))).returning({ id: gameTimebackAssessmentTests.id });
|
|
104310
|
-
assertAssessmentOrderUpdateSucceeded(updatedRow);
|
|
104311
|
-
}
|
|
104312
|
-
});
|
|
104313
|
-
setAttribute("app.assessment.operation", "reorder_live_assessments");
|
|
104314
|
-
}
|
|
104315
106187
|
async listQuestions(integrationId, qtiTestIdentifier) {
|
|
104316
106188
|
const client2 = this.requireClient();
|
|
104317
106189
|
await this.requireAssessmentRow(integrationId, qtiTestIdentifier);
|
|
@@ -104367,7 +106239,7 @@ class TimebackAssessmentsService {
|
|
|
104367
106239
|
assertReviewBankSinkOwnership(existingSink, integrationId);
|
|
104368
106240
|
}
|
|
104369
106241
|
let unchangedResult = null;
|
|
104370
|
-
if (existingSink) {
|
|
106242
|
+
if (existingSink?.title === PLAYCADEMY_REVIEW_BANK_TITLE) {
|
|
104371
106243
|
try {
|
|
104372
106244
|
unchangedResult = await unchangedReviewBankSynchronizationResult({
|
|
104373
106245
|
sink: existingSink,
|
|
@@ -104401,8 +106273,17 @@ class TimebackAssessmentsService {
|
|
|
104401
106273
|
const diff = reviewBankSynchronizationDiff({ existingSink, plan });
|
|
104402
106274
|
addedItemCount = diff.addedItemCount;
|
|
104403
106275
|
removedItemCount = diff.removedItemCount;
|
|
104404
|
-
catalogUnchanged =
|
|
106276
|
+
catalogUnchanged = false;
|
|
104405
106277
|
}
|
|
106278
|
+
const artifactTest = catalogUnchanged ? existingSink : buildReviewBankSinkInput({
|
|
106279
|
+
identifier: plan.bankIdentifier,
|
|
106280
|
+
title: PLAYCADEMY_REVIEW_BANK_TITLE,
|
|
106281
|
+
metadata: reviewBankSinkMetadata(integrationId, plan),
|
|
106282
|
+
itemIdentifiers: plan.itemIdentifiers,
|
|
106283
|
+
itemHref: (identifier) => qtiItemHref(client2, identifier),
|
|
106284
|
+
includeIdentifier: true
|
|
106285
|
+
});
|
|
106286
|
+
const artifactsAvailable = await this.storeReviewBankGeneration(client2, artifactTest, catalogUnchanged ? reviewBankTestItemIdentifiers(existingSink) : plan.itemIdentifiers);
|
|
104406
106287
|
const committed = await this.withLockedIntegrationAssessmentRows(integrationId, async (tx, lockedAssociations) => {
|
|
104407
106288
|
const lockedSources = this.reviewBankMasterySources(lockedAssociations);
|
|
104408
106289
|
const lockedFingerprint = await reviewBankContributorFingerprint(lockedSources);
|
|
@@ -104435,7 +106316,10 @@ class TimebackAssessmentsService {
|
|
|
104435
106316
|
unchanged,
|
|
104436
106317
|
warnings: plan.warnings
|
|
104437
106318
|
};
|
|
104438
|
-
return this.reviewMappingResult(
|
|
106319
|
+
return this.reviewMappingResult({
|
|
106320
|
+
...result,
|
|
106321
|
+
warnings: assessmentArtifactReviewBankWarnings(result.warnings, artifactsAvailable)
|
|
106322
|
+
});
|
|
104439
106323
|
});
|
|
104440
106324
|
if (committed) {
|
|
104441
106325
|
return committed;
|
|
@@ -104737,6 +106621,36 @@ class TimebackAssessmentsService {
|
|
|
104737
106621
|
};
|
|
104738
106622
|
});
|
|
104739
106623
|
}
|
|
106624
|
+
async storeReviewBankGeneration(client2, test, itemIdentifiers) {
|
|
106625
|
+
const startedAt = Date.now();
|
|
106626
|
+
const bank = await reviewBankIndexFromManifest(test.metadata, {
|
|
106627
|
+
bankIdentifier: test.identifier,
|
|
106628
|
+
membershipItemIdentifiers: itemIdentifiers
|
|
106629
|
+
});
|
|
106630
|
+
if (!bank) {
|
|
106631
|
+
throw new ValidationError(`Review bank ${test.identifier} has no revisioned mapping to compile`);
|
|
106632
|
+
}
|
|
106633
|
+
const identity = {
|
|
106634
|
+
kind: "review-bank",
|
|
106635
|
+
qtiTestIdentifier: bank.bankIdentifier,
|
|
106636
|
+
sourceContentRevision: bank.sourceContentRevision,
|
|
106637
|
+
bankRevision: bank.bankRevision
|
|
106638
|
+
};
|
|
106639
|
+
try {
|
|
106640
|
+
if (await this.deps.assessmentArtifacts.loadPair(identity)) {
|
|
106641
|
+
return true;
|
|
106642
|
+
}
|
|
106643
|
+
} catch (error88) {
|
|
106644
|
+
return this.handleArtifactFailure(test.identifier, "review-bank", startedAt, error88);
|
|
106645
|
+
}
|
|
106646
|
+
return this.compileAndStoreArtifacts(test.identifier, "review-bank", async () => compileReviewBankArtifacts({
|
|
106647
|
+
source: {
|
|
106648
|
+
test,
|
|
106649
|
+
questions: await hydrateQtiItemSelection(client2, test, itemIdentifiers)
|
|
106650
|
+
},
|
|
106651
|
+
bank
|
|
106652
|
+
}));
|
|
106653
|
+
}
|
|
104740
106654
|
async writeReviewBankSink(input) {
|
|
104741
106655
|
function sinkInput(title, includeIdentifier = false) {
|
|
104742
106656
|
return buildReviewBankSinkInput({
|
|
@@ -104749,11 +106663,11 @@ class TimebackAssessmentsService {
|
|
|
104749
106663
|
});
|
|
104750
106664
|
}
|
|
104751
106665
|
if (input.existingSink) {
|
|
104752
|
-
await input.client.qtiApi.assessmentTests.update(input.plan.bankIdentifier, sinkInput(
|
|
106666
|
+
await input.client.qtiApi.assessmentTests.update(input.plan.bankIdentifier, sinkInput(PLAYCADEMY_REVIEW_BANK_TITLE));
|
|
104753
106667
|
return;
|
|
104754
106668
|
}
|
|
104755
106669
|
try {
|
|
104756
|
-
const createInput = sinkInput(
|
|
106670
|
+
const createInput = sinkInput(PLAYCADEMY_REVIEW_BANK_TITLE, true);
|
|
104757
106671
|
await input.client.qtiApi.assessmentTests.create(createInput);
|
|
104758
106672
|
} catch (error88) {
|
|
104759
106673
|
if (!isApiError(error88) || error88.statusCode !== 409) {
|
|
@@ -104761,7 +106675,7 @@ class TimebackAssessmentsService {
|
|
|
104761
106675
|
}
|
|
104762
106676
|
const concurrentlyCreated = await input.client.qtiApi.assessmentTests.get(input.plan.bankIdentifier);
|
|
104763
106677
|
assertReviewBankSinkOwnership(concurrentlyCreated, input.integrationId);
|
|
104764
|
-
await input.client.qtiApi.assessmentTests.update(input.plan.bankIdentifier, sinkInput(
|
|
106678
|
+
await input.client.qtiApi.assessmentTests.update(input.plan.bankIdentifier, sinkInput(PLAYCADEMY_REVIEW_BANK_TITLE));
|
|
104765
106679
|
}
|
|
104766
106680
|
}
|
|
104767
106681
|
async installReviewBankAssociation(input) {
|
|
@@ -104774,15 +106688,14 @@ class TimebackAssessmentsService {
|
|
|
104774
106688
|
if (reservedKeyOwner && reservedKeyOwner.purpose !== "review") {
|
|
104775
106689
|
throw new ValidationError("The reserved review-bank assessment key is already in use.");
|
|
104776
106690
|
}
|
|
104777
|
-
const singletonCurrent = Boolean(singleton && singleton.assessmentKey === input.sinkIdentifier && singleton.status === "live" && singleton.
|
|
104778
|
-
const legacyCurrent = legacyReviews.every((association) => association.status === "archived" && association.
|
|
106691
|
+
const singletonCurrent = Boolean(singleton && singleton.assessmentKey === input.sinkIdentifier && singleton.status === "live" && singleton.standardFramework === null && singleton.standardIdentifier === null && singleton.diagnosticKey === null && singleton.diagnosticRoutingManifest === null);
|
|
106692
|
+
const legacyCurrent = legacyReviews.every((association) => association.status === "archived" && association.assessmentKey !== input.sinkIdentifier);
|
|
104779
106693
|
const changed = !singletonCurrent || !legacyCurrent;
|
|
104780
106694
|
await input.beforeInstall();
|
|
104781
106695
|
for (const legacy of legacyReviews) {
|
|
104782
|
-
if (legacy.status !== "archived" || legacy.
|
|
106696
|
+
if (legacy.status !== "archived" || legacy.assessmentKey === input.sinkIdentifier) {
|
|
104783
106697
|
await input.tx.update(gameTimebackAssessmentTests).set({
|
|
104784
106698
|
status: "archived",
|
|
104785
|
-
sortOrder: null,
|
|
104786
106699
|
...legacy.assessmentKey === input.sinkIdentifier ? { assessmentKey: null } : {},
|
|
104787
106700
|
updatedAt: new Date
|
|
104788
106701
|
}).where(eq(gameTimebackAssessmentTests.id, legacy.id));
|
|
@@ -104796,7 +106709,6 @@ class TimebackAssessmentsService {
|
|
|
104796
106709
|
assessmentKey: input.sinkIdentifier,
|
|
104797
106710
|
purpose: "review",
|
|
104798
106711
|
status: "live",
|
|
104799
|
-
sortOrder: null,
|
|
104800
106712
|
standardFramework: null,
|
|
104801
106713
|
standardIdentifier: null,
|
|
104802
106714
|
diagnosticKey: null,
|
|
@@ -104811,7 +106723,6 @@ class TimebackAssessmentsService {
|
|
|
104811
106723
|
qtiTestIdentifier: input.sinkIdentifier,
|
|
104812
106724
|
purpose: "review",
|
|
104813
106725
|
status: "live",
|
|
104814
|
-
sortOrder: null,
|
|
104815
106726
|
standardFramework: null,
|
|
104816
106727
|
standardIdentifier: null,
|
|
104817
106728
|
diagnosticKey: null,
|
|
@@ -104949,15 +106860,64 @@ class TimebackAssessmentsService {
|
|
|
104949
106860
|
return get();
|
|
104950
106861
|
}
|
|
104951
106862
|
async ensureManagedQtiPublication(client2, plan) {
|
|
104952
|
-
await runWithConcurrency(plan.items, QTI_HYDRATION_CONCURRENCY, async (plannedItem) => {
|
|
106863
|
+
const existingItems = await runWithConcurrency(plan.items, QTI_HYDRATION_CONCURRENCY, async (plannedItem) => {
|
|
104953
106864
|
const existing = await this.ensureImmutableQtiResource(async () => await client2.qtiApi.assessmentItems.get(plannedItem.qtiItemIdentifier), () => client2.course.createAssessmentItemXml({
|
|
104954
106865
|
xml: plannedItem.xml,
|
|
104955
106866
|
metadata: plannedItem.metadata
|
|
104956
106867
|
}));
|
|
104957
106868
|
await assertManagedAssessmentItem(existing, plannedItem);
|
|
106869
|
+
return existing;
|
|
104958
106870
|
});
|
|
104959
106871
|
const existingTest = await this.ensureImmutableQtiResource(async () => await client2.qtiApi.assessmentTests.get(plan.qtiTestIdentifier), () => client2.qtiApi.assessmentTests.create(plan.testInput));
|
|
104960
106872
|
assertManagedAssessmentPublication(existingTest, plan);
|
|
106873
|
+
return {
|
|
106874
|
+
test: existingTest,
|
|
106875
|
+
questions: {
|
|
106876
|
+
...plan.artifactSource.questions,
|
|
106877
|
+
assessmentTest: existingTest.identifier,
|
|
106878
|
+
title: existingTest.title,
|
|
106879
|
+
totalQuestions: existingItems.length,
|
|
106880
|
+
questions: plan.artifactSource.questions.questions.map((entry, index2) => ({
|
|
106881
|
+
...entry,
|
|
106882
|
+
reference: {
|
|
106883
|
+
...entry.reference,
|
|
106884
|
+
identifier: existingItems[index2].identifier
|
|
106885
|
+
},
|
|
106886
|
+
question: existingItems[index2]
|
|
106887
|
+
}))
|
|
106888
|
+
}
|
|
106889
|
+
};
|
|
106890
|
+
}
|
|
106891
|
+
async compileAndStoreArtifacts(qtiTestIdentifier, identityKind, compile) {
|
|
106892
|
+
const startedAt = Date.now();
|
|
106893
|
+
try {
|
|
106894
|
+
const compiled = await compile();
|
|
106895
|
+
const stored = await this.deps.assessmentArtifacts.storePair(compiled);
|
|
106896
|
+
addEvent("assessment.artifacts_compiled", {
|
|
106897
|
+
"app.assessment.qti_test_identifier": compiled.identity.qtiTestIdentifier,
|
|
106898
|
+
"app.assessment.artifact_identity_kind": compiled.identity.kind,
|
|
106899
|
+
"app.assessment.artifact_duration_ms": Date.now() - startedAt,
|
|
106900
|
+
"app.assessment.artifact_fallback_item_count": qtiFallbackItemCount(compiled),
|
|
106901
|
+
"app.assessment.playable_artifact_status": stored.playable.status,
|
|
106902
|
+
"app.assessment.scoring_artifact_status": stored.scoring.status
|
|
106903
|
+
});
|
|
106904
|
+
} catch (error88) {
|
|
106905
|
+
return this.handleArtifactFailure(qtiTestIdentifier, identityKind, startedAt, error88);
|
|
106906
|
+
}
|
|
106907
|
+
return true;
|
|
106908
|
+
}
|
|
106909
|
+
handleArtifactFailure(qtiTestIdentifier, identityKind, startedAt, error88) {
|
|
106910
|
+
addEvent("assessment.artifact_compilation_failed", {
|
|
106911
|
+
"app.assessment.qti_test_identifier": qtiTestIdentifier,
|
|
106912
|
+
"app.assessment.artifact_identity_kind": identityKind,
|
|
106913
|
+
"app.assessment.artifact_duration_ms": Date.now() - startedAt,
|
|
106914
|
+
"exception.type": errorType(error88),
|
|
106915
|
+
"app.error.message": errorMessage(error88)
|
|
106916
|
+
});
|
|
106917
|
+
return assessmentArtifactFailureFallback(error88);
|
|
106918
|
+
}
|
|
106919
|
+
async storeManagedArtifacts(source, diagnosticRoutingManifest) {
|
|
106920
|
+
return this.compileAndStoreArtifacts(source.test.identifier, "assessment", () => compileManagedAssessmentArtifacts({ source, diagnosticRoutingManifest }));
|
|
104961
106921
|
}
|
|
104962
106922
|
async publishManagedAssessment(row, purpose, diagnosticRoutingManifest, loaded) {
|
|
104963
106923
|
if (!row.assessmentKey) {
|
|
@@ -104972,8 +106932,10 @@ class TimebackAssessmentsService {
|
|
|
104972
106932
|
diagnosticRoutingManifest,
|
|
104973
106933
|
itemHref: (identifier) => qtiItemHref(client2, identifier)
|
|
104974
106934
|
});
|
|
104975
|
-
await this.ensureManagedQtiPublication(client2, plan);
|
|
104976
|
-
|
|
106935
|
+
const artifactSource = await this.ensureManagedQtiPublication(client2, plan);
|
|
106936
|
+
const artifactsAvailable = await this.storeManagedArtifacts(artifactSource, managedAssessmentDiagnosticRoutingManifest(artifactSource.test, plan.assessmentKey));
|
|
106937
|
+
const warning = assessmentArtifactPublicationWarning(artifactsAvailable);
|
|
106938
|
+
return { ...plan, ...warning ? { warning } : {} };
|
|
104977
106939
|
}
|
|
104978
106940
|
async cleanupQtiAssessmentCopy(client2, testIdentifier, itemIdentifiers) {
|
|
104979
106941
|
let testDeleted = !testIdentifier;
|
|
@@ -105006,7 +106968,7 @@ class TimebackAssessmentsService {
|
|
|
105006
106968
|
}
|
|
105007
106969
|
}
|
|
105008
106970
|
}
|
|
105009
|
-
associationSummary(row) {
|
|
106971
|
+
associationSummary(row, warning) {
|
|
105010
106972
|
return {
|
|
105011
106973
|
id: row.id,
|
|
105012
106974
|
integrationId: row.integrationId,
|
|
@@ -105014,11 +106976,11 @@ class TimebackAssessmentsService {
|
|
|
105014
106976
|
qtiTestIdentifier: row.qtiTestIdentifier,
|
|
105015
106977
|
purpose: row.purpose,
|
|
105016
106978
|
status: row.status,
|
|
105017
|
-
sortOrder: row.sortOrder,
|
|
105018
106979
|
standard: assessmentStandardForRow(row),
|
|
105019
106980
|
diagnostic: diagnosticDefinitionForRow(row),
|
|
105020
106981
|
createdAt: row.createdAt,
|
|
105021
|
-
updatedAt: row.updatedAt
|
|
106982
|
+
updatedAt: row.updatedAt,
|
|
106983
|
+
...warning ? { warning } : {}
|
|
105022
106984
|
};
|
|
105023
106985
|
}
|
|
105024
106986
|
associationImportSummary(row) {
|
|
@@ -105086,7 +107048,7 @@ class TimebackAssessmentsService {
|
|
|
105086
107048
|
};
|
|
105087
107049
|
}
|
|
105088
107050
|
}
|
|
105089
|
-
var
|
|
107051
|
+
var REVIEW_BANK_SYNC_ATTEMPTS = 2;
|
|
105090
107052
|
var init_timeback_assessments_service = __esm(async () => {
|
|
105091
107053
|
init_drizzle_orm();
|
|
105092
107054
|
init_helpers_index();
|
|
@@ -105095,6 +107057,8 @@ var init_timeback_assessments_service = __esm(async () => {
|
|
|
105095
107057
|
init_assessment_runtime2();
|
|
105096
107058
|
init_timeback3();
|
|
105097
107059
|
init_errors2();
|
|
107060
|
+
init_assessment_artifact_fallback_util();
|
|
107061
|
+
init_timeback_assessment_artifacts_util();
|
|
105098
107062
|
init_timeback_assessment_diagnostic_util();
|
|
105099
107063
|
init_timeback_assessment_import_util();
|
|
105100
107064
|
init_timeback_assessment_publication_util();
|
|
@@ -107118,6 +109082,8 @@ function createPlatformServices(deps) {
|
|
|
107118
109082
|
timebackClient,
|
|
107119
109083
|
alerts,
|
|
107120
109084
|
mintPlatformServiceToken,
|
|
109085
|
+
mintAssessmentPreparationReceipt,
|
|
109086
|
+
verifyAssessmentPreparationReceipt,
|
|
107121
109087
|
validateDeveloperAccessBySlug,
|
|
107122
109088
|
validateDeveloperAccess,
|
|
107123
109089
|
validateGameManagementAccess
|
|
@@ -107133,6 +109099,10 @@ function createPlatformServices(deps) {
|
|
|
107133
109099
|
environment: getPlatformEnvironment(config5),
|
|
107134
109100
|
storage
|
|
107135
109101
|
});
|
|
109102
|
+
const assessmentArtifacts = new AssessmentArtifactsService({
|
|
109103
|
+
bucketName: config5.assessmentArtifactBucket,
|
|
109104
|
+
storage
|
|
109105
|
+
});
|
|
107136
109106
|
const upload = new UploadService({
|
|
107137
109107
|
uploadBucket: config5.uploadBucket,
|
|
107138
109108
|
generatePresignedPutUrl: storage.generatePresignedPutUrl.bind(storage),
|
|
@@ -107178,16 +109148,21 @@ function createPlatformServices(deps) {
|
|
|
107178
109148
|
const timebackAssessments = new TimebackAssessmentsService({
|
|
107179
109149
|
db: db2,
|
|
107180
109150
|
timeback: timebackClient,
|
|
109151
|
+
assessmentArtifacts,
|
|
107181
109152
|
validateGameManagementAccess
|
|
107182
109153
|
});
|
|
107183
109154
|
const timebackAssessmentRuntime = new TimebackAssessmentRuntimeService({
|
|
107184
109155
|
db: db2,
|
|
107185
109156
|
timeback: timebackClient,
|
|
109157
|
+
assessmentArtifacts,
|
|
107186
109158
|
validateDeveloperAccess,
|
|
107187
|
-
assessmentRuntimeLock
|
|
109159
|
+
assessmentRuntimeLock,
|
|
109160
|
+
mintAssessmentPreparationReceipt,
|
|
109161
|
+
verifyAssessmentPreparationReceipt
|
|
107188
109162
|
});
|
|
107189
109163
|
return {
|
|
107190
109164
|
assessmentAssets,
|
|
109165
|
+
assessmentArtifacts,
|
|
107191
109166
|
bucket,
|
|
107192
109167
|
upload,
|
|
107193
109168
|
kv,
|
|
@@ -107204,6 +109179,7 @@ function createPlatformServices(deps) {
|
|
|
107204
109179
|
}
|
|
107205
109180
|
var init_platform2 = __esm(async () => {
|
|
107206
109181
|
init_config2();
|
|
109182
|
+
init_assessment_artifacts_service();
|
|
107207
109183
|
init_assessment_assets_service();
|
|
107208
109184
|
init_bucket_service();
|
|
107209
109185
|
init_database_service();
|
|
@@ -107930,6 +109906,8 @@ function createServices(ctx) {
|
|
|
107930
109906
|
timebackClient: timeback2,
|
|
107931
109907
|
alerts: infra2.alerts,
|
|
107932
109908
|
mintPlatformServiceToken: auth2.mintPlatformServiceToken.bind(auth2),
|
|
109909
|
+
mintAssessmentPreparationReceipt: auth2.mintAssessmentPreparationReceipt.bind(auth2),
|
|
109910
|
+
verifyAssessmentPreparationReceipt: auth2.verifyAssessmentPreparationReceipt.bind(auth2),
|
|
107933
109911
|
...gaming.validators
|
|
107934
109912
|
});
|
|
107935
109913
|
const standalone = createStandaloneServices({ db: db2, auth: auth2, timeback: timeback2 });
|
|
@@ -108069,6 +110047,15 @@ function defineControllerNames(namespace, handlers) {
|
|
|
108069
110047
|
return handlers;
|
|
108070
110048
|
}
|
|
108071
110049
|
|
|
110050
|
+
// ../api-core/src/utils/assessment-artifact-backfill.util.ts
|
|
110051
|
+
var init_assessment_artifact_backfill_util = __esm(() => {
|
|
110052
|
+
init_tables_index();
|
|
110053
|
+
init_timeback_assessment_artifacts_util();
|
|
110054
|
+
init_timeback_assessment_publication_util();
|
|
110055
|
+
init_timeback_qti_hydration_util();
|
|
110056
|
+
init_timeback_review_mapping_util();
|
|
110057
|
+
});
|
|
110058
|
+
|
|
108072
110059
|
// ../api-core/src/utils/lti.util.ts
|
|
108073
110060
|
function generateUsername(email5) {
|
|
108074
110061
|
const baseUsername = (email5.split("@")[0] || "user").toLowerCase();
|
|
@@ -108274,6 +110261,9 @@ var init_validation_util = __esm(() => {
|
|
|
108274
110261
|
var init_utils7 = __esm(() => {
|
|
108275
110262
|
init_auth_util();
|
|
108276
110263
|
init_assessment_runtime_lock_util();
|
|
110264
|
+
init_assessment_artifact_envelope_util();
|
|
110265
|
+
init_assessment_artifact_backfill_util();
|
|
110266
|
+
init_timeback_assessment_artifacts_util();
|
|
108277
110267
|
init_dashboard_util();
|
|
108278
110268
|
init_deployment_util();
|
|
108279
110269
|
init_leaderboard_util();
|
|
@@ -108431,6 +110421,32 @@ function createSandboxAuthProvider() {
|
|
|
108431
110421
|
}));
|
|
108432
110422
|
return `${header}.${payload}.sandbox`;
|
|
108433
110423
|
},
|
|
110424
|
+
async mintAssessmentPreparationReceipt(plan) {
|
|
110425
|
+
const now2 = Date.now();
|
|
110426
|
+
for (const [receipt2, prepared] of sandboxAssessmentPreparationReceipts) {
|
|
110427
|
+
if (prepared.expiresAt <= now2) {
|
|
110428
|
+
sandboxAssessmentPreparationReceipts.delete(receipt2);
|
|
110429
|
+
}
|
|
110430
|
+
}
|
|
110431
|
+
const receipt = `sandbox_assessment_preparation_${crypto.randomUUID()}`;
|
|
110432
|
+
const expiresAt = now2 + ASSESSMENT_PREPARATION_RECEIPT_LIFETIME_MS;
|
|
110433
|
+
sandboxAssessmentPreparationReceipts.set(receipt, {
|
|
110434
|
+
expiresAt,
|
|
110435
|
+
plan: structuredClone(plan)
|
|
110436
|
+
});
|
|
110437
|
+
return { receipt, expiresAt: new Date(expiresAt).toISOString() };
|
|
110438
|
+
},
|
|
110439
|
+
async verifyAssessmentPreparationReceipt(receipt) {
|
|
110440
|
+
const prepared = sandboxAssessmentPreparationReceipts.get(receipt);
|
|
110441
|
+
if (!prepared) {
|
|
110442
|
+
return null;
|
|
110443
|
+
}
|
|
110444
|
+
if (prepared.expiresAt <= Date.now()) {
|
|
110445
|
+
sandboxAssessmentPreparationReceipts.delete(receipt);
|
|
110446
|
+
return null;
|
|
110447
|
+
}
|
|
110448
|
+
return structuredClone(prepared.plan);
|
|
110449
|
+
},
|
|
108434
110450
|
async mintLogStreamToken(userId, workerId) {
|
|
108435
110451
|
const jti = crypto.randomUUID();
|
|
108436
110452
|
const payload = {
|
|
@@ -108472,11 +110488,13 @@ function createSandboxAuthProvider() {
|
|
|
108472
110488
|
}
|
|
108473
110489
|
};
|
|
108474
110490
|
}
|
|
108475
|
-
var sandboxApiKeys;
|
|
110491
|
+
var sandboxApiKeys, sandboxAssessmentPreparationReceipts, ASSESSMENT_PREPARATION_RECEIPT_LIFETIME_MS;
|
|
108476
110492
|
var init_auth_provider = __esm(() => {
|
|
108477
110493
|
init_src6();
|
|
108478
110494
|
init_constants();
|
|
108479
110495
|
sandboxApiKeys = new Map;
|
|
110496
|
+
sandboxAssessmentPreparationReceipts = new Map;
|
|
110497
|
+
ASSESSMENT_PREPARATION_RECEIPT_LIFETIME_MS = 5 * 60 * 1000;
|
|
108480
110498
|
});
|
|
108481
110499
|
|
|
108482
110500
|
// src/infrastructure/api/providers/cache.provider.ts
|
|
@@ -108651,6 +110669,7 @@ function buildConfig(options) {
|
|
|
108651
110669
|
baseUrl,
|
|
108652
110670
|
gameDomain: "localhost",
|
|
108653
110671
|
uploadBucket: "sandbox-uploads",
|
|
110672
|
+
assessmentArtifactBucket: "sandbox-assessment-artifacts",
|
|
108654
110673
|
ltiTestMode: true,
|
|
108655
110674
|
secretsManifestPepper: "sandbox-secrets-manifest-pepper",
|
|
108656
110675
|
...options.config
|
|
@@ -165108,7 +167127,7 @@ function parseQtiLibraryParams(searchParams) {
|
|
|
165108
167127
|
limit
|
|
165109
167128
|
};
|
|
165110
167129
|
}
|
|
165111
|
-
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, finalizeRuntimeAssessment, 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, synchronizeReviewBank,
|
|
167130
|
+
var populateStudent, getUser, getUserEnrollments, getUserById, setupIntegration, getIntegrations, getRemovedIntegrations, createIntegration, updateIntegration, deactivateCourse, reactivateCourse, getIntegrationConfig, verifyIntegration, getConfig, deleteIntegrations, endActivity, heartbeat, advanceCourse, unenrollCourse, prepareRuntimeAssessment, startRuntimeAssessment, getRuntimeAssessment, getLatestRuntimeAssessment, saveRuntimeAssessment, submitRuntimeAssessmentItem, submitRuntimeAssessment, finalizeRuntimeAssessment, 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, synchronizeReviewBank, reorderQuestions, removeAssessment, listQuestions, listQuestionLibrary, listTestLibrary, copyAssessment, createQuestion, updateQuestion, removeQuestion, timeback2;
|
|
165112
167131
|
var init_timeback_controller = __esm(() => {
|
|
165113
167132
|
init_esm();
|
|
165114
167133
|
init_src();
|
|
@@ -165352,13 +167371,24 @@ var init_timeback_controller = __esm(() => {
|
|
|
165352
167371
|
user: ctx.user
|
|
165353
167372
|
});
|
|
165354
167373
|
});
|
|
167374
|
+
prepareRuntimeAssessment = requireDeveloper(async (ctx) => {
|
|
167375
|
+
const body2 = await parseRequestBody(ctx.request, PrepareRuntimeAssessmentRequestSchema);
|
|
167376
|
+
const { gameId, studentId, ...input } = body2;
|
|
167377
|
+
return ctx.services.timebackAssessmentRuntime.prepare({
|
|
167378
|
+
gameId,
|
|
167379
|
+
studentId,
|
|
167380
|
+
input,
|
|
167381
|
+
user: ctx.user
|
|
167382
|
+
});
|
|
167383
|
+
});
|
|
165355
167384
|
startRuntimeAssessment = requireDeveloper(async (ctx) => {
|
|
165356
167385
|
const body2 = await parseRequestBody(ctx.request, StartRuntimeAssessmentRequestSchema);
|
|
165357
|
-
const { gameId, studentId, ...input } = body2;
|
|
167386
|
+
const { gameId, studentId, preparationReceipt, ...input } = body2;
|
|
165358
167387
|
return ctx.services.timebackAssessmentRuntime.start({
|
|
165359
167388
|
gameId,
|
|
165360
167389
|
studentId,
|
|
165361
167390
|
input,
|
|
167391
|
+
preparationReceipt,
|
|
165362
167392
|
user: ctx.user
|
|
165363
167393
|
});
|
|
165364
167394
|
});
|
|
@@ -165814,16 +167844,6 @@ var init_timeback_controller = __esm(() => {
|
|
|
165814
167844
|
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
165815
167845
|
return ctx.services.timebackAssessments.synchronizeReviewBank(integrationId);
|
|
165816
167846
|
});
|
|
165817
|
-
reorderAssessments = requireDeveloper(async (ctx) => {
|
|
165818
|
-
const { gameId, courseId } = ctx.params;
|
|
165819
|
-
if (!gameId || !courseId) {
|
|
165820
|
-
throw ApiError.badRequest("Missing gameId or courseId parameter");
|
|
165821
|
-
}
|
|
165822
|
-
const body2 = await parseRequestBody(ctx.request, ReorderAssessmentsRequestSchema);
|
|
165823
|
-
const integrationId = await ctx.services.timebackAssessments.resolveIntegrationId(gameId, courseId, ctx.user);
|
|
165824
|
-
await ctx.services.timebackAssessments.reorderAssessments(integrationId, body2.purpose, body2.testIdentifiers);
|
|
165825
|
-
return { success: true };
|
|
165826
|
-
});
|
|
165827
167847
|
reorderQuestions = requireDeveloper(async (ctx) => {
|
|
165828
167848
|
const { gameId, courseId, testIdentifier } = ctx.params;
|
|
165829
167849
|
if (!gameId || !courseId || !testIdentifier) {
|
|
@@ -165931,6 +167951,7 @@ var init_timeback_controller = __esm(() => {
|
|
|
165931
167951
|
heartbeat,
|
|
165932
167952
|
advanceCourse,
|
|
165933
167953
|
unenrollCourse,
|
|
167954
|
+
prepareRuntimeAssessment,
|
|
165934
167955
|
startRuntimeAssessment,
|
|
165935
167956
|
getLatestRuntimeAssessment,
|
|
165936
167957
|
getRuntimeAssessment,
|
|
@@ -165964,7 +167985,6 @@ var init_timeback_controller = __esm(() => {
|
|
|
165964
167985
|
attachExistingAssessments,
|
|
165965
167986
|
updateAssessment,
|
|
165966
167987
|
synchronizeReviewBank,
|
|
165967
|
-
reorderAssessments,
|
|
165968
167988
|
removeAssessment,
|
|
165969
167989
|
reorderQuestions,
|
|
165970
167990
|
listQuestions,
|
|
@@ -166924,6 +168944,7 @@ var init_timeback8 = __esm(async () => {
|
|
|
166924
168944
|
timebackRouter.post("/heartbeat", handle2(timeback2.heartbeat));
|
|
166925
168945
|
timebackRouter.post("/advance-course", handle2(timeback2.advanceCourse));
|
|
166926
168946
|
timebackRouter.post("/unenroll-course", handle2(timeback2.unenrollCourse));
|
|
168947
|
+
timebackRouter.post("/assessments/prepare", handle2(timeback2.prepareRuntimeAssessment));
|
|
166927
168948
|
timebackRouter.post("/assessments/start", handle2(timeback2.startRuntimeAssessment));
|
|
166928
168949
|
timebackRouter.get("/assessments/latest", handle2(timeback2.getLatestRuntimeAssessment));
|
|
166929
168950
|
timebackRouter.get("/assessments/:attemptId", handle2(timeback2.getRuntimeAssessment));
|