@playcademy/sandbox 0.8.1-beta.3 → 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 +2986 -880
- package/dist/server.js +2986 -880
- 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: {
|
|
@@ -11565,7 +11565,7 @@ function playcademyDiagnosticAssessmentItemResultMetadata(value) {
|
|
|
11565
11565
|
const normalized = metadata2.responses === undefined ? { ...metadata2, responses: {} } : metadata2;
|
|
11566
11566
|
return isPlaycademyDiagnosticAssessmentItemResultMetadataV1(normalized) ? normalized : null;
|
|
11567
11567
|
}
|
|
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, 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, SaveAssessmentBodySchema, SubmitAssessmentItemBodySchema, SubmitAssessmentBodySchema, FinalizeAssessmentBodySchema, StartRuntimeAssessmentRequestSchema, SaveRuntimeAssessmentRequestSchema, SubmitAssessmentItemRuntimeRequestSchema, SubmitRuntimeAssessmentRequestSchema, FinalizeRuntimeAssessmentRequestSchema;
|
|
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;
|
|
11569
11569
|
var init_assessment_runtime2 = __esm(() => {
|
|
11570
11570
|
init_timeback3();
|
|
11571
11571
|
init_timeback3();
|
|
@@ -11719,6 +11719,23 @@ var init_assessment_runtime2 = __esm(() => {
|
|
|
11719
11719
|
"qti-sum",
|
|
11720
11720
|
"qti-base-value"
|
|
11721
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`]));
|
|
11722
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;
|
|
11723
11740
|
COMMON_CORE_MATH_FRAMEWORK_ALIASES = new Set([
|
|
11724
11741
|
"ccssm",
|
|
@@ -11895,6 +11912,8 @@ var init_assessment_runtime2 = __esm(() => {
|
|
|
11895
11912
|
standard: AssessmentStandardRefSchema
|
|
11896
11913
|
})
|
|
11897
11914
|
]);
|
|
11915
|
+
AssessmentPreparationReceiptSchema = exports_external.string().max(32000);
|
|
11916
|
+
StartAssessmentRequestBodySchema = exports_external.intersection(StartAssessmentBodySchema, exports_external.object({ preparationReceipt: AssessmentPreparationReceiptSchema.optional() }));
|
|
11898
11917
|
SaveAssessmentBodySchema = exports_external.object({
|
|
11899
11918
|
expectedResponseVersion: exports_external.number().int().nonnegative(),
|
|
11900
11919
|
responses: exports_external.record(ResponseKeySchema, exports_external.record(ResponseKeySchema, AssessmentResponseValueSchema.nullable()))
|
|
@@ -11922,7 +11941,8 @@ var init_assessment_runtime2 = __esm(() => {
|
|
|
11922
11941
|
message: "Provide either masteredUnits or masteredUnitsAbsolute, not both",
|
|
11923
11942
|
path: ["masteredUnits"]
|
|
11924
11943
|
});
|
|
11925
|
-
|
|
11944
|
+
PrepareRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.and(StartAssessmentBodySchema);
|
|
11945
|
+
StartRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.and(StartAssessmentRequestBodySchema);
|
|
11926
11946
|
SaveRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.extend(SaveAssessmentBodySchema.shape);
|
|
11927
11947
|
SubmitAssessmentItemRuntimeRequestSchema = AssessmentRuntimeIdentitySchema.extend(SubmitAssessmentItemBodySchema.shape);
|
|
11928
11948
|
SubmitRuntimeAssessmentRequestSchema = AssessmentRuntimeIdentitySchema.extend(SubmitAssessmentBodySchema.shape).extend({
|
|
@@ -12450,6 +12470,7 @@ var init_schema = __esm(() => {
|
|
|
12450
12470
|
ltiTestMode: exports_external.boolean().default(false),
|
|
12451
12471
|
platformServiceJwt: platformServiceJwtConfigSchema.optional(),
|
|
12452
12472
|
uploadBucket: exports_external.string().optional(),
|
|
12473
|
+
assessmentArtifactBucket: exports_external.string().optional(),
|
|
12453
12474
|
cdnOrigin: exports_external.string().url().startsWith("https://").optional(),
|
|
12454
12475
|
queueIngressSecret: exports_external.string().optional(),
|
|
12455
12476
|
secretsManifestPepper: exports_external.string().optional()
|
|
@@ -38529,6 +38550,319 @@ var init_infra2 = __esm(() => {
|
|
|
38529
38550
|
init_kv_backup_service();
|
|
38530
38551
|
});
|
|
38531
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
|
+
|
|
38532
38866
|
// ../../node_modules/.bun/sharp@0.34.2/node_modules/sharp/lib/is.js
|
|
38533
38867
|
var require_is = __commonJS((exports, module2) => {
|
|
38534
38868
|
var defined = function(val) {
|
|
@@ -95657,6 +95991,365 @@ function supportedQtiQuestionInteractionType(question) {
|
|
|
95657
95991
|
return null;
|
|
95658
95992
|
}
|
|
95659
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
|
+
}
|
|
95660
96353
|
function isRecord22(value) {
|
|
95661
96354
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
95662
96355
|
}
|
|
@@ -95812,7 +96505,7 @@ function qtiStandardsFromMetadata(metadata2) {
|
|
|
95812
96505
|
standards.push(...qtiAlignmentStandardsFromMetadata2(metadata2));
|
|
95813
96506
|
return dedupeStandards2(standards);
|
|
95814
96507
|
}
|
|
95815
|
-
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;
|
|
95816
96509
|
var init_qti = __esm(() => {
|
|
95817
96510
|
init_timeback3();
|
|
95818
96511
|
init_timeback3();
|
|
@@ -96062,6 +96755,30 @@ var init_qti = __esm(() => {
|
|
|
96062
96755
|
"qti-sum",
|
|
96063
96756
|
"qti-base-value"
|
|
96064
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
|
+
};
|
|
96065
96782
|
COMMON_CORE_MATH_FRAMEWORK_ALIASES2 = new Set([
|
|
96066
96783
|
"ccssm",
|
|
96067
96784
|
"ccssmath",
|
|
@@ -96087,11 +96804,11 @@ var init_qti = __esm(() => {
|
|
|
96087
96804
|
});
|
|
96088
96805
|
|
|
96089
96806
|
// ../api-core/src/utils/timeback-grade-level-results.util.ts
|
|
96090
|
-
function
|
|
96807
|
+
function isRecord7(value) {
|
|
96091
96808
|
return Boolean(value) && typeof value === "object" && !Array.isArray(value);
|
|
96092
96809
|
}
|
|
96093
96810
|
function metadataOf(record3) {
|
|
96094
|
-
return
|
|
96811
|
+
return isRecord7(record3) && isRecord7(record3.metadata) ? record3.metadata : {};
|
|
96095
96812
|
}
|
|
96096
96813
|
function stringField3(value) {
|
|
96097
96814
|
return typeof value === "string" ? value : "";
|
|
@@ -96103,7 +96820,7 @@ function numberField(value) {
|
|
|
96103
96820
|
return typeof value === "number" && Number.isFinite(value) ? value : null;
|
|
96104
96821
|
}
|
|
96105
96822
|
function sourceIdFromRef(value) {
|
|
96106
|
-
return
|
|
96823
|
+
return isRecord7(value) ? stringField3(value.sourcedId) : "";
|
|
96107
96824
|
}
|
|
96108
96825
|
function buildGradeLevelTestResultWhere(options) {
|
|
96109
96826
|
return {
|
|
@@ -96179,7 +96896,7 @@ function mapGradeLevelTestSummary(result, options) {
|
|
|
96179
96896
|
};
|
|
96180
96897
|
}
|
|
96181
96898
|
function readLearningObjectiveSets(record3) {
|
|
96182
|
-
if (!
|
|
96899
|
+
if (!isRecord7(record3)) {
|
|
96183
96900
|
return [];
|
|
96184
96901
|
}
|
|
96185
96902
|
const metadata2 = metadataOf(record3);
|
|
@@ -96209,7 +96926,7 @@ function dedupeStandards3(standards) {
|
|
|
96209
96926
|
function standardsFromLineItem(lineItem) {
|
|
96210
96927
|
const standards = [];
|
|
96211
96928
|
for (const set3 of readLearningObjectiveSets(lineItem)) {
|
|
96212
|
-
if (
|
|
96929
|
+
if (isRecord7(set3)) {
|
|
96213
96930
|
const source = stringField3(set3.source) || "OneRoster";
|
|
96214
96931
|
const ids = Array.isArray(set3.learningObjectiveIds) ? set3.learningObjectiveIds : [];
|
|
96215
96932
|
for (const id of ids) {
|
|
@@ -96225,11 +96942,11 @@ function standardsFromLineItem(lineItem) {
|
|
|
96225
96942
|
function standardsFromResult(result) {
|
|
96226
96943
|
const standards = [];
|
|
96227
96944
|
for (const set3 of readLearningObjectiveSets(result)) {
|
|
96228
|
-
if (
|
|
96945
|
+
if (isRecord7(set3)) {
|
|
96229
96946
|
const source = stringField3(set3.source) || "OneRoster";
|
|
96230
96947
|
const values = Array.isArray(set3.learningObjectiveResults) ? set3.learningObjectiveResults : [];
|
|
96231
96948
|
for (const item of values) {
|
|
96232
|
-
if (
|
|
96949
|
+
if (isRecord7(item)) {
|
|
96233
96950
|
const id = stringField3(item.learningObjectiveId).trim();
|
|
96234
96951
|
if (id) {
|
|
96235
96952
|
standards.push({ source, id });
|
|
@@ -96241,7 +96958,7 @@ function standardsFromResult(result) {
|
|
|
96241
96958
|
return dedupeStandards3(standards);
|
|
96242
96959
|
}
|
|
96243
96960
|
function standardsFromQtiQuestion(questionRef) {
|
|
96244
|
-
const question =
|
|
96961
|
+
const question = isRecord7(questionRef) && isRecord7(questionRef.question) ? questionRef.question : null;
|
|
96245
96962
|
return qtiStandardsFromMetadata(metadataOf(question));
|
|
96246
96963
|
}
|
|
96247
96964
|
function enrichStandardsFromQti(standards, qtiStandards) {
|
|
@@ -96378,8 +97095,8 @@ function mapGradeLevelReviewQuestions(options) {
|
|
|
96378
97095
|
return options.questionLineItems.toSorted((a, b) => naturalTitleSort.compare(a.title, b.title)).map((lineItem) => {
|
|
96379
97096
|
const result = options.latestResultsByLineItem.get(lineItem.sourcedId ?? "");
|
|
96380
97097
|
const resultMetadata = metadataOf(result);
|
|
96381
|
-
const attemptedQuestionData =
|
|
96382
|
-
const attemptedResult =
|
|
97098
|
+
const attemptedQuestionData = isRecord7(resultMetadata.attemptedQuestionData) ? resultMetadata.attemptedQuestionData : null;
|
|
97099
|
+
const attemptedResult = isRecord7(attemptedQuestionData?.result) ? attemptedQuestionData.result : null;
|
|
96383
97100
|
const index2 = parseQuestionIndex(lineItem.title);
|
|
96384
97101
|
const qtiQuestion = index2 ? qtiQuestionByIndex.get(index2) ?? options.qtiQuestions[index2 - 1] : undefined;
|
|
96385
97102
|
const qtiStandards = standardsFromQtiQuestion(qtiQuestion);
|
|
@@ -98422,6 +99139,452 @@ var init_timeback_admin_service = __esm(async () => {
|
|
|
98422
99139
|
]);
|
|
98423
99140
|
});
|
|
98424
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
|
+
|
|
98425
99588
|
// ../data/src/locks.ts
|
|
98426
99589
|
function createInMemoryDatabaseLock() {
|
|
98427
99590
|
const tails = new Map;
|
|
@@ -98542,6 +99705,810 @@ var init_assessment_runtime_lock_util = __esm(() => {
|
|
|
98542
99705
|
};
|
|
98543
99706
|
});
|
|
98544
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
|
+
|
|
98545
100512
|
// ../api-core/src/utils/timeback-qti-authoring.util.ts
|
|
98546
100513
|
function recordValue(value) {
|
|
98547
100514
|
return value && typeof value === "object" && !Array.isArray(value) ? value : undefined;
|
|
@@ -99483,810 +101450,6 @@ var init_timeback_qti_authoring_util = __esm(() => {
|
|
|
99483
101450
|
];
|
|
99484
101451
|
});
|
|
99485
101452
|
|
|
99486
|
-
// ../api-core/src/utils/timeback-assessment-runtime.util.ts
|
|
99487
|
-
function assessmentPreparationContentionError(expectedResponseVersion, responseVersion, kind) {
|
|
99488
|
-
if (expectedResponseVersion === responseVersion) {
|
|
99489
|
-
return new ServiceUnavailableError(`The ${kind} response state changed repeatedly while this item was being prepared. Try again shortly.`, {
|
|
99490
|
-
retryable: true,
|
|
99491
|
-
reason: "PREPARATION_CONTENTION",
|
|
99492
|
-
expectedResponseVersion,
|
|
99493
|
-
responseVersion
|
|
99494
|
-
});
|
|
99495
|
-
}
|
|
99496
|
-
return AssessmentRuntimeError.from(assessmentResponseVersionConflict(expectedResponseVersion, responseVersion));
|
|
99497
|
-
}
|
|
99498
|
-
function assessmentTrackableCourse(integration) {
|
|
99499
|
-
if (!isTimebackGrade2(integration.grade) || !isTimebackSubject(integration.subject)) {
|
|
99500
|
-
return null;
|
|
99501
|
-
}
|
|
99502
|
-
return { grade: integration.grade, subject: integration.subject };
|
|
99503
|
-
}
|
|
99504
|
-
function buildAssessmentActivityData(input) {
|
|
99505
|
-
if (!input.course) {
|
|
99506
|
-
return;
|
|
99507
|
-
}
|
|
99508
|
-
return {
|
|
99509
|
-
activityId: input.activityId,
|
|
99510
|
-
activityName: input.activityName,
|
|
99511
|
-
...input.course,
|
|
99512
|
-
courseId: input.courseId
|
|
99513
|
-
};
|
|
99514
|
-
}
|
|
99515
|
-
function buildDiagnosticAssessmentSnapshot(input) {
|
|
99516
|
-
return {
|
|
99517
|
-
attemptId: input.attemptId,
|
|
99518
|
-
responseVersion: input.metadata.responseVersion,
|
|
99519
|
-
status: "in_progress",
|
|
99520
|
-
flow: "platform-routed-item-submit",
|
|
99521
|
-
...input.activityData ? { activityData: input.activityData } : {},
|
|
99522
|
-
assessment: assessmentPresentationForAttempt(input.assessment, input.attemptId),
|
|
99523
|
-
selection: {
|
|
99524
|
-
kind: "platform-routed-diagnostic",
|
|
99525
|
-
purpose: "diagnostic",
|
|
99526
|
-
definitionId: input.metadata.diagnostic.definitionId,
|
|
99527
|
-
diagnosticKey: input.metadata.diagnostic.diagnosticKey,
|
|
99528
|
-
routingRevision: input.metadata.diagnostic.routingRevision
|
|
99529
|
-
},
|
|
99530
|
-
routing: projectDiagnosticRoutingSnapshot(input.metadata.diagnostic.routingRevision, input.state),
|
|
99531
|
-
completion: null
|
|
99532
|
-
};
|
|
99533
|
-
}
|
|
99534
|
-
function buildConventionalAssessmentSnapshot(input) {
|
|
99535
|
-
const metadata2 = input.metadata;
|
|
99536
|
-
let selection;
|
|
99537
|
-
if (metadata2.purpose === "review") {
|
|
99538
|
-
selection = {
|
|
99539
|
-
kind: "standards-review",
|
|
99540
|
-
purpose: metadata2.purpose,
|
|
99541
|
-
standards: [...metadata2.review.standards],
|
|
99542
|
-
candidateItemsPerStandard: metadata2.review.candidateItemsPerStandard,
|
|
99543
|
-
selections: [...metadata2.review.selections],
|
|
99544
|
-
fulfillment: metadata2.review.fulfillment
|
|
99545
|
-
};
|
|
99546
|
-
} else if (metadata2.purpose === "mastery") {
|
|
99547
|
-
selection = {
|
|
99548
|
-
kind: "standard-quiz",
|
|
99549
|
-
purpose: metadata2.purpose,
|
|
99550
|
-
standard: metadata2.mastery.standard
|
|
99551
|
-
};
|
|
99552
|
-
} else {
|
|
99553
|
-
selection = { kind: "fixed-test", purpose: metadata2.purpose };
|
|
99554
|
-
}
|
|
99555
|
-
return {
|
|
99556
|
-
attemptId: input.attemptId,
|
|
99557
|
-
responseVersion: metadata2.responseVersion,
|
|
99558
|
-
status: "in_progress",
|
|
99559
|
-
flow: assessmentFlowForPurpose(metadata2.purpose),
|
|
99560
|
-
...input.activityData ? { activityData: input.activityData } : {},
|
|
99561
|
-
assessment: assessmentPresentationForAttempt(input.assessment, input.attemptId),
|
|
99562
|
-
responses: metadata2.responses,
|
|
99563
|
-
itemSubmissions: metadata2.itemSubmissions,
|
|
99564
|
-
score: null,
|
|
99565
|
-
selection
|
|
99566
|
-
};
|
|
99567
|
-
}
|
|
99568
|
-
function buildAssessmentAwardRecord(input, mastery) {
|
|
99569
|
-
return {
|
|
99570
|
-
xpAwarded: input.xpAwarded,
|
|
99571
|
-
...input.masteredUnits !== undefined ? { masteredUnits: input.masteredUnits } : {},
|
|
99572
|
-
...input.masteredUnitsAbsolute !== undefined ? { masteredUnitsAbsolute: input.masteredUnitsAbsolute } : {},
|
|
99573
|
-
masteredUnitsApplied: mastery.masteredUnitsApplied,
|
|
99574
|
-
...mastery.pctCompleteApp !== undefined ? { pctCompleteApp: mastery.pctCompleteApp } : {},
|
|
99575
|
-
...mastery.warnings ? { warnings: mastery.warnings } : {}
|
|
99576
|
-
};
|
|
99577
|
-
}
|
|
99578
|
-
function stageAssessmentAttemptSupersession(attempt) {
|
|
99579
|
-
Object.assign(attempt.result, ASSESSMENT_ATTEMPT_SUPERSEDED);
|
|
99580
|
-
Object.assign(attempt.selection, ASSESSMENT_ATTEMPT_SUPERSEDED);
|
|
99581
|
-
}
|
|
99582
|
-
function shouldConfirmAssessmentVersionUnavailable(details) {
|
|
99583
|
-
return typeof details === "object" && details !== null && "expectedRevision" in details && typeof details.expectedRevision === "string" && !("currentRevision" in details);
|
|
99584
|
-
}
|
|
99585
|
-
function buildAssessmentAttemptSupersessionUpdate(result) {
|
|
99586
|
-
if (result.status !== "active" || !isAssessmentAttemptOpen({
|
|
99587
|
-
inProgress: result.inProgress ?? "",
|
|
99588
|
-
scoreStatus: result.scoreStatus
|
|
99589
|
-
})) {
|
|
99590
|
-
return null;
|
|
99591
|
-
}
|
|
99592
|
-
return {
|
|
99593
|
-
status: "active",
|
|
99594
|
-
assessmentLineItem: result.assessmentLineItem,
|
|
99595
|
-
student: result.student,
|
|
99596
|
-
score: result.score ?? 0,
|
|
99597
|
-
scoreDate: result.scoreDate,
|
|
99598
|
-
...ASSESSMENT_ATTEMPT_SUPERSEDED,
|
|
99599
|
-
metadata: result.metadata ?? {}
|
|
99600
|
-
};
|
|
99601
|
-
}
|
|
99602
|
-
async function resolveAvailableAssessmentAttempt(initialDecision, select3, resume) {
|
|
99603
|
-
let decision = initialDecision;
|
|
99604
|
-
while (decision?.kind === "resume") {
|
|
99605
|
-
const snapshot = await resume(decision);
|
|
99606
|
-
if (snapshot !== null) {
|
|
99607
|
-
return { kind: "resumed", snapshot };
|
|
99608
|
-
}
|
|
99609
|
-
decision = select3();
|
|
99610
|
-
}
|
|
99611
|
-
return { kind: "continue", decision };
|
|
99612
|
-
}
|
|
99613
|
-
function buildReviewItemResultMetadata(input) {
|
|
99614
|
-
const administration = itemAdministration(input.metadata.itemSubmissions, input.selection.itemIdentifier);
|
|
99615
|
-
if (!administration) {
|
|
99616
|
-
throw new Error(`Cannot create a review result for unadministered candidate ${input.selection.itemIdentifier}`);
|
|
99617
|
-
}
|
|
99618
|
-
return {
|
|
99619
|
-
version: PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_VERSION,
|
|
99620
|
-
parentAttemptId: input.parentAttemptId,
|
|
99621
|
-
activityId: input.metadata.activityId,
|
|
99622
|
-
integrationId: input.metadata.integrationId,
|
|
99623
|
-
enrollmentId: input.metadata.enrollmentId,
|
|
99624
|
-
bankRevision: input.metadata.review.bankRevision,
|
|
99625
|
-
itemIdentifier: input.selection.itemIdentifier,
|
|
99626
|
-
standard: input.selection.standard,
|
|
99627
|
-
deliveredAt: administration.submittedAt,
|
|
99628
|
-
responseVersion: input.responseVersion ?? input.metadata.responseVersion,
|
|
99629
|
-
responses: input.metadata.responses[input.selection.itemIdentifier] ?? {},
|
|
99630
|
-
...input.submissionId ? { submissionId: input.submissionId } : {},
|
|
99631
|
-
...input.isCorrect === undefined ? {} : { isCorrect: input.isCorrect }
|
|
99632
|
-
};
|
|
99633
|
-
}
|
|
99634
|
-
function buildOpenReviewChildResult(input) {
|
|
99635
|
-
return {
|
|
99636
|
-
status: "active",
|
|
99637
|
-
assessmentLineItem: { sourcedId: input.childLineItemId },
|
|
99638
|
-
student: input.student,
|
|
99639
|
-
score: 0,
|
|
99640
|
-
scoreDate: input.metadata.deliveredAt,
|
|
99641
|
-
...ASSESSMENT_ATTEMPT_OPEN,
|
|
99642
|
-
metadata: {
|
|
99643
|
-
[PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY]: input.metadata
|
|
99644
|
-
}
|
|
99645
|
-
};
|
|
99646
|
-
}
|
|
99647
|
-
function diagnosticQuestionLineItemId(parentLineItemId, itemIdentifier) {
|
|
99648
|
-
return deterministicUUID(["playcademy:diagnostic-question-line-item:v1", parentLineItemId, itemIdentifier].join("\x00"));
|
|
99649
|
-
}
|
|
99650
|
-
function diagnosticQuestionResultId(parentAttemptId, childLineItemId) {
|
|
99651
|
-
return deterministicUUID(["playcademy:diagnostic-question-result:v1", parentAttemptId, childLineItemId].join("\x00"));
|
|
99652
|
-
}
|
|
99653
|
-
function diagnosticAdministration(input) {
|
|
99654
|
-
const ledgerIndex = input.metadata.diagnostic.ledger.findIndex((entry) => entry.routingNodeKey === input.ledgerEntry.routingNodeKey);
|
|
99655
|
-
const persistedEntry = input.metadata.diagnostic.ledger[ledgerIndex];
|
|
99656
|
-
const administration = input.metadata.itemSubmissions[ledgerIndex];
|
|
99657
|
-
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) {
|
|
99658
|
-
throw new Error(`Cannot create a diagnostic result for uncommitted routing node ${input.ledgerEntry.routingNodeKey}`);
|
|
99659
|
-
}
|
|
99660
|
-
return administration;
|
|
99661
|
-
}
|
|
99662
|
-
function buildDiagnosticItemResultMetadata(input) {
|
|
99663
|
-
const administration = diagnosticAdministration(input);
|
|
99664
|
-
return {
|
|
99665
|
-
version: PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_VERSION,
|
|
99666
|
-
parentAttemptId: input.parentAttemptId,
|
|
99667
|
-
activityId: input.metadata.activityId,
|
|
99668
|
-
integrationId: input.metadata.integrationId,
|
|
99669
|
-
enrollmentId: input.metadata.enrollmentId,
|
|
99670
|
-
definitionId: input.metadata.diagnostic.definitionId,
|
|
99671
|
-
diagnosticKey: input.metadata.diagnostic.diagnosticKey,
|
|
99672
|
-
routingRevision: input.metadata.diagnostic.routingRevision,
|
|
99673
|
-
contentRevision: input.metadata.selectedTest.contentRevision,
|
|
99674
|
-
stageKey: input.ledgerEntry.stageKey,
|
|
99675
|
-
trackKey: input.ledgerEntry.trackKey,
|
|
99676
|
-
routingNodeKey: input.ledgerEntry.routingNodeKey,
|
|
99677
|
-
itemIdentifier: input.ledgerEntry.itemIdentifier,
|
|
99678
|
-
deliveredAt: administration.submittedAt,
|
|
99679
|
-
responseVersion: administration.responseVersion,
|
|
99680
|
-
responses: input.metadata.responses[input.ledgerEntry.itemIdentifier] ?? {},
|
|
99681
|
-
submissionId: administration.submissionId,
|
|
99682
|
-
isCorrect: input.ledgerEntry.isCorrect
|
|
99683
|
-
};
|
|
99684
|
-
}
|
|
99685
|
-
function resolveDiagnosticChildResult(input) {
|
|
99686
|
-
if (!input.childResult) {
|
|
99687
|
-
return { action: "create" };
|
|
99688
|
-
}
|
|
99689
|
-
const childMetadata = playcademyDiagnosticAssessmentItemResultMetadata(input.childResult.metadata);
|
|
99690
|
-
const expectedMetadata = buildDiagnosticItemResultMetadata({
|
|
99691
|
-
metadata: input.parentMetadata,
|
|
99692
|
-
ledgerEntry: input.ledgerEntry,
|
|
99693
|
-
parentAttemptId: input.parentResult.sourcedId
|
|
99694
|
-
});
|
|
99695
|
-
if (input.childResult.assessmentLineItem.sourcedId !== input.childLineItemId || input.childResult.student.sourcedId !== input.parentResult.student.sourcedId || !childMetadata || canonicalJson2(childMetadata) !== canonicalJson2(expectedMetadata)) {
|
|
99696
|
-
return { action: "restore", reason: "incompatible" };
|
|
99697
|
-
}
|
|
99698
|
-
return input.childResult.status === "active" ? { action: "reuse", metadata: expectedMetadata } : { action: "restore", reason: "inactive" };
|
|
99699
|
-
}
|
|
99700
|
-
function buildFinalizedDiagnosticChildResult(input) {
|
|
99701
|
-
const administration = diagnosticAdministration(input);
|
|
99702
|
-
const metadata2 = buildDiagnosticItemResultMetadata(input);
|
|
99703
|
-
return {
|
|
99704
|
-
resultUpdate: {
|
|
99705
|
-
status: "active",
|
|
99706
|
-
assessmentLineItem: { sourcedId: input.childLineItemId },
|
|
99707
|
-
student: input.student,
|
|
99708
|
-
score: administration.score.earned,
|
|
99709
|
-
scoreDate: administration.submittedAt,
|
|
99710
|
-
...ASSESSMENT_ATTEMPT_COMPLETED,
|
|
99711
|
-
metadata: {
|
|
99712
|
-
[PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY]: metadata2
|
|
99713
|
-
}
|
|
99714
|
-
}
|
|
99715
|
-
};
|
|
99716
|
-
}
|
|
99717
|
-
function buildFinalizedReviewChildResult(input) {
|
|
99718
|
-
const metadata2 = buildReviewItemResultMetadata({
|
|
99719
|
-
metadata: input.metadata,
|
|
99720
|
-
selection: input.selection,
|
|
99721
|
-
parentAttemptId: input.parentAttemptId,
|
|
99722
|
-
submissionId: input.submissionId,
|
|
99723
|
-
isCorrect: input.itemResult.isCorrect
|
|
99724
|
-
});
|
|
99725
|
-
return {
|
|
99726
|
-
resultUpdate: {
|
|
99727
|
-
status: "active",
|
|
99728
|
-
assessmentLineItem: { sourcedId: input.childLineItemId },
|
|
99729
|
-
student: input.student,
|
|
99730
|
-
score: input.itemResult.score.earned,
|
|
99731
|
-
scoreDate: input.timestamp,
|
|
99732
|
-
...ASSESSMENT_ATTEMPT_COMPLETED,
|
|
99733
|
-
metadata: {
|
|
99734
|
-
[PLAYCADEMY_ASSESSMENT_ITEM_RESULT_METADATA_KEY]: metadata2
|
|
99735
|
-
}
|
|
99736
|
-
},
|
|
99737
|
-
outcome: {
|
|
99738
|
-
itemIdentifier: input.selection.itemIdentifier,
|
|
99739
|
-
standard: input.selection.standard,
|
|
99740
|
-
answered: Object.keys(input.metadata.responses[input.selection.itemIdentifier] ?? {}).length > 0,
|
|
99741
|
-
score: input.itemResult.score,
|
|
99742
|
-
isCorrect: input.itemResult.isCorrect
|
|
99743
|
-
}
|
|
99744
|
-
};
|
|
99745
|
-
}
|
|
99746
|
-
function resolveReviewChildResult(input) {
|
|
99747
|
-
if (!input.childResult) {
|
|
99748
|
-
return { action: "create" };
|
|
99749
|
-
}
|
|
99750
|
-
const childMetadata = playcademyAssessmentItemResultMetadata(input.childResult.metadata);
|
|
99751
|
-
const expectedResponses = input.parentMetadata.responses[input.selection.itemIdentifier] ?? {};
|
|
99752
|
-
const administration = itemAdministration(input.parentMetadata.itemSubmissions, input.selection.itemIdentifier);
|
|
99753
|
-
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)) {
|
|
99754
|
-
return { action: "restore", reason: "incompatible" };
|
|
99755
|
-
}
|
|
99756
|
-
return input.childResult.status === "active" ? { action: "reuse", metadata: childMetadata } : { action: "restore", reason: "inactive" };
|
|
99757
|
-
}
|
|
99758
|
-
function buildAssessmentSubmitResult(input) {
|
|
99759
|
-
if (!input.metadata.score || !input.metadata.submissionId) {
|
|
99760
|
-
return null;
|
|
99761
|
-
}
|
|
99762
|
-
if (input.metadata.purpose === "diagnostic" && input.metadata.diagnostic) {
|
|
99763
|
-
const tracks = input.metadata.completion?.diagnosticTracks;
|
|
99764
|
-
return tracks ? {
|
|
99765
|
-
purpose: input.metadata.purpose,
|
|
99766
|
-
attemptId: input.attemptId,
|
|
99767
|
-
submissionId: input.metadata.submissionId,
|
|
99768
|
-
diagnosticKey: input.metadata.diagnostic.diagnosticKey,
|
|
99769
|
-
responseVersion: input.metadata.responseVersion,
|
|
99770
|
-
status: "awaiting_award",
|
|
99771
|
-
tracks
|
|
99772
|
-
} : null;
|
|
99773
|
-
}
|
|
99774
|
-
const base = {
|
|
99775
|
-
attemptId: input.attemptId,
|
|
99776
|
-
submissionId: input.metadata.submissionId,
|
|
99777
|
-
responseVersion: input.metadata.responseVersion,
|
|
99778
|
-
status: "awaiting_award",
|
|
99779
|
-
responses: input.metadata.responses,
|
|
99780
|
-
score: input.metadata.score
|
|
99781
|
-
};
|
|
99782
|
-
if (input.metadata.purpose === "review") {
|
|
99783
|
-
const itemOutcomes = input.metadata.completion?.itemOutcomes;
|
|
99784
|
-
return itemOutcomes ? { ...base, purpose: input.metadata.purpose, itemOutcomes } : null;
|
|
99785
|
-
}
|
|
99786
|
-
return { ...base, purpose: input.metadata.purpose };
|
|
99787
|
-
}
|
|
99788
|
-
function assessmentAwardFromResult(result, metadata2) {
|
|
99789
|
-
if (metadata2.award) {
|
|
99790
|
-
return metadata2.award;
|
|
99791
|
-
}
|
|
99792
|
-
const xp = result.metadata?.xp;
|
|
99793
|
-
return typeof xp === "number" && Number.isFinite(xp) && xp >= 0 ? { xpAwarded: xp, masteredUnitsApplied: 0 } : null;
|
|
99794
|
-
}
|
|
99795
|
-
function buildCompletedAssessmentSubmitResult(input) {
|
|
99796
|
-
const attemptId = input.result.sourcedId;
|
|
99797
|
-
const award = assessmentAwardFromResult(input.result, input.metadata);
|
|
99798
|
-
if (award === null) {
|
|
99799
|
-
throw new GoneError("This completed assessment is missing its recorded XP.", {
|
|
99800
|
-
attemptId
|
|
99801
|
-
});
|
|
99802
|
-
}
|
|
99803
|
-
const submitted = buildAssessmentSubmitResult({ attemptId, metadata: input.metadata });
|
|
99804
|
-
if (!submitted) {
|
|
99805
|
-
throw new GoneError("This completed assessment has incomplete persisted results.", {
|
|
99806
|
-
attemptId
|
|
99807
|
-
});
|
|
99808
|
-
}
|
|
99809
|
-
return {
|
|
99810
|
-
...submitted,
|
|
99811
|
-
status: "completed",
|
|
99812
|
-
xpAwarded: award.xpAwarded,
|
|
99813
|
-
masteredUnitsApplied: award.masteredUnitsApplied,
|
|
99814
|
-
...award.pctCompleteApp !== undefined ? { pctCompleteApp: award.pctCompleteApp } : {},
|
|
99815
|
-
...award.warnings ? { warnings: award.warnings } : {}
|
|
99816
|
-
};
|
|
99817
|
-
}
|
|
99818
|
-
function assessmentAttemptId(input) {
|
|
99819
|
-
return deterministicUUID([
|
|
99820
|
-
"playcademy:assessment-attempt",
|
|
99821
|
-
input.gameId,
|
|
99822
|
-
input.studentId,
|
|
99823
|
-
input.courseId,
|
|
99824
|
-
input.activityId,
|
|
99825
|
-
input.purpose,
|
|
99826
|
-
String(input.attemptNumber)
|
|
99827
|
-
].join("\x00"));
|
|
99828
|
-
}
|
|
99829
|
-
async function findLatestAssessmentResult(results, match) {
|
|
99830
|
-
let latest = null;
|
|
99831
|
-
let latestTimestamp = 0;
|
|
99832
|
-
for await (const result of results) {
|
|
99833
|
-
const resultTimestamp = assessmentAttemptTimestamp(result.scoreDate);
|
|
99834
|
-
if (latest && resultTimestamp < latestTimestamp) {
|
|
99835
|
-
break;
|
|
99836
|
-
}
|
|
99837
|
-
const matched = match(result);
|
|
99838
|
-
if (matched !== null && (!latest || compareNewestCompletedAssessmentAttempt({ attemptId: result.sourcedId, scoreDate: result.scoreDate }, {
|
|
99839
|
-
attemptId: latest.result.sourcedId,
|
|
99840
|
-
scoreDate: latest.result.scoreDate
|
|
99841
|
-
}) < 0)) {
|
|
99842
|
-
latest = { result, match: matched };
|
|
99843
|
-
latestTimestamp = resultTimestamp;
|
|
99844
|
-
}
|
|
99845
|
-
}
|
|
99846
|
-
return latest;
|
|
99847
|
-
}
|
|
99848
|
-
function playableChoice(choice) {
|
|
99849
|
-
return {
|
|
99850
|
-
identifier: choice.identifier,
|
|
99851
|
-
content: choice.content,
|
|
99852
|
-
...choice.contentNodes ? { contentNodes: choice.contentNodes } : {}
|
|
99853
|
-
};
|
|
99854
|
-
}
|
|
99855
|
-
function playableAssociableChoice(choice) {
|
|
99856
|
-
const matchMax = qtiAttributeCapacity(choice.attributes, "match-max", 0);
|
|
99857
|
-
if (matchMax === null) {
|
|
99858
|
-
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", "The selected assessment contains an invalid Match capacity.");
|
|
99859
|
-
}
|
|
99860
|
-
return {
|
|
99861
|
-
...playableChoice(choice),
|
|
99862
|
-
matchMax
|
|
99863
|
-
};
|
|
99864
|
-
}
|
|
99865
|
-
function requireChoices2(choices) {
|
|
99866
|
-
if (choices.length === 0 || choices.some((candidate) => !candidate.identifier)) {
|
|
99867
|
-
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", "The selected assessment contains an interaction Playcademy cannot render.");
|
|
99868
|
-
}
|
|
99869
|
-
return choices;
|
|
99870
|
-
}
|
|
99871
|
-
function isPlayableAssessmentShape(value) {
|
|
99872
|
-
return PLAYABLE_SHAPE_SET.has(value);
|
|
99873
|
-
}
|
|
99874
|
-
function playableGraphic(graphic) {
|
|
99875
|
-
return {
|
|
99876
|
-
src: graphic.src,
|
|
99877
|
-
...graphic.width === undefined ? {} : { width: graphic.width },
|
|
99878
|
-
...graphic.height === undefined ? {} : { height: graphic.height },
|
|
99879
|
-
...graphic.description ? { description: graphic.description } : {}
|
|
99880
|
-
};
|
|
99881
|
-
}
|
|
99882
|
-
function requireGraphic(questionIdentifier, graphic) {
|
|
99883
|
-
if (!graphic) {
|
|
99884
|
-
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${questionIdentifier} is missing a required graphic.`);
|
|
99885
|
-
}
|
|
99886
|
-
return playableGraphic(graphic);
|
|
99887
|
-
}
|
|
99888
|
-
function requireHotspots(questionIdentifier, hotspots) {
|
|
99889
|
-
if (hotspots.length === 0) {
|
|
99890
|
-
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${questionIdentifier} declares no graphic regions.`);
|
|
99891
|
-
}
|
|
99892
|
-
return hotspots.map((hotspot) => ({
|
|
99893
|
-
identifier: hotspot.identifier,
|
|
99894
|
-
shape: isPlayableAssessmentShape(hotspot.shape) ? hotspot.shape : "default",
|
|
99895
|
-
coords: [...hotspot.coords],
|
|
99896
|
-
...hotspot.matchMax === undefined ? {} : { matchMax: hotspot.matchMax },
|
|
99897
|
-
...hotspot.description ? { description: hotspot.description } : {}
|
|
99898
|
-
}));
|
|
99899
|
-
}
|
|
99900
|
-
function parseInteraction2(questionIdentifier, interaction) {
|
|
99901
|
-
const { type } = interaction;
|
|
99902
|
-
const responseIdentifier = interaction.attributes["response-identifier"];
|
|
99903
|
-
if (!responseIdentifier) {
|
|
99904
|
-
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${questionIdentifier} has no response identifier.`);
|
|
99905
|
-
}
|
|
99906
|
-
const prompt = interaction.embeddedPrompt ? { prompt: interaction.embeddedPrompt } : {};
|
|
99907
|
-
if (type === "choice") {
|
|
99908
|
-
const choices = requireChoices2(interaction.choices.map(playableChoice));
|
|
99909
|
-
return {
|
|
99910
|
-
type,
|
|
99911
|
-
responseIdentifier,
|
|
99912
|
-
...prompt,
|
|
99913
|
-
choices,
|
|
99914
|
-
minChoices: qtiAttributeNumber(interaction.attributes, "min-choices", 1),
|
|
99915
|
-
maxChoices: qtiAttributeMaximum(interaction.attributes, "max-choices", 1, choices.length)
|
|
99916
|
-
};
|
|
99917
|
-
}
|
|
99918
|
-
if (type === "inline-choice") {
|
|
99919
|
-
return {
|
|
99920
|
-
type,
|
|
99921
|
-
responseIdentifier,
|
|
99922
|
-
...prompt,
|
|
99923
|
-
choices: requireChoices2(interaction.choices.map(playableChoice))
|
|
99924
|
-
};
|
|
99925
|
-
}
|
|
99926
|
-
if (type === "text-entry") {
|
|
99927
|
-
const expectedLength = qtiAttributeNumber(interaction.attributes, "expected-length", 0);
|
|
99928
|
-
return {
|
|
99929
|
-
type,
|
|
99930
|
-
responseIdentifier,
|
|
99931
|
-
...prompt,
|
|
99932
|
-
...Number.isInteger(expectedLength) && expectedLength > 0 ? { expectedLength } : {},
|
|
99933
|
-
...interaction.attributes.placeholder ? { placeholder: interaction.attributes.placeholder } : {}
|
|
99934
|
-
};
|
|
99935
|
-
}
|
|
99936
|
-
if (type === "order") {
|
|
99937
|
-
return {
|
|
99938
|
-
type,
|
|
99939
|
-
responseIdentifier,
|
|
99940
|
-
...prompt,
|
|
99941
|
-
choices: requireChoices2(interaction.choices.map(playableChoice)),
|
|
99942
|
-
shuffle: qtiAttributeBoolean(interaction.attributes, "shuffle")
|
|
99943
|
-
};
|
|
99944
|
-
}
|
|
99945
|
-
if (type === "match") {
|
|
99946
|
-
const sets = interaction.choiceSets;
|
|
99947
|
-
if (sets.length !== 2) {
|
|
99948
|
-
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", "Match interactions require exactly two choice sets.");
|
|
99949
|
-
}
|
|
99950
|
-
const sourceChoices = requireChoices2(sets[0].map(playableAssociableChoice));
|
|
99951
|
-
const targetChoices = requireChoices2(sets[1].map(playableAssociableChoice));
|
|
99952
|
-
return {
|
|
99953
|
-
type,
|
|
99954
|
-
responseIdentifier,
|
|
99955
|
-
...prompt,
|
|
99956
|
-
sourceChoices,
|
|
99957
|
-
targetChoices,
|
|
99958
|
-
maxAssociations: qtiAttributeMaximum(interaction.attributes, "max-associations", 1, sourceChoices.length * targetChoices.length),
|
|
99959
|
-
shuffle: qtiAttributeBoolean(interaction.attributes, "shuffle")
|
|
99960
|
-
};
|
|
99961
|
-
}
|
|
99962
|
-
if (type === "hotspot") {
|
|
99963
|
-
const image = requireGraphic(questionIdentifier, interaction.graphic);
|
|
99964
|
-
const hotspots = requireHotspots(questionIdentifier, interaction.hotspots);
|
|
99965
|
-
return {
|
|
99966
|
-
type,
|
|
99967
|
-
responseIdentifier,
|
|
99968
|
-
...prompt,
|
|
99969
|
-
image,
|
|
99970
|
-
hotspots,
|
|
99971
|
-
minChoices: qtiAttributeNumber(interaction.attributes, "min-choices", 1),
|
|
99972
|
-
maxChoices: qtiAttributeMaximum(interaction.attributes, "max-choices", 1, hotspots.length)
|
|
99973
|
-
};
|
|
99974
|
-
}
|
|
99975
|
-
if (type === "gap-match") {
|
|
99976
|
-
if (interaction.gapTexts.length === 0 || interaction.gaps.length === 0 || interaction.textWithGaps === undefined || !interaction.gapContent || !qtiGapContentMatchesGaps(interaction.gapContent, interaction.gaps)) {
|
|
99977
|
-
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${questionIdentifier} has an incomplete gap-match interaction.`);
|
|
99978
|
-
}
|
|
99979
|
-
return {
|
|
99980
|
-
type,
|
|
99981
|
-
responseIdentifier,
|
|
99982
|
-
...prompt,
|
|
99983
|
-
content: interaction.textWithGaps,
|
|
99984
|
-
contentNodes: interaction.gapContent,
|
|
99985
|
-
gapTexts: interaction.gapTexts.map((gapText) => ({
|
|
99986
|
-
identifier: gapText.identifier,
|
|
99987
|
-
content: gapText.content,
|
|
99988
|
-
matchMax: gapText.matchMax
|
|
99989
|
-
})),
|
|
99990
|
-
gaps: [...interaction.gaps],
|
|
99991
|
-
maxAssociations: qtiAttributeNumber(interaction.attributes, "max-associations", 0)
|
|
99992
|
-
};
|
|
99993
|
-
}
|
|
99994
|
-
if (type === "graphic-gap-match") {
|
|
99995
|
-
const image = requireGraphic(questionIdentifier, interaction.graphic);
|
|
99996
|
-
const hotspots = requireHotspots(questionIdentifier, interaction.hotspots);
|
|
99997
|
-
if (interaction.gapImages.length === 0) {
|
|
99998
|
-
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${questionIdentifier} has no draggable gap images.`);
|
|
99999
|
-
}
|
|
100000
|
-
return {
|
|
100001
|
-
type,
|
|
100002
|
-
responseIdentifier,
|
|
100003
|
-
...prompt,
|
|
100004
|
-
image,
|
|
100005
|
-
gapImages: interaction.gapImages.map((gapImage) => ({
|
|
100006
|
-
identifier: gapImage.identifier,
|
|
100007
|
-
...gapImage.graphic ? { image: playableGraphic(gapImage.graphic) } : {},
|
|
100008
|
-
matchMax: gapImage.matchMax,
|
|
100009
|
-
...gapImage.description ? { description: gapImage.description } : {}
|
|
100010
|
-
})),
|
|
100011
|
-
hotspots,
|
|
100012
|
-
maxAssociations: qtiAttributeNumber(interaction.attributes, "max-associations", 0)
|
|
100013
|
-
};
|
|
100014
|
-
}
|
|
100015
|
-
if (type === "select-point") {
|
|
100016
|
-
return {
|
|
100017
|
-
type,
|
|
100018
|
-
responseIdentifier,
|
|
100019
|
-
...prompt,
|
|
100020
|
-
image: requireGraphic(questionIdentifier, interaction.graphic),
|
|
100021
|
-
minChoices: qtiAttributeNumber(interaction.attributes, "min-choices", 0),
|
|
100022
|
-
maxChoices: qtiAttributeNumber(interaction.attributes, "max-choices", 0)
|
|
100023
|
-
};
|
|
100024
|
-
}
|
|
100025
|
-
if (type === "graphic-associate") {
|
|
100026
|
-
return {
|
|
100027
|
-
type,
|
|
100028
|
-
responseIdentifier,
|
|
100029
|
-
...prompt,
|
|
100030
|
-
image: requireGraphic(questionIdentifier, interaction.graphic),
|
|
100031
|
-
hotspots: requireHotspots(questionIdentifier, interaction.hotspots),
|
|
100032
|
-
maxAssociations: qtiAttributeNumber(interaction.attributes, "max-associations", 1)
|
|
100033
|
-
};
|
|
100034
|
-
}
|
|
100035
|
-
if (type === "position-object") {
|
|
100036
|
-
return {
|
|
100037
|
-
type,
|
|
100038
|
-
responseIdentifier,
|
|
100039
|
-
...prompt,
|
|
100040
|
-
stage: requireGraphic(questionIdentifier, interaction.graphic),
|
|
100041
|
-
object: requireGraphic(questionIdentifier, interaction.objectGraphic),
|
|
100042
|
-
maxPlacements: qtiAttributeNumber(interaction.attributes, "max-choices", 1)
|
|
100043
|
-
};
|
|
100044
|
-
}
|
|
100045
|
-
if (type === "hottext") {
|
|
100046
|
-
const segments = interaction.textSegments;
|
|
100047
|
-
if (!segments.some((segment) => segment.selectable) || segments.some((segment) => segment.selectable && (!segment.identifier || !segment.content))) {
|
|
100048
|
-
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", "Hottext interactions require identified selectable content.");
|
|
100049
|
-
}
|
|
100050
|
-
return {
|
|
100051
|
-
type,
|
|
100052
|
-
responseIdentifier,
|
|
100053
|
-
...prompt,
|
|
100054
|
-
segments,
|
|
100055
|
-
maxChoices: qtiAttributeMaximum(interaction.attributes, "max-choices", 1, segments.filter((segment) => segment.selectable).length)
|
|
100056
|
-
};
|
|
100057
|
-
}
|
|
100058
|
-
const unsupported = type;
|
|
100059
|
-
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${questionIdentifier} uses unsupported interaction ${unsupported}.`);
|
|
100060
|
-
}
|
|
100061
|
-
function normalizedQtiItem(question) {
|
|
100062
|
-
if (!question.rawXml) {
|
|
100063
|
-
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${question.identifier} has no authoritative QTI XML.`);
|
|
100064
|
-
}
|
|
100065
|
-
try {
|
|
100066
|
-
return parseQtiItemXml(question.rawXml);
|
|
100067
|
-
} catch (error88) {
|
|
100068
|
-
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${question.identifier} contains invalid QTI XML.`, { reason: errorMessage(error88) });
|
|
100069
|
-
}
|
|
100070
|
-
}
|
|
100071
|
-
function parsePlayableItem(question) {
|
|
100072
|
-
const parsed = normalizedQtiItem(question);
|
|
100073
|
-
const interactions = playableQtiInteractions(parsed);
|
|
100074
|
-
if (!interactions) {
|
|
100075
|
-
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${question.identifier} must contain only supported interactions with unique response identifiers.`);
|
|
100076
|
-
}
|
|
100077
|
-
return {
|
|
100078
|
-
identifier: question.identifier,
|
|
100079
|
-
title: question.title,
|
|
100080
|
-
prompt: parsed.bodyTextWithBlanks,
|
|
100081
|
-
...parsed.bodyContent ? { promptContent: parsed.bodyContent } : {},
|
|
100082
|
-
maxScore: qtiItemMaxScore(parsed),
|
|
100083
|
-
interactions: interactions.map((interaction) => parseInteraction2(question.identifier, interaction))
|
|
100084
|
-
};
|
|
100085
|
-
}
|
|
100086
|
-
function assessmentContentRevision(test, questions) {
|
|
100087
|
-
return sha256Hex(canonicalJson2({
|
|
100088
|
-
identifier: test.identifier,
|
|
100089
|
-
title: test.title,
|
|
100090
|
-
qtiVersion: test.qtiVersion,
|
|
100091
|
-
parts: test["qti-test-part"],
|
|
100092
|
-
questions: questions.questions.map(({ reference, question }) => ({
|
|
100093
|
-
identifier: reference.identifier,
|
|
100094
|
-
rawXml: question.rawXml
|
|
100095
|
-
}))
|
|
100096
|
-
}));
|
|
100097
|
-
}
|
|
100098
|
-
async function buildPlayableAssessment(test, questions) {
|
|
100099
|
-
return {
|
|
100100
|
-
identifier: test.identifier,
|
|
100101
|
-
contractVersion: PLAYCADEMY_ASSESSMENT_CONTRACT_VERSION,
|
|
100102
|
-
contentRevision: await assessmentContentRevision(test, questions),
|
|
100103
|
-
title: test.title,
|
|
100104
|
-
items: questions.questions.map(({ question }) => parsePlayableItem(question))
|
|
100105
|
-
};
|
|
100106
|
-
}
|
|
100107
|
-
function assessmentFixtureScoringKeys(assessment, questions) {
|
|
100108
|
-
const questionByIdentifier = new Map(questions.questions.map(({ question }) => [question.identifier, question]));
|
|
100109
|
-
const correctResponses = {};
|
|
100110
|
-
const responseAreas = {};
|
|
100111
|
-
for (const item of assessment.items) {
|
|
100112
|
-
const question = questionByIdentifier.get(item.identifier);
|
|
100113
|
-
const normalizedDeclarations = question ? normalizedQtiItem(question).responseDeclarations : [];
|
|
100114
|
-
const itemResponses = {};
|
|
100115
|
-
for (const interaction of item.interactions) {
|
|
100116
|
-
const responseIdentifier = interaction.responseIdentifier;
|
|
100117
|
-
const declaration = question?.responseDeclarations?.find((candidate) => candidate.identifier === responseIdentifier);
|
|
100118
|
-
const declaredValues = declaration?.correctResponse?.value ?? [];
|
|
100119
|
-
const normalizedDeclaration = normalizedDeclarations.find((candidate) => candidate.identifier === responseIdentifier);
|
|
100120
|
-
const values = declaredValues.length > 0 ? declaredValues : normalizedDeclaration?.correctValues ?? [];
|
|
100121
|
-
const areas = interaction.type === "select-point" || interaction.type === "position-object" ? normalizedDeclaration?.areaMapping?.entries.filter((entry) => entry.value > 0 && isPlayableAssessmentShape(entry.shape)) : undefined;
|
|
100122
|
-
if (areas?.length) {
|
|
100123
|
-
(responseAreas[item.identifier] ??= {})[responseIdentifier] = areas.map((entry) => ({
|
|
100124
|
-
shape: entry.shape,
|
|
100125
|
-
coords: [...entry.coords],
|
|
100126
|
-
value: entry.value
|
|
100127
|
-
}));
|
|
100128
|
-
}
|
|
100129
|
-
if (values.length > 0) {
|
|
100130
|
-
const cardinality = declaration?.cardinality ?? normalizedDeclaration?.cardinality;
|
|
100131
|
-
itemResponses[responseIdentifier] = cardinality === "single" && values.length === 1 ? values[0] : values;
|
|
100132
|
-
} else if (!areas?.length) {
|
|
100133
|
-
throw new AssessmentRuntimeError("UNSUPPORTED_INTERACTION", `Assessment item ${item.identifier} has no fixture scoring key.`);
|
|
100134
|
-
}
|
|
100135
|
-
}
|
|
100136
|
-
correctResponses[item.identifier] = itemResponses;
|
|
100137
|
-
}
|
|
100138
|
-
return {
|
|
100139
|
-
correctResponses,
|
|
100140
|
-
...Object.keys(responseAreas).length > 0 ? { responseAreas } : {}
|
|
100141
|
-
};
|
|
100142
|
-
}
|
|
100143
|
-
function prepareDiagnosticAssessmentResponses(assessment, current, input) {
|
|
100144
|
-
const update2 = { [input.itemIdentifier]: input.responses };
|
|
100145
|
-
validateAssessmentResponseUpdate(assessment, update2);
|
|
100146
|
-
const responses = applyAssessmentResponseUpdate(current, update2);
|
|
100147
|
-
const item = assessment.items.find((candidate) => candidate.identifier === input.itemIdentifier);
|
|
100148
|
-
const itemResponses = responses[input.itemIdentifier];
|
|
100149
|
-
const missingResponse = item?.interactions.find((interaction) => itemResponses?.[interaction.responseIdentifier] === undefined);
|
|
100150
|
-
if (!item || !itemResponses || missingResponse) {
|
|
100151
|
-
throw new AssessmentRuntimeError("INVALID_RESPONSE", `Diagnostic item ${input.itemIdentifier} requires a response for every interaction.`, {
|
|
100152
|
-
itemIdentifier: input.itemIdentifier,
|
|
100153
|
-
...missingResponse ? { responseIdentifier: missingResponse.responseIdentifier } : {}
|
|
100154
|
-
});
|
|
100155
|
-
}
|
|
100156
|
-
validateAssessmentResponses(assessment, { [input.itemIdentifier]: itemResponses });
|
|
100157
|
-
return responses;
|
|
100158
|
-
}
|
|
100159
|
-
function validateAssessmentResponseUpdate(assessment, update2) {
|
|
100160
|
-
const message = assessmentResponseUpdateValidationMessage(assessment, update2);
|
|
100161
|
-
if (message) {
|
|
100162
|
-
throw new AssessmentRuntimeError("INVALID_RESPONSE", message);
|
|
100163
|
-
}
|
|
100164
|
-
}
|
|
100165
|
-
function validateAssessmentResponses(assessment, responses) {
|
|
100166
|
-
const message = assessmentResponseValidationMessage(assessment, responses);
|
|
100167
|
-
if (message) {
|
|
100168
|
-
throw new AssessmentRuntimeError("INVALID_RESPONSE", message);
|
|
100169
|
-
}
|
|
100170
|
-
}
|
|
100171
|
-
function validateAssessmentDraftResponses(assessment, responses) {
|
|
100172
|
-
const message = assessmentDraftResponseValidationMessage(assessment, responses);
|
|
100173
|
-
if (message) {
|
|
100174
|
-
throw new AssessmentRuntimeError("INVALID_RESPONSE", message);
|
|
100175
|
-
}
|
|
100176
|
-
}
|
|
100177
|
-
function assessmentScoringRequests(assessment, responses) {
|
|
100178
|
-
return assessment.items.map((item) => ({
|
|
100179
|
-
itemIdentifier: item.identifier,
|
|
100180
|
-
maxScore: item.maxScore,
|
|
100181
|
-
requests: item.interactions.map((interaction) => ({
|
|
100182
|
-
identifier: interaction.responseIdentifier,
|
|
100183
|
-
response: responses[item.identifier]?.[interaction.responseIdentifier] ?? ""
|
|
100184
|
-
}))
|
|
100185
|
-
}));
|
|
100186
|
-
}
|
|
100187
|
-
function buildAssessmentResultMetadata(input) {
|
|
100188
|
-
return {
|
|
100189
|
-
[PLAYCADEMY_ASSESSMENT_RESULT_METADATA_KEY]: input.metadata,
|
|
100190
|
-
assessmentPurpose: input.metadata.purpose,
|
|
100191
|
-
...assessmentPurposeClassificationFields2(input.metadata.purpose, input)
|
|
100192
|
-
};
|
|
100193
|
-
}
|
|
100194
|
-
function mergeAssessmentRuntimeMetadata(resultMetadata, metadata2) {
|
|
100195
|
-
return {
|
|
100196
|
-
...resultMetadata,
|
|
100197
|
-
[PLAYCADEMY_ASSESSMENT_RESULT_METADATA_KEY]: metadata2
|
|
100198
|
-
};
|
|
100199
|
-
}
|
|
100200
|
-
function aggregateAssessmentScore(itemScores, possible) {
|
|
100201
|
-
const earned = itemScores.reduce((sum, itemScore) => sum + (Number.isFinite(itemScore) ? itemScore : 0), 0);
|
|
100202
|
-
return {
|
|
100203
|
-
earned,
|
|
100204
|
-
possible,
|
|
100205
|
-
normalized: possible === 0 ? 0 : Math.min(1, Math.max(0, earned / possible))
|
|
100206
|
-
};
|
|
100207
|
-
}
|
|
100208
|
-
function countCorrectQuestions(verdicts) {
|
|
100209
|
-
return verdicts.filter((verdict) => verdict === true).length;
|
|
100210
|
-
}
|
|
100211
|
-
function assessmentScoreFromPercentage(percentage, possible) {
|
|
100212
|
-
const normalized = Math.min(1, Math.max(0, (percentage ?? 0) / 100));
|
|
100213
|
-
const earned = Math.round(normalized * possible * 1e6) / 1e6;
|
|
100214
|
-
return { earned, possible, normalized };
|
|
100215
|
-
}
|
|
100216
|
-
function buildAssessmentResultSubmission(input) {
|
|
100217
|
-
const metadata2 = {
|
|
100218
|
-
...input.metadata,
|
|
100219
|
-
submissionId: input.submissionId,
|
|
100220
|
-
score: input.score,
|
|
100221
|
-
completion: {
|
|
100222
|
-
testName: input.testName,
|
|
100223
|
-
courseName: input.courseName,
|
|
100224
|
-
totalQuestions: input.totalQuestions,
|
|
100225
|
-
correctQuestions: input.correctQuestions,
|
|
100226
|
-
...input.session ? { session: input.session } : {},
|
|
100227
|
-
...input.itemOutcomes ? { itemOutcomes: input.itemOutcomes } : {},
|
|
100228
|
-
...input.diagnosticTracks ? { diagnosticTracks: input.diagnosticTracks } : {}
|
|
100229
|
-
},
|
|
100230
|
-
updatedAt: input.timestamp
|
|
100231
|
-
};
|
|
100232
|
-
return {
|
|
100233
|
-
metadata: metadata2,
|
|
100234
|
-
resultUpdate: {
|
|
100235
|
-
status: input.result.status,
|
|
100236
|
-
assessmentLineItem: input.result.assessmentLineItem,
|
|
100237
|
-
student: input.result.student,
|
|
100238
|
-
score: input.score.normalized * 100,
|
|
100239
|
-
scoreDate: input.timestamp,
|
|
100240
|
-
...ASSESSMENT_ATTEMPT_AWAITING_AWARD,
|
|
100241
|
-
metadata: mergeAssessmentRuntimeMetadata(input.result.metadata, metadata2)
|
|
100242
|
-
}
|
|
100243
|
-
};
|
|
100244
|
-
}
|
|
100245
|
-
function buildAssessmentResultAwardFinalization(input) {
|
|
100246
|
-
const completion = input.metadata.completion;
|
|
100247
|
-
if (!completion || completion.totalQuestions === undefined) {
|
|
100248
|
-
throw new Error("A scored assessment must retain its completion facts before finalization");
|
|
100249
|
-
}
|
|
100250
|
-
const metadata2 = {
|
|
100251
|
-
...input.metadata,
|
|
100252
|
-
finalizedAt: input.timestamp,
|
|
100253
|
-
updatedAt: input.timestamp,
|
|
100254
|
-
award: input.award
|
|
100255
|
-
};
|
|
100256
|
-
const masteryRequested = input.award.masteredUnits !== undefined || input.award.masteredUnitsAbsolute !== undefined;
|
|
100257
|
-
return {
|
|
100258
|
-
metadata: metadata2,
|
|
100259
|
-
resultUpdate: {
|
|
100260
|
-
status: input.result.status,
|
|
100261
|
-
assessmentLineItem: input.result.assessmentLineItem,
|
|
100262
|
-
student: input.result.student,
|
|
100263
|
-
score: input.result.score ?? 0,
|
|
100264
|
-
scoreDate: input.result.scoreDate,
|
|
100265
|
-
...ASSESSMENT_ATTEMPT_COMPLETED,
|
|
100266
|
-
metadata: {
|
|
100267
|
-
...mergeAssessmentRuntimeMetadata(input.result.metadata, metadata2),
|
|
100268
|
-
xp: input.award.xpAwarded,
|
|
100269
|
-
...masteryRequested ? { masteredUnits: input.award.masteredUnitsApplied } : {},
|
|
100270
|
-
totalQuestions: completion.totalQuestions,
|
|
100271
|
-
correctQuestions: completion.correctQuestions,
|
|
100272
|
-
appName: input.appName
|
|
100273
|
-
}
|
|
100274
|
-
}
|
|
100275
|
-
};
|
|
100276
|
-
}
|
|
100277
|
-
var PLAYABLE_SHAPE_SET;
|
|
100278
|
-
var init_timeback_assessment_runtime_util = __esm(() => {
|
|
100279
|
-
init_src();
|
|
100280
|
-
init_assessment_runtime2();
|
|
100281
|
-
init_qti();
|
|
100282
|
-
init_types2();
|
|
100283
|
-
init_utils6();
|
|
100284
|
-
init_timeback4();
|
|
100285
|
-
init_uuid();
|
|
100286
|
-
init_errors2();
|
|
100287
|
-
PLAYABLE_SHAPE_SET = new Set(PLAYABLE_ASSESSMENT_SHAPES);
|
|
100288
|
-
});
|
|
100289
|
-
|
|
100290
101453
|
// ../api-core/src/utils/timeback-review-mapping.util.ts
|
|
100291
101454
|
function reviewBankTestItemIdentifiers(test) {
|
|
100292
101455
|
return qtiTestParts(test).flatMap((part) => part["qti-assessment-section"].flatMap((section) => (section["qti-assessment-item-ref"] ?? []).map((reference) => reference.identifier)));
|
|
@@ -100540,7 +101703,7 @@ async function prepareReviewMappingUpdate(test, questions) {
|
|
|
100540
101703
|
}
|
|
100541
101704
|
};
|
|
100542
101705
|
}
|
|
100543
|
-
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;
|
|
100544
101707
|
var init_timeback_review_mapping_util = __esm(() => {
|
|
100545
101708
|
init_esm();
|
|
100546
101709
|
init_assessment_runtime2();
|
|
@@ -100583,6 +101746,259 @@ var init_timeback_review_mapping_util = __esm(() => {
|
|
|
100583
101746
|
});
|
|
100584
101747
|
});
|
|
100585
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
|
+
|
|
100586
102002
|
// ../api-core/src/utils/timeback-assessment-publication.util.ts
|
|
100587
102003
|
function assessmentKeyFromManagedQtiIdentifier(identifier) {
|
|
100588
102004
|
const assessmentKey = /^playcademy-test\.(.+)\.[a-f0-9]{64}$/.exec(identifier)?.[1];
|
|
@@ -100704,7 +102120,7 @@ async function prepareManagedAssessmentPublication(input) {
|
|
|
100704
102120
|
sourceItemIdentifier: question.identifier,
|
|
100705
102121
|
qtiItemIdentifier,
|
|
100706
102122
|
contentHash,
|
|
100707
|
-
xml: canonicalManagedAssetUrls(rewriteQtiItemIdentity(declarationNeutralQtiXml(question.rawXml), qtiItemIdentifier, title)),
|
|
102123
|
+
xml: canonicalManagedAssetUrls(rewriteQtiItemIdentity(declarationNeutralQtiXml(question.rawXml).trim(), qtiItemIdentifier, title)),
|
|
100708
102124
|
metadata: {
|
|
100709
102125
|
...metadata2,
|
|
100710
102126
|
ownerSystem: PLAYCADEMY_QTI_OWNER_SYSTEM,
|
|
@@ -100793,7 +102209,11 @@ async function prepareManagedAssessmentPublication(input) {
|
|
|
100793
102209
|
qtiTestIdentifier,
|
|
100794
102210
|
testInput,
|
|
100795
102211
|
items,
|
|
100796
|
-
diagnosticRoutingManifest: compiledRouting
|
|
102212
|
+
diagnosticRoutingManifest: compiledRouting,
|
|
102213
|
+
artifactSource: {
|
|
102214
|
+
test: testInput,
|
|
102215
|
+
questions: physicalQuestions
|
|
102216
|
+
}
|
|
100797
102217
|
};
|
|
100798
102218
|
}
|
|
100799
102219
|
function assertManagedAssessmentPublication(test, expected) {
|
|
@@ -100991,7 +102411,7 @@ var init_timeback_qti_hydration_util = __esm(() => {
|
|
|
100991
102411
|
});
|
|
100992
102412
|
|
|
100993
102413
|
// ../api-core/src/services/timeback-assessment-runtime.service.ts
|
|
100994
|
-
var TimebackAssessmentRuntimeService;
|
|
102414
|
+
var AssessmentPreparationPrerequisiteError, TimebackAssessmentRuntimeService;
|
|
100995
102415
|
var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
100996
102416
|
init_drizzle_orm();
|
|
100997
102417
|
init_src();
|
|
@@ -101003,17 +102423,30 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
101003
102423
|
init_types2();
|
|
101004
102424
|
init_uuid();
|
|
101005
102425
|
init_errors2();
|
|
102426
|
+
init_assessment_artifact_runtime_util();
|
|
101006
102427
|
init_assessment_runtime_lock_util();
|
|
102428
|
+
init_timeback_assessment_artifacts_util();
|
|
102429
|
+
init_timeback_assessment_preparation_util();
|
|
101007
102430
|
init_timeback_assessment_publication_util();
|
|
101008
102431
|
init_timeback_assessment_rules_util();
|
|
101009
102432
|
init_timeback_assessment_runtime_util();
|
|
101010
102433
|
init_timeback_qti_hydration_util();
|
|
101011
102434
|
await __promiseAll([
|
|
101012
102435
|
init_dist5(),
|
|
101013
|
-
init_errors8()
|
|
102436
|
+
init_errors8(),
|
|
102437
|
+
init_assessment_artifact_loader_util()
|
|
101014
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
|
+
};
|
|
101015
102447
|
TimebackAssessmentRuntimeService = class TimebackAssessmentRuntimeService {
|
|
101016
102448
|
static ASSESSMENT_CACHE_LIMIT = 32;
|
|
102449
|
+
static PREPARATION_IN_FLIGHT_LIMIT = 32;
|
|
101017
102450
|
static ASSESSMENT_CACHE_TTL_MS = 60000;
|
|
101018
102451
|
static EXPORT_CONCURRENCY = 4;
|
|
101019
102452
|
static ITEM_SUBMISSION_MAX_PREPARATION_ATTEMPTS = 3;
|
|
@@ -101033,16 +102466,65 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
101033
102466
|
defaultTTL: TimebackAssessmentRuntimeService.ASSESSMENT_CACHE_TTL_MS,
|
|
101034
102467
|
maxSize: TimebackAssessmentRuntimeService.ASSESSMENT_CACHE_LIMIT
|
|
101035
102468
|
});
|
|
102469
|
+
artifactLoader;
|
|
102470
|
+
preparations = new AssessmentPreparationFlights(TimebackAssessmentRuntimeService.PREPARATION_IN_FLIGHT_LIMIT);
|
|
101036
102471
|
constructor(deps) {
|
|
101037
102472
|
this.deps = deps;
|
|
102473
|
+
this.artifactLoader = new AssessmentArtifactRuntimeLoader(deps.assessmentArtifacts, {
|
|
102474
|
+
maxSize: TimebackAssessmentRuntimeService.ASSESSMENT_CACHE_LIMIT
|
|
102475
|
+
});
|
|
102476
|
+
}
|
|
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;
|
|
101038
102511
|
}
|
|
101039
|
-
async start({
|
|
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({
|
|
101040
102523
|
gameId,
|
|
101041
102524
|
studentId,
|
|
101042
102525
|
input,
|
|
101043
102526
|
user
|
|
101044
102527
|
}) {
|
|
101045
|
-
await this.deps.validateDeveloperAccess(user, gameId);
|
|
101046
102528
|
if (input.purpose === "diagnostic") {
|
|
101047
102529
|
return this.startDiagnostic({ gameId, studentId, input, user });
|
|
101048
102530
|
}
|
|
@@ -101151,6 +102633,285 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
101151
102633
|
return this.snapshot(result, metadata2, assessment, context2.integration);
|
|
101152
102634
|
});
|
|
101153
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
|
+
}
|
|
101154
102915
|
async startDiagnostic({
|
|
101155
102916
|
gameId,
|
|
101156
102917
|
studentId,
|
|
@@ -101805,7 +103566,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
101805
103566
|
let scoring = null;
|
|
101806
103567
|
if (loaded.state.status === "in-progress" && expected?.nodeKey === input.routingNodeKey && expected.itemIdentifier === input.itemIdentifier && attempt.metadata.responseVersion === input.expectedResponseVersion) {
|
|
101807
103568
|
const responses = prepareDiagnosticAssessmentResponses(assessment, attempt.metadata.responses, input);
|
|
101808
|
-
scoring = await this.scoreItem(assessment, responses, input.itemIdentifier);
|
|
103569
|
+
scoring = await this.scoreItem(assessment, responses, input.itemIdentifier, assessmentArtifactIdentityFromAttempt(attempt.metadata));
|
|
101809
103570
|
}
|
|
101810
103571
|
return {
|
|
101811
103572
|
responseVersion: attempt.metadata.responseVersion,
|
|
@@ -101816,7 +103577,8 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
101816
103577
|
scoring
|
|
101817
103578
|
};
|
|
101818
103579
|
}
|
|
101819
|
-
async scoreItems(assessment, responses, itemIdentifiers) {
|
|
103580
|
+
async scoreItems(assessment, responses, artifactIdentity, itemIdentifiers) {
|
|
103581
|
+
await this.artifactLoader.loadMatchedScoring(assessment, artifactIdentity);
|
|
101820
103582
|
const client2 = this.requireClient();
|
|
101821
103583
|
const scoringItems = assessmentScoringRequests(assessment, responses).filter((item) => !itemIdentifiers || itemIdentifiers.has(item.itemIdentifier));
|
|
101822
103584
|
const scoringTasks = scoringItems.flatMap((item, itemIndex) => item.requests.map((request) => ({
|
|
@@ -101846,8 +103608,8 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
101846
103608
|
};
|
|
101847
103609
|
});
|
|
101848
103610
|
}
|
|
101849
|
-
async scoreItem(assessment, responses, itemIdentifier) {
|
|
101850
|
-
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]));
|
|
101851
103613
|
return scoring;
|
|
101852
103614
|
}
|
|
101853
103615
|
async prepareItemSubmission(params, input) {
|
|
@@ -101875,7 +103637,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
101875
103637
|
itemSubmissions: attempt.metadata.itemSubmissions,
|
|
101876
103638
|
assessment
|
|
101877
103639
|
}, input);
|
|
101878
|
-
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;
|
|
101879
103641
|
return {
|
|
101880
103642
|
responseVersion: attempt.metadata.responseVersion,
|
|
101881
103643
|
submissionId: input.submissionId,
|
|
@@ -101888,7 +103650,7 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
101888
103650
|
const assessment = await this.loadAttemptAssessment(attempt.metadata);
|
|
101889
103651
|
validateAssessmentResponses(assessment, attempt.metadata.responses);
|
|
101890
103652
|
const administeredItems = attempt.metadata.purpose === "review" ? administeredItemIdentifiers(attempt.metadata.itemSubmissions) : undefined;
|
|
101891
|
-
const itemResults = await this.scoreItems(assessment, attempt.metadata.responses, administeredItems);
|
|
103653
|
+
const itemResults = await this.scoreItems(assessment, attempt.metadata.responses, assessmentArtifactIdentityFromAttempt(attempt.metadata), administeredItems);
|
|
101892
103654
|
return {
|
|
101893
103655
|
responseVersion: attempt.metadata.responseVersion,
|
|
101894
103656
|
assessment,
|
|
@@ -102380,23 +104142,24 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
102380
104142
|
liveTestIntegrationIds: integrationIdsWithLiveTests
|
|
102381
104143
|
};
|
|
102382
104144
|
}
|
|
102383
|
-
async selectIntegration(candidates, studentId, input) {
|
|
104145
|
+
async selectIntegration(candidates, studentId, input, results) {
|
|
102384
104146
|
const {
|
|
102385
104147
|
rows: candidateRows,
|
|
102386
104148
|
enrollmentByCourse,
|
|
102387
104149
|
liveTestIntegrationIds: integrationIdsWithLiveTests
|
|
102388
104150
|
} = candidates;
|
|
102389
104151
|
const needsOpenAnnotation = candidateRows.length > 1 || candidateRows.some((row) => !integrationIdsWithLiveTests.has(row.id));
|
|
102390
|
-
const
|
|
104152
|
+
const openAttemptScope = {
|
|
102391
104153
|
studentId,
|
|
102392
104154
|
activityId: input.activityId,
|
|
102393
104155
|
purpose: input.purpose,
|
|
102394
|
-
enrollmentIdByCourse: new Map(candidateRows.map((row) => [
|
|
102395
|
-
row.courseId,
|
|
102396
|
-
enrollmentByCourse.get(row.courseId).id
|
|
102397
|
-
])),
|
|
104156
|
+
enrollmentIdByCourse: new Map(candidateRows.map((row) => [row.courseId, enrollmentByCourse.get(row.courseId).id])),
|
|
102398
104157
|
requestFingerprint: this.startRequestFingerprint(input)
|
|
102399
|
-
}
|
|
104158
|
+
};
|
|
104159
|
+
let openAttemptCourseIds = new Set;
|
|
104160
|
+
if (needsOpenAnnotation) {
|
|
104161
|
+
openAttemptCourseIds = results ? this.openAttemptCourseIdsFromResults(results, openAttemptScope) : await this.openAttemptCourseIds(openAttemptScope);
|
|
104162
|
+
}
|
|
102400
104163
|
const annotated = candidateRows.map((row) => ({
|
|
102401
104164
|
...row,
|
|
102402
104165
|
hasLiveTests: integrationIdsWithLiveTests.has(row.id),
|
|
@@ -102430,14 +104193,24 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
102430
104193
|
})).filter((test) => !matchesMasteryStandard || matchesMasteryStandard(test.standard));
|
|
102431
104194
|
}
|
|
102432
104195
|
async openAttemptCourseIds(scope) {
|
|
102433
|
-
|
|
104196
|
+
return this.openAttemptCourseIdsFromResults(await this.studentAssessmentResults(scope.studentId), scope);
|
|
104197
|
+
}
|
|
104198
|
+
async studentAssessmentResults(studentId) {
|
|
104199
|
+
const stream = this.requireClient().api.oneroster.assessmentResults.stream({
|
|
102434
104200
|
where: {
|
|
102435
|
-
"student.sourcedId":
|
|
104201
|
+
"student.sourcedId": studentId,
|
|
102436
104202
|
status: ONEROSTER_STATUS2.active
|
|
102437
104203
|
}
|
|
102438
104204
|
});
|
|
104205
|
+
const results = [];
|
|
104206
|
+
for await (const result of stream) {
|
|
104207
|
+
results.push(result);
|
|
104208
|
+
}
|
|
104209
|
+
return results;
|
|
104210
|
+
}
|
|
104211
|
+
openAttemptCourseIdsFromResults(results, scope) {
|
|
102439
104212
|
const courseIds = new Set;
|
|
102440
|
-
for
|
|
104213
|
+
for (const result of results) {
|
|
102441
104214
|
const metadata2 = playcademyAssessmentResultMetadata(result.metadata);
|
|
102442
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))) {
|
|
102443
104216
|
courseIds.add(metadata2.courseId);
|
|
@@ -102446,15 +104219,12 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
102446
104219
|
return courseIds;
|
|
102447
104220
|
}
|
|
102448
104221
|
async listAttempts(scope) {
|
|
102449
|
-
|
|
102450
|
-
|
|
102451
|
-
|
|
102452
|
-
status: ONEROSTER_STATUS2.active
|
|
102453
|
-
}
|
|
102454
|
-
});
|
|
104222
|
+
return this.attemptListing(await this.studentAssessmentResults(scope.studentId), scope);
|
|
104223
|
+
}
|
|
104224
|
+
attemptListing(results, scope) {
|
|
102455
104225
|
const attempts = new Map;
|
|
102456
104226
|
const reviewChildren = new Map;
|
|
102457
|
-
for
|
|
104227
|
+
for (const result of results) {
|
|
102458
104228
|
const metadata2 = playcademyAssessmentResultMetadata(result.metadata);
|
|
102459
104229
|
const childMetadata = scope.purpose === "review" ? playcademyAssessmentItemResultMetadata(result.metadata) : null;
|
|
102460
104230
|
if (childMetadata && childMetadata.activityId === scope.activityId && childMetadata.integrationId === scope.integrationId && childMetadata.enrollmentId === scope.enrollmentId && result.student.sourcedId === scope.studentId) {
|
|
@@ -102666,6 +104436,57 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
102666
104436
|
...counts.selectedCount === undefined ? {} : { "app.assessment.selected_count": counts.selectedCount }
|
|
102667
104437
|
});
|
|
102668
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
|
+
}
|
|
102669
104490
|
async loadReviewBankCatalog(identifier, expectedSourceRevision) {
|
|
102670
104491
|
if (expectedSourceRevision) {
|
|
102671
104492
|
const cached3 = this.reviewBankCache.get(`${identifier}\x00${expectedSourceRevision}`);
|
|
@@ -102718,6 +104539,16 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
102718
104539
|
return assessment;
|
|
102719
104540
|
}
|
|
102720
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
|
+
}
|
|
102721
104552
|
const assessment = await this.cachedReviewSelection({
|
|
102722
104553
|
identifier: catalog.test.identifier,
|
|
102723
104554
|
contentRevision: catalog.bank.sourceContentRevision,
|
|
@@ -102731,6 +104562,14 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
102731
104562
|
}
|
|
102732
104563
|
async loadPinnedReviewBankSource(metadata2) {
|
|
102733
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
|
+
}
|
|
102734
104573
|
let assessment;
|
|
102735
104574
|
try {
|
|
102736
104575
|
assessment = await this.cachedReviewSelection({
|
|
@@ -102756,18 +104595,19 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
102756
104595
|
}
|
|
102757
104596
|
return {
|
|
102758
104597
|
assessment,
|
|
102759
|
-
bank:
|
|
102760
|
-
bankIdentifier: assessment.identifier,
|
|
102761
|
-
sourceContentRevision: metadata2.selectedTest.contentRevision,
|
|
102762
|
-
bankRevision: metadata2.review.bankRevision,
|
|
102763
|
-
items: metadata2.review.selections.map((selection) => ({
|
|
102764
|
-
itemIdentifier: selection.itemIdentifier,
|
|
102765
|
-
standards: [selection.standard]
|
|
102766
|
-
}))
|
|
102767
|
-
}
|
|
104598
|
+
bank: pinnedReviewBankFromAttempt(metadata2, assessment.identifier)
|
|
102768
104599
|
};
|
|
102769
104600
|
}
|
|
102770
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");
|
|
102771
104611
|
if (expectedRevision) {
|
|
102772
104612
|
const cached3 = this.assessmentCache.get(`${identifier}\x00${expectedRevision}`);
|
|
102773
104613
|
if (cached3) {
|
|
@@ -103535,6 +105375,72 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
103535
105375
|
scoreFromResult(result, possible, metadata2) {
|
|
103536
105376
|
return metadata2.score ?? assessmentScoreFromPercentage(result.score, possible);
|
|
103537
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
|
+
}
|
|
103538
105444
|
async claimAttemptId(scope, from) {
|
|
103539
105445
|
for (let attemptNumber = from;attemptNumber < from + 20; attemptNumber += 1) {
|
|
103540
105446
|
const attemptId = await assessmentAttemptId({ ...scope, attemptNumber });
|
|
@@ -103690,6 +105596,35 @@ var init_timeback_assessment_runtime_service = __esm(async () => {
|
|
|
103690
105596
|
};
|
|
103691
105597
|
});
|
|
103692
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
|
+
|
|
103693
105628
|
// ../api-core/src/utils/timeback-assessment-diagnostic.util.ts
|
|
103694
105629
|
function diagnosticRoutingCapabilities(assessment, scoring) {
|
|
103695
105630
|
return assessment.items.map((item) => ({
|
|
@@ -104030,7 +105965,7 @@ class TimebackAssessmentsService {
|
|
|
104030
105965
|
assertManagedAssessmentIdentity(loaded.test, entry.assessmentKey);
|
|
104031
105966
|
assertAssessmentHasQuestions(loaded.questions.questions);
|
|
104032
105967
|
assertPlayableAssessmentImportQuestions(targetStatus, loaded.questions.questions.map(({ question }) => question));
|
|
104033
|
-
return { entry,
|
|
105968
|
+
return { entry, source: loaded };
|
|
104034
105969
|
} catch (error88) {
|
|
104035
105970
|
return { entry, error: error88 };
|
|
104036
105971
|
}
|
|
@@ -104050,7 +105985,7 @@ class TimebackAssessmentsService {
|
|
|
104050
105985
|
const pendingInserts = [];
|
|
104051
105986
|
for (const validation of input.validated) {
|
|
104052
105987
|
const { entry } = validation;
|
|
104053
|
-
const base = this.associationImportResultBase(entry, "
|
|
105988
|
+
const base = this.associationImportResultBase(entry, "source" in validation ? validation.source.test : undefined, input.gameSlug);
|
|
104054
105989
|
if ("error" in validation) {
|
|
104055
105990
|
input.results[entry.index] = {
|
|
104056
105991
|
...base,
|
|
@@ -104061,7 +105996,7 @@ class TimebackAssessmentsService {
|
|
|
104061
105996
|
const existingForKey = input.existingByKey.get(entry.assessmentKey);
|
|
104062
105997
|
const decision = planAssessmentAssociationImport(entry, existingForKey, input.existingByIdentifier.get(entry.qtiTestIdentifier));
|
|
104063
105998
|
if (decision.kind === "insert") {
|
|
104064
|
-
pendingInserts.push({ entry,
|
|
105999
|
+
pendingInserts.push({ entry, source: validation.source });
|
|
104065
106000
|
} else {
|
|
104066
106001
|
input.results[entry.index] = {
|
|
104067
106002
|
...base,
|
|
@@ -104093,9 +106028,13 @@ class TimebackAssessmentsService {
|
|
|
104093
106028
|
}
|
|
104094
106029
|
}
|
|
104095
106030
|
async insertImportedAssessments(input) {
|
|
104096
|
-
for (const { entry,
|
|
104097
|
-
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);
|
|
104098
106033
|
try {
|
|
106034
|
+
let artifactsAvailable = true;
|
|
106035
|
+
if (input.targetStatus === "live") {
|
|
106036
|
+
artifactsAvailable = await this.storeManagedArtifacts(source, managedAssessmentDiagnosticRoutingManifest(source.test, entry.assessmentKey));
|
|
106037
|
+
}
|
|
104099
106038
|
const [row] = await input.db.insert(gameTimebackAssessmentTests).values({
|
|
104100
106039
|
integrationId: input.integrationId,
|
|
104101
106040
|
assessmentKey: entry.assessmentKey,
|
|
@@ -104118,11 +106057,12 @@ class TimebackAssessmentsService {
|
|
|
104118
106057
|
throw new Error("Assessment association insert conflicted");
|
|
104119
106058
|
}
|
|
104120
106059
|
} else {
|
|
106060
|
+
const message = attachedAssessmentImportMessage(input.targetStatus, base.editable ?? false);
|
|
104121
106061
|
input.results[entry.index] = {
|
|
104122
106062
|
...base,
|
|
104123
106063
|
status: "created",
|
|
104124
106064
|
association: this.associationImportSummary(row),
|
|
104125
|
-
message:
|
|
106065
|
+
message: assessmentArtifactImportMessage(message, artifactsAvailable)
|
|
104126
106066
|
};
|
|
104127
106067
|
}
|
|
104128
106068
|
} catch (error88) {
|
|
@@ -104188,6 +106128,7 @@ class TimebackAssessmentsService {
|
|
|
104188
106128
|
assertQtiTestOwnedByGame(test, ownership.gameSlug);
|
|
104189
106129
|
await client2.qtiApi.assessmentTests.update(qtiTestIdentifier, buildQtiTestUpdateInput(test, input.title));
|
|
104190
106130
|
}
|
|
106131
|
+
let publicationWarning;
|
|
104191
106132
|
if (publishing) {
|
|
104192
106133
|
if (nextPurpose === "diagnostic" && !nextDiagnostic) {
|
|
104193
106134
|
throw new ValidationError("Platform-routed diagnostics require a diagnostic key and routing manifest before publication.");
|
|
@@ -104203,6 +106144,7 @@ class TimebackAssessmentsService {
|
|
|
104203
106144
|
if (publishing) {
|
|
104204
106145
|
const publication = await this.publishManagedAssessment(row, nextPurpose, nextDiagnostic?.routingManifest ?? null, loaded);
|
|
104205
106146
|
updates.qtiTestIdentifier = publication.qtiTestIdentifier;
|
|
106147
|
+
publicationWarning = publication.warning;
|
|
104206
106148
|
if (nextPurpose === "diagnostic") {
|
|
104207
106149
|
updates.diagnosticRoutingManifest = publication.diagnosticRoutingManifest;
|
|
104208
106150
|
}
|
|
@@ -104214,7 +106156,7 @@ class TimebackAssessmentsService {
|
|
|
104214
106156
|
updated = updatedRow ?? row;
|
|
104215
106157
|
}
|
|
104216
106158
|
setAttribute("app.assessment.operation", input.title !== undefined ? "update_test" : "update_association");
|
|
104217
|
-
return this.associationSummary(updated);
|
|
106159
|
+
return this.associationSummary(updated, publicationWarning);
|
|
104218
106160
|
});
|
|
104219
106161
|
} catch (error88) {
|
|
104220
106162
|
if (isUniqueViolation(error88)) {
|
|
@@ -104297,7 +106239,7 @@ class TimebackAssessmentsService {
|
|
|
104297
106239
|
assertReviewBankSinkOwnership(existingSink, integrationId);
|
|
104298
106240
|
}
|
|
104299
106241
|
let unchangedResult = null;
|
|
104300
|
-
if (existingSink) {
|
|
106242
|
+
if (existingSink?.title === PLAYCADEMY_REVIEW_BANK_TITLE) {
|
|
104301
106243
|
try {
|
|
104302
106244
|
unchangedResult = await unchangedReviewBankSynchronizationResult({
|
|
104303
106245
|
sink: existingSink,
|
|
@@ -104331,8 +106273,17 @@ class TimebackAssessmentsService {
|
|
|
104331
106273
|
const diff = reviewBankSynchronizationDiff({ existingSink, plan });
|
|
104332
106274
|
addedItemCount = diff.addedItemCount;
|
|
104333
106275
|
removedItemCount = diff.removedItemCount;
|
|
104334
|
-
catalogUnchanged =
|
|
106276
|
+
catalogUnchanged = false;
|
|
104335
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);
|
|
104336
106287
|
const committed = await this.withLockedIntegrationAssessmentRows(integrationId, async (tx, lockedAssociations) => {
|
|
104337
106288
|
const lockedSources = this.reviewBankMasterySources(lockedAssociations);
|
|
104338
106289
|
const lockedFingerprint = await reviewBankContributorFingerprint(lockedSources);
|
|
@@ -104365,7 +106316,10 @@ class TimebackAssessmentsService {
|
|
|
104365
106316
|
unchanged,
|
|
104366
106317
|
warnings: plan.warnings
|
|
104367
106318
|
};
|
|
104368
|
-
return this.reviewMappingResult(
|
|
106319
|
+
return this.reviewMappingResult({
|
|
106320
|
+
...result,
|
|
106321
|
+
warnings: assessmentArtifactReviewBankWarnings(result.warnings, artifactsAvailable)
|
|
106322
|
+
});
|
|
104369
106323
|
});
|
|
104370
106324
|
if (committed) {
|
|
104371
106325
|
return committed;
|
|
@@ -104667,6 +106621,36 @@ class TimebackAssessmentsService {
|
|
|
104667
106621
|
};
|
|
104668
106622
|
});
|
|
104669
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
|
+
}
|
|
104670
106654
|
async writeReviewBankSink(input) {
|
|
104671
106655
|
function sinkInput(title, includeIdentifier = false) {
|
|
104672
106656
|
return buildReviewBankSinkInput({
|
|
@@ -104679,11 +106663,11 @@ class TimebackAssessmentsService {
|
|
|
104679
106663
|
});
|
|
104680
106664
|
}
|
|
104681
106665
|
if (input.existingSink) {
|
|
104682
|
-
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));
|
|
104683
106667
|
return;
|
|
104684
106668
|
}
|
|
104685
106669
|
try {
|
|
104686
|
-
const createInput = sinkInput(
|
|
106670
|
+
const createInput = sinkInput(PLAYCADEMY_REVIEW_BANK_TITLE, true);
|
|
104687
106671
|
await input.client.qtiApi.assessmentTests.create(createInput);
|
|
104688
106672
|
} catch (error88) {
|
|
104689
106673
|
if (!isApiError(error88) || error88.statusCode !== 409) {
|
|
@@ -104691,7 +106675,7 @@ class TimebackAssessmentsService {
|
|
|
104691
106675
|
}
|
|
104692
106676
|
const concurrentlyCreated = await input.client.qtiApi.assessmentTests.get(input.plan.bankIdentifier);
|
|
104693
106677
|
assertReviewBankSinkOwnership(concurrentlyCreated, input.integrationId);
|
|
104694
|
-
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));
|
|
104695
106679
|
}
|
|
104696
106680
|
}
|
|
104697
106681
|
async installReviewBankAssociation(input) {
|
|
@@ -104876,15 +106860,64 @@ class TimebackAssessmentsService {
|
|
|
104876
106860
|
return get();
|
|
104877
106861
|
}
|
|
104878
106862
|
async ensureManagedQtiPublication(client2, plan) {
|
|
104879
|
-
await runWithConcurrency(plan.items, QTI_HYDRATION_CONCURRENCY, async (plannedItem) => {
|
|
106863
|
+
const existingItems = await runWithConcurrency(plan.items, QTI_HYDRATION_CONCURRENCY, async (plannedItem) => {
|
|
104880
106864
|
const existing = await this.ensureImmutableQtiResource(async () => await client2.qtiApi.assessmentItems.get(plannedItem.qtiItemIdentifier), () => client2.course.createAssessmentItemXml({
|
|
104881
106865
|
xml: plannedItem.xml,
|
|
104882
106866
|
metadata: plannedItem.metadata
|
|
104883
106867
|
}));
|
|
104884
106868
|
await assertManagedAssessmentItem(existing, plannedItem);
|
|
106869
|
+
return existing;
|
|
104885
106870
|
});
|
|
104886
106871
|
const existingTest = await this.ensureImmutableQtiResource(async () => await client2.qtiApi.assessmentTests.get(plan.qtiTestIdentifier), () => client2.qtiApi.assessmentTests.create(plan.testInput));
|
|
104887
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 }));
|
|
104888
106921
|
}
|
|
104889
106922
|
async publishManagedAssessment(row, purpose, diagnosticRoutingManifest, loaded) {
|
|
104890
106923
|
if (!row.assessmentKey) {
|
|
@@ -104899,8 +106932,10 @@ class TimebackAssessmentsService {
|
|
|
104899
106932
|
diagnosticRoutingManifest,
|
|
104900
106933
|
itemHref: (identifier) => qtiItemHref(client2, identifier)
|
|
104901
106934
|
});
|
|
104902
|
-
await this.ensureManagedQtiPublication(client2, plan);
|
|
104903
|
-
|
|
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 } : {} };
|
|
104904
106939
|
}
|
|
104905
106940
|
async cleanupQtiAssessmentCopy(client2, testIdentifier, itemIdentifiers) {
|
|
104906
106941
|
let testDeleted = !testIdentifier;
|
|
@@ -104933,7 +106968,7 @@ class TimebackAssessmentsService {
|
|
|
104933
106968
|
}
|
|
104934
106969
|
}
|
|
104935
106970
|
}
|
|
104936
|
-
associationSummary(row) {
|
|
106971
|
+
associationSummary(row, warning) {
|
|
104937
106972
|
return {
|
|
104938
106973
|
id: row.id,
|
|
104939
106974
|
integrationId: row.integrationId,
|
|
@@ -104944,7 +106979,8 @@ class TimebackAssessmentsService {
|
|
|
104944
106979
|
standard: assessmentStandardForRow(row),
|
|
104945
106980
|
diagnostic: diagnosticDefinitionForRow(row),
|
|
104946
106981
|
createdAt: row.createdAt,
|
|
104947
|
-
updatedAt: row.updatedAt
|
|
106982
|
+
updatedAt: row.updatedAt,
|
|
106983
|
+
...warning ? { warning } : {}
|
|
104948
106984
|
};
|
|
104949
106985
|
}
|
|
104950
106986
|
associationImportSummary(row) {
|
|
@@ -105012,7 +107048,7 @@ class TimebackAssessmentsService {
|
|
|
105012
107048
|
};
|
|
105013
107049
|
}
|
|
105014
107050
|
}
|
|
105015
|
-
var
|
|
107051
|
+
var REVIEW_BANK_SYNC_ATTEMPTS = 2;
|
|
105016
107052
|
var init_timeback_assessments_service = __esm(async () => {
|
|
105017
107053
|
init_drizzle_orm();
|
|
105018
107054
|
init_helpers_index();
|
|
@@ -105021,6 +107057,8 @@ var init_timeback_assessments_service = __esm(async () => {
|
|
|
105021
107057
|
init_assessment_runtime2();
|
|
105022
107058
|
init_timeback3();
|
|
105023
107059
|
init_errors2();
|
|
107060
|
+
init_assessment_artifact_fallback_util();
|
|
107061
|
+
init_timeback_assessment_artifacts_util();
|
|
105024
107062
|
init_timeback_assessment_diagnostic_util();
|
|
105025
107063
|
init_timeback_assessment_import_util();
|
|
105026
107064
|
init_timeback_assessment_publication_util();
|
|
@@ -107044,6 +109082,8 @@ function createPlatformServices(deps) {
|
|
|
107044
109082
|
timebackClient,
|
|
107045
109083
|
alerts,
|
|
107046
109084
|
mintPlatformServiceToken,
|
|
109085
|
+
mintAssessmentPreparationReceipt,
|
|
109086
|
+
verifyAssessmentPreparationReceipt,
|
|
107047
109087
|
validateDeveloperAccessBySlug,
|
|
107048
109088
|
validateDeveloperAccess,
|
|
107049
109089
|
validateGameManagementAccess
|
|
@@ -107059,6 +109099,10 @@ function createPlatformServices(deps) {
|
|
|
107059
109099
|
environment: getPlatformEnvironment(config5),
|
|
107060
109100
|
storage
|
|
107061
109101
|
});
|
|
109102
|
+
const assessmentArtifacts = new AssessmentArtifactsService({
|
|
109103
|
+
bucketName: config5.assessmentArtifactBucket,
|
|
109104
|
+
storage
|
|
109105
|
+
});
|
|
107062
109106
|
const upload = new UploadService({
|
|
107063
109107
|
uploadBucket: config5.uploadBucket,
|
|
107064
109108
|
generatePresignedPutUrl: storage.generatePresignedPutUrl.bind(storage),
|
|
@@ -107104,16 +109148,21 @@ function createPlatformServices(deps) {
|
|
|
107104
109148
|
const timebackAssessments = new TimebackAssessmentsService({
|
|
107105
109149
|
db: db2,
|
|
107106
109150
|
timeback: timebackClient,
|
|
109151
|
+
assessmentArtifacts,
|
|
107107
109152
|
validateGameManagementAccess
|
|
107108
109153
|
});
|
|
107109
109154
|
const timebackAssessmentRuntime = new TimebackAssessmentRuntimeService({
|
|
107110
109155
|
db: db2,
|
|
107111
109156
|
timeback: timebackClient,
|
|
109157
|
+
assessmentArtifacts,
|
|
107112
109158
|
validateDeveloperAccess,
|
|
107113
|
-
assessmentRuntimeLock
|
|
109159
|
+
assessmentRuntimeLock,
|
|
109160
|
+
mintAssessmentPreparationReceipt,
|
|
109161
|
+
verifyAssessmentPreparationReceipt
|
|
107114
109162
|
});
|
|
107115
109163
|
return {
|
|
107116
109164
|
assessmentAssets,
|
|
109165
|
+
assessmentArtifacts,
|
|
107117
109166
|
bucket,
|
|
107118
109167
|
upload,
|
|
107119
109168
|
kv,
|
|
@@ -107130,6 +109179,7 @@ function createPlatformServices(deps) {
|
|
|
107130
109179
|
}
|
|
107131
109180
|
var init_platform2 = __esm(async () => {
|
|
107132
109181
|
init_config2();
|
|
109182
|
+
init_assessment_artifacts_service();
|
|
107133
109183
|
init_assessment_assets_service();
|
|
107134
109184
|
init_bucket_service();
|
|
107135
109185
|
init_database_service();
|
|
@@ -107856,6 +109906,8 @@ function createServices(ctx) {
|
|
|
107856
109906
|
timebackClient: timeback2,
|
|
107857
109907
|
alerts: infra2.alerts,
|
|
107858
109908
|
mintPlatformServiceToken: auth2.mintPlatformServiceToken.bind(auth2),
|
|
109909
|
+
mintAssessmentPreparationReceipt: auth2.mintAssessmentPreparationReceipt.bind(auth2),
|
|
109910
|
+
verifyAssessmentPreparationReceipt: auth2.verifyAssessmentPreparationReceipt.bind(auth2),
|
|
107859
109911
|
...gaming.validators
|
|
107860
109912
|
});
|
|
107861
109913
|
const standalone = createStandaloneServices({ db: db2, auth: auth2, timeback: timeback2 });
|
|
@@ -107995,6 +110047,15 @@ function defineControllerNames(namespace, handlers) {
|
|
|
107995
110047
|
return handlers;
|
|
107996
110048
|
}
|
|
107997
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
|
+
|
|
107998
110059
|
// ../api-core/src/utils/lti.util.ts
|
|
107999
110060
|
function generateUsername(email5) {
|
|
108000
110061
|
const baseUsername = (email5.split("@")[0] || "user").toLowerCase();
|
|
@@ -108200,6 +110261,9 @@ var init_validation_util = __esm(() => {
|
|
|
108200
110261
|
var init_utils7 = __esm(() => {
|
|
108201
110262
|
init_auth_util();
|
|
108202
110263
|
init_assessment_runtime_lock_util();
|
|
110264
|
+
init_assessment_artifact_envelope_util();
|
|
110265
|
+
init_assessment_artifact_backfill_util();
|
|
110266
|
+
init_timeback_assessment_artifacts_util();
|
|
108203
110267
|
init_dashboard_util();
|
|
108204
110268
|
init_deployment_util();
|
|
108205
110269
|
init_leaderboard_util();
|
|
@@ -108357,6 +110421,32 @@ function createSandboxAuthProvider() {
|
|
|
108357
110421
|
}));
|
|
108358
110422
|
return `${header}.${payload}.sandbox`;
|
|
108359
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
|
+
},
|
|
108360
110450
|
async mintLogStreamToken(userId, workerId) {
|
|
108361
110451
|
const jti = crypto.randomUUID();
|
|
108362
110452
|
const payload = {
|
|
@@ -108398,11 +110488,13 @@ function createSandboxAuthProvider() {
|
|
|
108398
110488
|
}
|
|
108399
110489
|
};
|
|
108400
110490
|
}
|
|
108401
|
-
var sandboxApiKeys;
|
|
110491
|
+
var sandboxApiKeys, sandboxAssessmentPreparationReceipts, ASSESSMENT_PREPARATION_RECEIPT_LIFETIME_MS;
|
|
108402
110492
|
var init_auth_provider = __esm(() => {
|
|
108403
110493
|
init_src6();
|
|
108404
110494
|
init_constants();
|
|
108405
110495
|
sandboxApiKeys = new Map;
|
|
110496
|
+
sandboxAssessmentPreparationReceipts = new Map;
|
|
110497
|
+
ASSESSMENT_PREPARATION_RECEIPT_LIFETIME_MS = 5 * 60 * 1000;
|
|
108406
110498
|
});
|
|
108407
110499
|
|
|
108408
110500
|
// src/infrastructure/api/providers/cache.provider.ts
|
|
@@ -108577,6 +110669,7 @@ function buildConfig(options) {
|
|
|
108577
110669
|
baseUrl,
|
|
108578
110670
|
gameDomain: "localhost",
|
|
108579
110671
|
uploadBucket: "sandbox-uploads",
|
|
110672
|
+
assessmentArtifactBucket: "sandbox-assessment-artifacts",
|
|
108580
110673
|
ltiTestMode: true,
|
|
108581
110674
|
secretsManifestPepper: "sandbox-secrets-manifest-pepper",
|
|
108582
110675
|
...options.config
|
|
@@ -165034,7 +167127,7 @@ function parseQtiLibraryParams(searchParams) {
|
|
|
165034
167127
|
limit
|
|
165035
167128
|
};
|
|
165036
167129
|
}
|
|
165037
|
-
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, reorderQuestions, removeAssessment, listQuestions, listQuestionLibrary, listTestLibrary, copyAssessment, createQuestion, updateQuestion, removeQuestion, timeback2;
|
|
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;
|
|
165038
167131
|
var init_timeback_controller = __esm(() => {
|
|
165039
167132
|
init_esm();
|
|
165040
167133
|
init_src();
|
|
@@ -165278,13 +167371,24 @@ var init_timeback_controller = __esm(() => {
|
|
|
165278
167371
|
user: ctx.user
|
|
165279
167372
|
});
|
|
165280
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
|
+
});
|
|
165281
167384
|
startRuntimeAssessment = requireDeveloper(async (ctx) => {
|
|
165282
167385
|
const body2 = await parseRequestBody(ctx.request, StartRuntimeAssessmentRequestSchema);
|
|
165283
|
-
const { gameId, studentId, ...input } = body2;
|
|
167386
|
+
const { gameId, studentId, preparationReceipt, ...input } = body2;
|
|
165284
167387
|
return ctx.services.timebackAssessmentRuntime.start({
|
|
165285
167388
|
gameId,
|
|
165286
167389
|
studentId,
|
|
165287
167390
|
input,
|
|
167391
|
+
preparationReceipt,
|
|
165288
167392
|
user: ctx.user
|
|
165289
167393
|
});
|
|
165290
167394
|
});
|
|
@@ -165847,6 +167951,7 @@ var init_timeback_controller = __esm(() => {
|
|
|
165847
167951
|
heartbeat,
|
|
165848
167952
|
advanceCourse,
|
|
165849
167953
|
unenrollCourse,
|
|
167954
|
+
prepareRuntimeAssessment,
|
|
165850
167955
|
startRuntimeAssessment,
|
|
165851
167956
|
getLatestRuntimeAssessment,
|
|
165852
167957
|
getRuntimeAssessment,
|
|
@@ -166839,6 +168944,7 @@ var init_timeback8 = __esm(async () => {
|
|
|
166839
168944
|
timebackRouter.post("/heartbeat", handle2(timeback2.heartbeat));
|
|
166840
168945
|
timebackRouter.post("/advance-course", handle2(timeback2.advanceCourse));
|
|
166841
168946
|
timebackRouter.post("/unenroll-course", handle2(timeback2.unenrollCourse));
|
|
168947
|
+
timebackRouter.post("/assessments/prepare", handle2(timeback2.prepareRuntimeAssessment));
|
|
166842
168948
|
timebackRouter.post("/assessments/start", handle2(timeback2.startRuntimeAssessment));
|
|
166843
168949
|
timebackRouter.get("/assessments/latest", handle2(timeback2.getLatestRuntimeAssessment));
|
|
166844
168950
|
timebackRouter.get("/assessments/:attemptId", handle2(timeback2.getRuntimeAssessment));
|