@playcademy/sandbox 0.12.2-beta.2 → 0.12.2-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 +535 -133
- package/dist/server.js +535 -133
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -697,7 +697,7 @@ var package_default;
|
|
|
697
697
|
var init_package = __esm(() => {
|
|
698
698
|
package_default = {
|
|
699
699
|
name: "@playcademy/sandbox",
|
|
700
|
-
version: "0.12.2-beta.
|
|
700
|
+
version: "0.12.2-beta.4",
|
|
701
701
|
description: "Local development server for Playcademy game development",
|
|
702
702
|
type: "module",
|
|
703
703
|
exports: {
|
|
@@ -21068,6 +21068,9 @@ var init_helpers = __esm(() => {
|
|
|
21068
21068
|
function isBaseGameTimebackIntegration() {
|
|
21069
21069
|
return sql`${gameTimebackIntegrations.kind} = ${BASE_GAME_TIMEBACK_INTEGRATION_KIND}`;
|
|
21070
21070
|
}
|
|
21071
|
+
function isBaseGameTimebackIntegrationRow(integration) {
|
|
21072
|
+
return integration.kind === BASE_GAME_TIMEBACK_INTEGRATION_KIND;
|
|
21073
|
+
}
|
|
21071
21074
|
function isActiveGameTimebackIntegrationStatus(statusColumn = gameTimebackIntegrations.status) {
|
|
21072
21075
|
return sql`${statusColumn} IS DISTINCT FROM ${DEACTIVATED_GAME_TIMEBACK_INTEGRATION_STATUS}`;
|
|
21073
21076
|
}
|
|
@@ -48676,6 +48679,7 @@ var init_schemas4 = __esm(() => {
|
|
|
48676
48679
|
appName: stringType().optional(),
|
|
48677
48680
|
sensorUrl: stringType().url().optional(),
|
|
48678
48681
|
courseId: stringType().min(1).optional(),
|
|
48682
|
+
enrollmentId: stringType().min(1).optional(),
|
|
48679
48683
|
courseName: stringType().optional(),
|
|
48680
48684
|
studentEmail: stringType().email().optional()
|
|
48681
48685
|
});
|
|
@@ -80041,6 +80045,9 @@ function normalizedIdentityCase2(value) {
|
|
|
80041
80045
|
function frameworkAliasKey2(value) {
|
|
80042
80046
|
return value.normalize("NFKC").toLocaleLowerCase("en-US").replace(/[^a-z0-9]+/g, "");
|
|
80043
80047
|
}
|
|
80048
|
+
function isUuidShaped2(value) {
|
|
80049
|
+
return UUID_PATTERN2.test(value.trim());
|
|
80050
|
+
}
|
|
80044
80051
|
function isCommonCoreMathIdentifier2(identifier) {
|
|
80045
80052
|
const canonical = normalizedIdentityCase2(normalizedWhitespace2(identifier));
|
|
80046
80053
|
return canonical.startsWith("CCSS.MATH.") || /^(?:K|[1-8])\.(?:CC|OA|NBT|NF|MD|RP|NS|EE|F|G|SP)\./.test(canonical) || /^(?:HS)?(?:N|A|F|G|S)-[A-Z]+\./.test(canonical) || /^MP\.?\d/.test(canonical);
|
|
@@ -80072,6 +80079,14 @@ function canonicalIdentifier2(framework, authoredIdentifier) {
|
|
|
80072
80079
|
}
|
|
80073
80080
|
return identifier;
|
|
80074
80081
|
}
|
|
80082
|
+
function readableAlignmentIdentifier2(alignment) {
|
|
80083
|
+
const identifier = alignment.identifier?.trim() ?? "";
|
|
80084
|
+
if (identifier && !isUuidShaped2(identifier)) {
|
|
80085
|
+
return identifier;
|
|
80086
|
+
}
|
|
80087
|
+
const id = alignment.id.trim();
|
|
80088
|
+
return id && !isUuidShaped2(id) ? id : "";
|
|
80089
|
+
}
|
|
80075
80090
|
function canonicalAssessmentStandardRef2(standard) {
|
|
80076
80091
|
const authoredFramework = normalizedWhitespace2(standard.framework);
|
|
80077
80092
|
const framework = canonicalFramework2(authoredFramework, standard.identifier);
|
|
@@ -80178,7 +80193,23 @@ function qtiStandardsFromMetadata(metadata2) {
|
|
|
80178
80193
|
standards.push(...qtiAlignmentStandardsFromMetadata2(metadata2));
|
|
80179
80194
|
return dedupeStandards2(standards);
|
|
80180
80195
|
}
|
|
80181
|
-
|
|
80196
|
+
function qtiAssessmentStandardRefsFromMetadata2(metadata2) {
|
|
80197
|
+
const refs = new Map;
|
|
80198
|
+
for (const alignment of qtiAlignmentStandardsFromMetadata2(metadata2)) {
|
|
80199
|
+
const identifier = readableAlignmentIdentifier2(alignment);
|
|
80200
|
+
if (identifier) {
|
|
80201
|
+
const canonical = canonicalAssessmentStandardRef2({
|
|
80202
|
+
framework: alignment.source,
|
|
80203
|
+
identifier
|
|
80204
|
+
});
|
|
80205
|
+
if (canonical.framework && canonical.identifier) {
|
|
80206
|
+
refs.set(assessmentStandardRefKey2(canonical), canonical);
|
|
80207
|
+
}
|
|
80208
|
+
}
|
|
80209
|
+
}
|
|
80210
|
+
return [...refs.values()];
|
|
80211
|
+
}
|
|
80212
|
+
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 = "", QTI_FLOAT_VALUE, PLAYABLE_POINT_VALUE, POINT_INTERACTION_TYPES2, INDEPENDENT_PROCESSING_TAGS2, PRIVATE_PLAYABLE_KEYS2, trimmedNonEmptyString2, finiteNumber22, positiveFiniteNumber2, IDENTIFIER_VALUE2, DIRECTED_PAIR_VALUE2, scoringMatchComparisonSchema2, scoringNumericEqualComparisonSchema2, scoringNumericEqualRoundedComparisonSchema2, scoringRuleSchema2, scoringRulesSchema2, scoringItemSchema2, assessmentScoringArtifactItemsSchema2, assessmentScoringArtifactSchema2, SUPPORTED_BASE_TYPES2, SUPPORTED_CARDINALITIES2, MATCH_CORRECT_TEMPLATES2, QtiScoringArtifactValidationError, UUID_PATTERN2, COMMON_CORE_MATH_FRAMEWORK_ALIASES2, COMMON_CORE_ELA_FRAMEWORK_ALIASES2, COMMON_CORE_FRAMEWORK_ALIASES2;
|
|
80182
80213
|
var init_qti = __esm(() => {
|
|
80183
80214
|
init_timeback4();
|
|
80184
80215
|
init_timeback4();
|
|
@@ -80540,6 +80571,7 @@ var init_qti = __esm(() => {
|
|
|
80540
80571
|
this.name = "QtiScoringArtifactValidationError";
|
|
80541
80572
|
}
|
|
80542
80573
|
};
|
|
80574
|
+
UUID_PATTERN2 = /^(?:urn:uuid:)?\{?[0-9a-f]{8}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{4}-[0-9a-f]{12}\}?$/i;
|
|
80543
80575
|
COMMON_CORE_MATH_FRAMEWORK_ALIASES2 = new Set([
|
|
80544
80576
|
"ccssm",
|
|
80545
80577
|
"ccssmath",
|
|
@@ -80749,7 +80781,7 @@ function pruneUnreadableStandards(standards) {
|
|
|
80749
80781
|
return standards.filter(hasReadableStandardLabel);
|
|
80750
80782
|
}
|
|
80751
80783
|
function parseQuestionIndex(title) {
|
|
80752
|
-
const match = title
|
|
80784
|
+
const match = title?.match(/\bquestion[\s_-]*(\d+)\b/i);
|
|
80753
80785
|
const index = match ? Number(match[1]) : Number.NaN;
|
|
80754
80786
|
return Number.isFinite(index) && index > 0 ? index : null;
|
|
80755
80787
|
}
|
|
@@ -80783,14 +80815,15 @@ function qtiQuestionsByIndex(qtiQuestions) {
|
|
|
80783
80815
|
}
|
|
80784
80816
|
return questionsByIndex;
|
|
80785
80817
|
}
|
|
80818
|
+
function normalizedQuestionScore(score, scorePercentile) {
|
|
80819
|
+
const rawValue = numberField(score) ?? numberField(scorePercentile);
|
|
80820
|
+
return rawValue !== null && rawValue > 1 ? rawValue / 100 : rawValue;
|
|
80821
|
+
}
|
|
80786
80822
|
function getQuestionCorrectness(result) {
|
|
80787
80823
|
if (!result) {
|
|
80788
80824
|
return "unknown";
|
|
80789
80825
|
}
|
|
80790
|
-
const
|
|
80791
|
-
const scorePercentile = numberField(result.scorePercentile);
|
|
80792
|
-
const rawValue = score ?? scorePercentile;
|
|
80793
|
-
const value = rawValue !== null && rawValue > 1 ? rawValue / 100 : rawValue;
|
|
80826
|
+
const value = normalizedQuestionScore(result.score, result.scorePercentile);
|
|
80794
80827
|
if (value === null) {
|
|
80795
80828
|
return "unknown";
|
|
80796
80829
|
}
|
|
@@ -81054,10 +81087,71 @@ var init_timeback_mastery_completion_util = __esm(async () => {
|
|
|
81054
81087
|
await init_errors8();
|
|
81055
81088
|
});
|
|
81056
81089
|
|
|
81090
|
+
// ../api-core/src/utils/bounded-map.util.ts
|
|
81091
|
+
function setBounded(map, key, value, maxSize) {
|
|
81092
|
+
map.delete(key);
|
|
81093
|
+
if (map.size >= maxSize) {
|
|
81094
|
+
const oldestKey = map.keys().next().value;
|
|
81095
|
+
if (oldestKey !== undefined) {
|
|
81096
|
+
map.delete(oldestKey);
|
|
81097
|
+
}
|
|
81098
|
+
}
|
|
81099
|
+
map.set(key, value);
|
|
81100
|
+
}
|
|
81101
|
+
|
|
81102
|
+
// ../api-core/src/utils/timeback-qti-test-lookup.util.ts
|
|
81103
|
+
async function resolveQtiTestId(client, testName, parentLineItem) {
|
|
81104
|
+
const cacheKey = `${parentLineItem.sourcedId ?? ""}:${testName}`;
|
|
81105
|
+
const cached = cache.get(cacheKey);
|
|
81106
|
+
if (cached) {
|
|
81107
|
+
setBounded(cache, cacheKey, cached, MAX_CACHE_SIZE);
|
|
81108
|
+
return cached;
|
|
81109
|
+
}
|
|
81110
|
+
let resource = null;
|
|
81111
|
+
const resourceId = sourceIdFromRef(parentLineItem.componentResource);
|
|
81112
|
+
if (resourceId) {
|
|
81113
|
+
resource = await client.api.oneroster.resources.get(resourceId).catch(() => null);
|
|
81114
|
+
}
|
|
81115
|
+
for (const candidate of qtiIdCandidates(parentLineItem, resource)) {
|
|
81116
|
+
const test = await client.qtiApi.assessmentTests.get(candidate).catch(() => null);
|
|
81117
|
+
if (test?.identifier) {
|
|
81118
|
+
setBounded(cache, cacheKey, test.identifier, MAX_CACHE_SIZE);
|
|
81119
|
+
return test.identifier;
|
|
81120
|
+
}
|
|
81121
|
+
}
|
|
81122
|
+
const normalizedTestName = testName.trim();
|
|
81123
|
+
if (!normalizedTestName) {
|
|
81124
|
+
return "";
|
|
81125
|
+
}
|
|
81126
|
+
for (let page = 1;page <= TITLE_LOOKUP_MAX_PAGES; page++) {
|
|
81127
|
+
const list = await client.qtiApi.assessmentTests.list({
|
|
81128
|
+
query: normalizedTestName,
|
|
81129
|
+
limit: TITLE_LOOKUP_LIMIT,
|
|
81130
|
+
page,
|
|
81131
|
+
sort: "title",
|
|
81132
|
+
order: "asc"
|
|
81133
|
+
}).catch(() => null);
|
|
81134
|
+
const qtiTestId = findQtiAssessmentTestIdByTitle(list?.items ?? [], normalizedTestName);
|
|
81135
|
+
if (qtiTestId) {
|
|
81136
|
+
setBounded(cache, cacheKey, qtiTestId, MAX_CACHE_SIZE);
|
|
81137
|
+
return qtiTestId;
|
|
81138
|
+
}
|
|
81139
|
+
const itemCount = list?.items?.length ?? 0;
|
|
81140
|
+
if (!list || itemCount < TITLE_LOOKUP_LIMIT || typeof list.pages === "number" && page >= list.pages) {
|
|
81141
|
+
break;
|
|
81142
|
+
}
|
|
81143
|
+
}
|
|
81144
|
+
return "";
|
|
81145
|
+
}
|
|
81146
|
+
var MAX_CACHE_SIZE = 500, TITLE_LOOKUP_LIMIT = 50, TITLE_LOOKUP_MAX_PAGES = 5, cache;
|
|
81147
|
+
var init_timeback_qti_test_lookup_util = __esm(() => {
|
|
81148
|
+
init_timeback_grade_level_results_util();
|
|
81149
|
+
cache = new Map;
|
|
81150
|
+
});
|
|
81151
|
+
|
|
81057
81152
|
// ../api-core/src/services/timeback-admin.service.ts
|
|
81058
81153
|
class TimebackAdminService {
|
|
81059
81154
|
deps;
|
|
81060
|
-
qtiTestIdCache = new Map;
|
|
81061
81155
|
static XP_PRECISION_FACTOR = 10;
|
|
81062
81156
|
static RECENT_ACTIVITY_LIMIT = 20;
|
|
81063
81157
|
static MAX_STUDENT_ACTIVITY_LIMIT = 200;
|
|
@@ -81072,9 +81166,6 @@ class TimebackAdminService {
|
|
|
81072
81166
|
static GAME_METRICS_FETCH_TIMEOUT_MS = 1e4;
|
|
81073
81167
|
static GAME_METRICS_LIST_FETCH_TIMEOUT_MS = 3000;
|
|
81074
81168
|
static GAME_METRICS_RUN_IDS_PER_REQUEST = 50;
|
|
81075
|
-
static MAX_QTI_TEST_ID_CACHE_SIZE = 500;
|
|
81076
|
-
static QTI_TEST_TITLE_LOOKUP_LIMIT = 50;
|
|
81077
|
-
static QTI_TEST_TITLE_LOOKUP_MAX_PAGES = 5;
|
|
81078
81169
|
constructor(deps) {
|
|
81079
81170
|
this.deps = deps;
|
|
81080
81171
|
}
|
|
@@ -81934,68 +82025,6 @@ class TimebackAdminService {
|
|
|
81934
82025
|
return { activities: activitiesWithGameMetrics, hasMore };
|
|
81935
82026
|
});
|
|
81936
82027
|
}
|
|
81937
|
-
rememberQtiTestId(cacheKey, qtiTestId) {
|
|
81938
|
-
if (!qtiTestId) {
|
|
81939
|
-
this.qtiTestIdCache.delete(cacheKey);
|
|
81940
|
-
return;
|
|
81941
|
-
}
|
|
81942
|
-
if (this.qtiTestIdCache.has(cacheKey)) {
|
|
81943
|
-
this.qtiTestIdCache.delete(cacheKey);
|
|
81944
|
-
} else if (this.qtiTestIdCache.size >= TimebackAdminService.MAX_QTI_TEST_ID_CACHE_SIZE) {
|
|
81945
|
-
const oldestKey = this.qtiTestIdCache.keys().next().value;
|
|
81946
|
-
if (oldestKey !== undefined) {
|
|
81947
|
-
this.qtiTestIdCache.delete(oldestKey);
|
|
81948
|
-
}
|
|
81949
|
-
}
|
|
81950
|
-
this.qtiTestIdCache.set(cacheKey, qtiTestId);
|
|
81951
|
-
}
|
|
81952
|
-
async resolveQtiTestIdForGradeLevelResult(client, testName, parentLineItem) {
|
|
81953
|
-
const cacheKey = `${parentLineItem.sourcedId ?? ""}:${testName}`;
|
|
81954
|
-
const cachedQtiTestId = cacheKey ? this.qtiTestIdCache.get(cacheKey) : undefined;
|
|
81955
|
-
if (cachedQtiTestId) {
|
|
81956
|
-
this.rememberQtiTestId(cacheKey, cachedQtiTestId);
|
|
81957
|
-
return cachedQtiTestId;
|
|
81958
|
-
}
|
|
81959
|
-
let resource = null;
|
|
81960
|
-
const resourceId = sourceIdFromRef(parentLineItem.componentResource);
|
|
81961
|
-
if (resourceId) {
|
|
81962
|
-
resource = await client.api.oneroster.resources.get(resourceId).catch(() => null);
|
|
81963
|
-
}
|
|
81964
|
-
for (const candidate of qtiIdCandidates(parentLineItem, resource)) {
|
|
81965
|
-
const test = await client.qtiApi.assessmentTests.get(candidate).catch(() => null);
|
|
81966
|
-
if (test?.identifier) {
|
|
81967
|
-
if (cacheKey) {
|
|
81968
|
-
this.rememberQtiTestId(cacheKey, test.identifier);
|
|
81969
|
-
}
|
|
81970
|
-
return test.identifier;
|
|
81971
|
-
}
|
|
81972
|
-
}
|
|
81973
|
-
const normalizedTestName = testName.trim();
|
|
81974
|
-
if (!normalizedTestName) {
|
|
81975
|
-
return "";
|
|
81976
|
-
}
|
|
81977
|
-
for (let page = 1;page <= TimebackAdminService.QTI_TEST_TITLE_LOOKUP_MAX_PAGES; page++) {
|
|
81978
|
-
const list = await client.qtiApi.assessmentTests.list({
|
|
81979
|
-
query: normalizedTestName,
|
|
81980
|
-
limit: TimebackAdminService.QTI_TEST_TITLE_LOOKUP_LIMIT,
|
|
81981
|
-
page,
|
|
81982
|
-
sort: "title",
|
|
81983
|
-
order: "asc"
|
|
81984
|
-
}).catch(() => null);
|
|
81985
|
-
const qtiTestId = findQtiAssessmentTestIdByTitle(list?.items ?? [], normalizedTestName);
|
|
81986
|
-
if (qtiTestId) {
|
|
81987
|
-
if (cacheKey) {
|
|
81988
|
-
this.rememberQtiTestId(cacheKey, qtiTestId);
|
|
81989
|
-
}
|
|
81990
|
-
return qtiTestId;
|
|
81991
|
-
}
|
|
81992
|
-
const itemCount = list?.items?.length ?? 0;
|
|
81993
|
-
if (!list || itemCount < TimebackAdminService.QTI_TEST_TITLE_LOOKUP_LIMIT || typeof list.pages === "number" && page >= list.pages) {
|
|
81994
|
-
break;
|
|
81995
|
-
}
|
|
81996
|
-
}
|
|
81997
|
-
return "";
|
|
81998
|
-
}
|
|
81999
82028
|
async getGradeLevelTestResults(user, options) {
|
|
82000
82029
|
return this.withClientTelemetry(async () => {
|
|
82001
82030
|
const { gameId, courseId, studentId } = options;
|
|
@@ -82098,7 +82127,7 @@ class TimebackAdminService {
|
|
|
82098
82127
|
}).then((page) => page.data) : [];
|
|
82099
82128
|
const latestResultsByLineItem = latestAssessmentResultsByLineItem(questionResults);
|
|
82100
82129
|
const testName = resolveGradeLevelTestName(parentResult, parentLineItem);
|
|
82101
|
-
const qtiTestId = await
|
|
82130
|
+
const qtiTestId = await resolveQtiTestId(client, testName, parentLineItem);
|
|
82102
82131
|
const qtiQuestions = qtiTestId ? await client.qtiApi.assessmentTests.getQuestions(qtiTestId).then((response) => response.questions ?? []).catch(() => []) : [];
|
|
82103
82132
|
const questions = mapGradeLevelReviewQuestions({
|
|
82104
82133
|
questionLineItems,
|
|
@@ -82792,6 +82821,7 @@ var init_timeback_admin_service = __esm(async () => {
|
|
|
82792
82821
|
init_timeback_discrepancy_queue_util();
|
|
82793
82822
|
init_timeback_game_metrics_comparison_util();
|
|
82794
82823
|
init_timeback_grade_level_results_util();
|
|
82824
|
+
init_timeback_qti_test_lookup_util();
|
|
82795
82825
|
init_timeback_util();
|
|
82796
82826
|
await __promiseAll([
|
|
82797
82827
|
init_dist5(),
|
|
@@ -90690,6 +90720,22 @@ var init_timeback_assessments_service = __esm(async () => {
|
|
|
90690
90720
|
await init_errors8();
|
|
90691
90721
|
});
|
|
90692
90722
|
|
|
90723
|
+
// ../api-core/src/utils/timeback-attribution.util.ts
|
|
90724
|
+
function resolveRuntimeAttribution(base, holeFilling, requestedCourseId, recordedCourseId) {
|
|
90725
|
+
const recorded = [base, ...holeFilling].find((integration) => integration.courseId === recordedCourseId);
|
|
90726
|
+
if (recorded) {
|
|
90727
|
+
return { integration: recorded, requestedCourseIdStatus: "pinned_by_run" };
|
|
90728
|
+
}
|
|
90729
|
+
if (requestedCourseId === undefined) {
|
|
90730
|
+
return { integration: base, requestedCourseIdStatus: "absent" };
|
|
90731
|
+
}
|
|
90732
|
+
if (requestedCourseId === base.courseId) {
|
|
90733
|
+
return { integration: base, requestedCourseIdStatus: "accepted" };
|
|
90734
|
+
}
|
|
90735
|
+
const requestedHoleFilling = holeFilling.find((integration) => integration.courseId === requestedCourseId);
|
|
90736
|
+
return requestedHoleFilling ? { integration: requestedHoleFilling, requestedCourseIdStatus: "accepted_hole_filling" } : { integration: base, requestedCourseIdStatus: "ignored_mismatch" };
|
|
90737
|
+
}
|
|
90738
|
+
|
|
90693
90739
|
// ../api-core/src/utils/timeback-create-integration.util.ts
|
|
90694
90740
|
function buildTimebackBaseConfigFromExistingConfig(config) {
|
|
90695
90741
|
return {
|
|
@@ -90782,6 +90828,128 @@ var init_timeback_create_integration_util = __esm(() => {
|
|
|
90782
90828
|
init_timeback_util();
|
|
90783
90829
|
});
|
|
90784
90830
|
|
|
90831
|
+
// ../api-core/src/utils/timeback-hole-filling-context.util.ts
|
|
90832
|
+
function buildAssignmentResultWhere(options) {
|
|
90833
|
+
return {
|
|
90834
|
+
status: "active",
|
|
90835
|
+
"student.sourcedId": options.studentId,
|
|
90836
|
+
"metadata.resultType": options.resultType,
|
|
90837
|
+
"metadata.assignmentId": options.assignmentId
|
|
90838
|
+
};
|
|
90839
|
+
}
|
|
90840
|
+
function assignmentIdOf(record) {
|
|
90841
|
+
const value = metadataOf(record).assignmentId;
|
|
90842
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
90843
|
+
return String(value);
|
|
90844
|
+
}
|
|
90845
|
+
const text = stringField3(value).trim();
|
|
90846
|
+
return text || undefined;
|
|
90847
|
+
}
|
|
90848
|
+
function toTriggeringResult(result) {
|
|
90849
|
+
const metadata2 = metadataOf(result);
|
|
90850
|
+
const testType = normalizeTestType(metadata2.testType);
|
|
90851
|
+
const assignmentId = assignmentIdOf(result);
|
|
90852
|
+
const lineItemId = sourceIdFromRef(result.assessmentLineItem);
|
|
90853
|
+
if (!assignmentId || !lineItemId || result.scoreStatus !== SCORE_STATUS2.fullyGraded || !isAllowedGradeLevelTestType(testType)) {
|
|
90854
|
+
return null;
|
|
90855
|
+
}
|
|
90856
|
+
return {
|
|
90857
|
+
resultId: result.sourcedId ?? "",
|
|
90858
|
+
assignmentId,
|
|
90859
|
+
lineItemId,
|
|
90860
|
+
testName: stringField3(metadata2.testName) || lineItemId,
|
|
90861
|
+
testType,
|
|
90862
|
+
score: numberField(result.score),
|
|
90863
|
+
scoreDate: result.scoreDate ?? null,
|
|
90864
|
+
totalQuestions: numberField(metadata2.totalQuestions),
|
|
90865
|
+
correctQuestions: numberField(metadata2.correctQuestions)
|
|
90866
|
+
};
|
|
90867
|
+
}
|
|
90868
|
+
function isMissed(resultText, score, scorePercentile) {
|
|
90869
|
+
const marking = stringField3(resultText).trim().toLowerCase();
|
|
90870
|
+
if (marking) {
|
|
90871
|
+
return marking !== "correct";
|
|
90872
|
+
}
|
|
90873
|
+
const value = normalizedQuestionScore(score, scorePercentile);
|
|
90874
|
+
return value !== null && value < 1;
|
|
90875
|
+
}
|
|
90876
|
+
function missedQuestionsFromInlineResponses(result) {
|
|
90877
|
+
const responses = metadataOf(result).responses;
|
|
90878
|
+
const questions = isRecord6(responses) && Array.isArray(responses.questions) ? responses.questions : undefined;
|
|
90879
|
+
if (!questions?.length) {
|
|
90880
|
+
return;
|
|
90881
|
+
}
|
|
90882
|
+
return questions.flatMap((question) => {
|
|
90883
|
+
if (!isRecord6(question) || !isMissed(question.result, question.score)) {
|
|
90884
|
+
return [];
|
|
90885
|
+
}
|
|
90886
|
+
const questionNumber = numberField(question.questionNumber);
|
|
90887
|
+
const qtiItemId = stringField3(question.questionQtiIdentifier).trim();
|
|
90888
|
+
return questionNumber === null ? [] : [{ questionNumber, ...qtiItemId && { qtiItemId } }];
|
|
90889
|
+
});
|
|
90890
|
+
}
|
|
90891
|
+
function questionNumberFromLineItemId(lineItemId) {
|
|
90892
|
+
const match = lineItemId.match(/_q(\d+)$/);
|
|
90893
|
+
const value = match ? Number(match[1]) : Number.NaN;
|
|
90894
|
+
return Number.isInteger(value) && value > 0 ? value : null;
|
|
90895
|
+
}
|
|
90896
|
+
function missedQuestionsFromQuestionResults(questionResults) {
|
|
90897
|
+
return [...latestAssessmentResultsByLineItem(questionResults)].flatMap(([lineItemId, result]) => {
|
|
90898
|
+
const questionNumber = questionNumberFromLineItemId(lineItemId);
|
|
90899
|
+
const responses = metadataOf(result).responses;
|
|
90900
|
+
const response = isRecord6(responses) && isRecord6(responses.response) ? responses.response : undefined;
|
|
90901
|
+
if (questionNumber === null || !isMissed(response?.result, result.score, result.scorePercentile)) {
|
|
90902
|
+
return [];
|
|
90903
|
+
}
|
|
90904
|
+
const qtiItemId = stringField3(response?.questionQtiIdentifier).trim();
|
|
90905
|
+
return [{ questionNumber, ...qtiItemId && { qtiItemId } }];
|
|
90906
|
+
}).toSorted((a, b) => a.questionNumber - b.questionNumber);
|
|
90907
|
+
}
|
|
90908
|
+
function qtiItemIdForQuestionNumber(qtiQuestions) {
|
|
90909
|
+
const byNumber = qtiQuestionsByIndex(qtiQuestions);
|
|
90910
|
+
return (questionNumber) => {
|
|
90911
|
+
const question = byNumber.get(questionNumber) ?? qtiQuestions[questionNumber - 1];
|
|
90912
|
+
return question ? question.question.identifier || question.reference.identifier : undefined;
|
|
90913
|
+
};
|
|
90914
|
+
}
|
|
90915
|
+
function groupMissedStandards(missed, standardsByItemId, itemIdFor) {
|
|
90916
|
+
const byKey = new Map;
|
|
90917
|
+
const untagged = [];
|
|
90918
|
+
for (const question of missed) {
|
|
90919
|
+
const itemId = question.qtiItemId ?? itemIdFor(question.questionNumber);
|
|
90920
|
+
const standards = itemId ? standardsByItemId.get(itemId) ?? [] : [];
|
|
90921
|
+
if (!standards.length) {
|
|
90922
|
+
untagged.push(question.questionNumber);
|
|
90923
|
+
}
|
|
90924
|
+
for (const standard of standards) {
|
|
90925
|
+
const key = `${standard.framework}\x00${standard.identifier}`;
|
|
90926
|
+
const entry = byKey.get(key) ?? {
|
|
90927
|
+
framework: standard.framework,
|
|
90928
|
+
code: standard.identifier,
|
|
90929
|
+
questionNumbers: [],
|
|
90930
|
+
qtiItemIds: []
|
|
90931
|
+
};
|
|
90932
|
+
entry.questionNumbers.push(question.questionNumber);
|
|
90933
|
+
if (itemId && !entry.qtiItemIds.includes(itemId)) {
|
|
90934
|
+
entry.qtiItemIds.push(itemId);
|
|
90935
|
+
}
|
|
90936
|
+
byKey.set(key, entry);
|
|
90937
|
+
}
|
|
90938
|
+
}
|
|
90939
|
+
return {
|
|
90940
|
+
missedStandards: [...byKey.values()].toSorted((a, b) => b.questionNumbers.length - a.questionNumbers.length || a.code.localeCompare(b.code, undefined, { numeric: true })),
|
|
90941
|
+
untaggedQuestionNumbers: untagged.toSorted((a, b) => a - b)
|
|
90942
|
+
};
|
|
90943
|
+
}
|
|
90944
|
+
function standardsFromQtiItem(item) {
|
|
90945
|
+
return qtiAssessmentStandardRefsFromMetadata2(metadataOf(item));
|
|
90946
|
+
}
|
|
90947
|
+
var init_timeback_hole_filling_context_util = __esm(() => {
|
|
90948
|
+
init_constants2();
|
|
90949
|
+
init_qti();
|
|
90950
|
+
init_timeback_grade_level_results_util();
|
|
90951
|
+
});
|
|
90952
|
+
|
|
90785
90953
|
// ../api-core/src/utils/timeback-promotion.util.ts
|
|
90786
90954
|
async function promoteCompletedCourse({
|
|
90787
90955
|
db,
|
|
@@ -90911,14 +91079,20 @@ var init_timeback_service = __esm(async () => {
|
|
|
90911
91079
|
init_helpers_index();
|
|
90912
91080
|
init_tables_index();
|
|
90913
91081
|
init_spans();
|
|
91082
|
+
init_constants2();
|
|
90914
91083
|
init_types2();
|
|
90915
91084
|
init_errors2();
|
|
90916
91085
|
init_timeback_create_integration_util();
|
|
91086
|
+
init_timeback_grade_level_results_util();
|
|
91087
|
+
init_timeback_hole_filling_context_util();
|
|
90917
91088
|
init_timeback_promotion_util();
|
|
91089
|
+
init_timeback_qti_test_lookup_util();
|
|
90918
91090
|
init_timeback_removed_integration_util();
|
|
90919
91091
|
init_timeback_util();
|
|
90920
91092
|
await init_dist5();
|
|
90921
91093
|
TimebackService = class TimebackService {
|
|
91094
|
+
static HOLE_FILLING_CONTEXT_TTL_MS = 10 * 60 * 1000;
|
|
91095
|
+
static HOLE_FILLING_CONTEXT_CACHE_SIZE = 1000;
|
|
90922
91096
|
static HEARTBEAT_DEDUPE_TTL_MS = 5 * 60 * 1000;
|
|
90923
91097
|
static processedHeartbeatWindows = new Map;
|
|
90924
91098
|
static inFlightHeartbeatWindows = new Map;
|
|
@@ -90926,6 +91100,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
90926
91100
|
static COMPLETION_IN_FLIGHT_SETTLE_DELAY_MS = 400;
|
|
90927
91101
|
static COMPLETION_IN_FLIGHT_SETTLE_ATTEMPTS = 5;
|
|
90928
91102
|
deps;
|
|
91103
|
+
holeFillingContextCache = new Map;
|
|
90929
91104
|
static cleanHeartbeatDedupeCache(now = Date.now()) {
|
|
90930
91105
|
for (const [key, timestamp] of this.processedHeartbeatWindows) {
|
|
90931
91106
|
if (now - timestamp > this.HEARTBEAT_DEDUPE_TTL_MS) {
|
|
@@ -90949,13 +91124,13 @@ var init_timeback_service = __esm(async () => {
|
|
|
90949
91124
|
static clearInFlightHeartbeatWindow(key) {
|
|
90950
91125
|
this.inFlightHeartbeatWindows.delete(key);
|
|
90951
91126
|
}
|
|
90952
|
-
static buildCompletionCaliperEventId(key, kind, discriminator = "completion") {
|
|
91127
|
+
static buildCompletionCaliperEventId(key, kind, baseCourseId, discriminator = "completion") {
|
|
90953
91128
|
const hash = createHash3("sha256").update([
|
|
90954
91129
|
"playcademy",
|
|
90955
91130
|
"timeback",
|
|
90956
91131
|
kind,
|
|
90957
91132
|
key.gameId,
|
|
90958
|
-
|
|
91133
|
+
baseCourseId,
|
|
90959
91134
|
key.studentId,
|
|
90960
91135
|
key.runId,
|
|
90961
91136
|
key.activityId,
|
|
@@ -91021,11 +91196,14 @@ var init_timeback_service = __esm(async () => {
|
|
|
91021
91196
|
} : null
|
|
91022
91197
|
};
|
|
91023
91198
|
}
|
|
91024
|
-
static async acquireCompletionReservation(db, key) {
|
|
91025
|
-
|
|
91199
|
+
static async acquireCompletionReservation(db, key, otherCourseIds = []) {
|
|
91200
|
+
function reserve() {
|
|
91201
|
+
return otherCourseIds.length > 0 ? TimebackService.reserveCompletionAcrossCourses(db, key, otherCourseIds) : TimebackService.reserveCompletion(db, key);
|
|
91202
|
+
}
|
|
91203
|
+
let reserveOutcome = await reserve();
|
|
91026
91204
|
for (let settleAttempt = 0;reserveOutcome.status === "in_flight" && settleAttempt < this.COMPLETION_IN_FLIGHT_SETTLE_ATTEMPTS; settleAttempt++) {
|
|
91027
91205
|
await sleep(this.COMPLETION_IN_FLIGHT_SETTLE_DELAY_MS);
|
|
91028
|
-
reserveOutcome = await
|
|
91206
|
+
reserveOutcome = await reserve();
|
|
91029
91207
|
}
|
|
91030
91208
|
if (reserveOutcome.status === "duplicate") {
|
|
91031
91209
|
setAttribute("app.timeback.end_activity_status", "blocked_by_dedupe_guard");
|
|
@@ -91044,8 +91222,24 @@ var init_timeback_service = __esm(async () => {
|
|
|
91044
91222
|
}
|
|
91045
91223
|
return { status: "reserved", token: reserveOutcome.token };
|
|
91046
91224
|
}
|
|
91047
|
-
static async
|
|
91048
|
-
|
|
91225
|
+
static async reserveCompletionAcrossCourses(db, key, otherCourseIds) {
|
|
91226
|
+
return db.transaction(async (tx) => {
|
|
91227
|
+
const database = tx;
|
|
91228
|
+
const runKey = `timeback-completion:${key.gameId.toLowerCase()}:${key.studentId}:${key.runId.toLowerCase()}:${key.activityId}`;
|
|
91229
|
+
await database.execute(sql`SELECT pg_advisory_xact_lock(hashtextextended(${runKey}, 0))`);
|
|
91230
|
+
const onOtherCourse = await database.query.gameTimebackActivityCompletions.findFirst({
|
|
91231
|
+
columns: { id: true },
|
|
91232
|
+
where: and(eq(gameTimebackActivityCompletions.gameId, key.gameId), eq(gameTimebackActivityCompletions.studentId, key.studentId), eq(gameTimebackActivityCompletions.runId, key.runId), eq(gameTimebackActivityCompletions.activityId, key.activityId), inArray(gameTimebackActivityCompletions.courseId, [...otherCourseIds]))
|
|
91233
|
+
});
|
|
91234
|
+
if (onOtherCourse) {
|
|
91235
|
+
setAttribute("app.timeback.end_activity_status", "completion_reserved_on_other_course");
|
|
91236
|
+
throw new ServiceUnavailableError("An end-activity submission for this run is already in flight — retry shortly");
|
|
91237
|
+
}
|
|
91238
|
+
return this.reserveCompletion(database, key);
|
|
91239
|
+
});
|
|
91240
|
+
}
|
|
91241
|
+
static async runWithCompletionReservation(db, key, emit, awardFromResult, otherCourseIds = []) {
|
|
91242
|
+
const acquired = await this.acquireCompletionReservation(db, key, otherCourseIds);
|
|
91049
91243
|
if (acquired.status === "duplicate") {
|
|
91050
91244
|
return acquired;
|
|
91051
91245
|
}
|
|
@@ -91080,6 +91274,9 @@ var init_timeback_service = __esm(async () => {
|
|
|
91080
91274
|
await db.delete(gameTimebackActivityCompletions).where(and(eq(gameTimebackActivityCompletions.id, token.id), eq(gameTimebackActivityCompletions.reservedAt, token.reservedAt), isNull(gameTimebackActivityCompletions.completedAt)));
|
|
91081
91275
|
}
|
|
91082
91276
|
static addResumeIdToExtensions(extensions, resumeId) {
|
|
91277
|
+
return TimebackService.addPlaycademyExtensions(extensions, { resumeId });
|
|
91278
|
+
}
|
|
91279
|
+
static addPlaycademyExtensions(extensions, fields) {
|
|
91083
91280
|
const base = extensions ?? {};
|
|
91084
91281
|
const existingPlaycademy = base.playcademy;
|
|
91085
91282
|
const playcademy = typeof existingPlaycademy === "object" && existingPlaycademy !== null && !Array.isArray(existingPlaycademy) ? existingPlaycademy : {};
|
|
@@ -91087,28 +91284,71 @@ var init_timeback_service = __esm(async () => {
|
|
|
91087
91284
|
...base,
|
|
91088
91285
|
playcademy: {
|
|
91089
91286
|
...playcademy,
|
|
91090
|
-
|
|
91287
|
+
...fields
|
|
91091
91288
|
}
|
|
91092
91289
|
};
|
|
91093
91290
|
}
|
|
91094
|
-
|
|
91095
|
-
|
|
91096
|
-
|
|
91097
|
-
|
|
91098
|
-
|
|
91099
|
-
|
|
91291
|
+
async resolveRuntimeCourse({
|
|
91292
|
+
gameId,
|
|
91293
|
+
studentId,
|
|
91294
|
+
runId,
|
|
91295
|
+
activityData,
|
|
91296
|
+
extensions
|
|
91297
|
+
}) {
|
|
91298
|
+
const integrations = await this.deps.db.query.gameTimebackIntegrations.findMany({
|
|
91299
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, activityData.grade), eq(gameTimebackIntegrations.subject, activityData.subject), isActiveGameTimebackIntegrationStatus())
|
|
91300
|
+
});
|
|
91301
|
+
const base = integrations.find(isBaseGameTimebackIntegrationRow);
|
|
91302
|
+
if (!base) {
|
|
91303
|
+
throw new NotFoundError(`Timeback integration for game (grade ${activityData.grade}, subject ${activityData.subject})`);
|
|
91100
91304
|
}
|
|
91305
|
+
const holeFilling = integrations.filter((integration) => !isBaseGameTimebackIntegrationRow(integration));
|
|
91306
|
+
const recordedCourseId = holeFilling.length > 0 && runId ? await this.findRecordedRunCourse({
|
|
91307
|
+
gameId,
|
|
91308
|
+
studentId,
|
|
91309
|
+
runId,
|
|
91310
|
+
activityId: activityData.activityId,
|
|
91311
|
+
courseIds: integrations.map((i2) => i2.courseId)
|
|
91312
|
+
}) : undefined;
|
|
91313
|
+
const { integration, requestedCourseIdStatus } = resolveRuntimeAttribution(base, holeFilling, activityData.courseId, recordedCourseId);
|
|
91314
|
+
const hintUsed = activityData.courseId === integration.courseId;
|
|
91315
|
+
const enrollmentId = hintUsed ? activityData.enrollmentId : undefined;
|
|
91316
|
+
const isHoleFilling = !isBaseGameTimebackIntegrationRow(integration);
|
|
91101
91317
|
return {
|
|
91102
|
-
|
|
91103
|
-
|
|
91318
|
+
integration,
|
|
91319
|
+
courseId: integration.courseId,
|
|
91320
|
+
baseCourseId: base.courseId,
|
|
91321
|
+
otherCourseIds: holeFilling.length > 0 ? integrations.map((other) => other.courseId).filter((courseId) => courseId !== integration.courseId) : [],
|
|
91322
|
+
courseName: activityData.courseId === undefined || hintUsed ? activityData.courseName : undefined,
|
|
91323
|
+
extensions: isHoleFilling ? TimebackService.addPlaycademyExtensions(extensions, {
|
|
91324
|
+
courseKind: HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND,
|
|
91325
|
+
baseCourseId: base.courseId,
|
|
91326
|
+
...enrollmentId && { enrollmentId }
|
|
91327
|
+
}) : extensions,
|
|
91104
91328
|
attributes: {
|
|
91105
|
-
"app.timeback.course_id":
|
|
91106
|
-
"app.timeback.resolved_course_id":
|
|
91107
|
-
"app.timeback.requested_course_id":
|
|
91108
|
-
"app.timeback.requested_course_id_status": requestedCourseIdStatus
|
|
91329
|
+
"app.timeback.course_id": integration.courseId,
|
|
91330
|
+
"app.timeback.resolved_course_id": integration.courseId,
|
|
91331
|
+
"app.timeback.requested_course_id": activityData.courseId,
|
|
91332
|
+
"app.timeback.requested_course_id_status": requestedCourseIdStatus,
|
|
91333
|
+
"app.timeback.course_kind": integration.kind,
|
|
91334
|
+
"app.timeback.enrollment_id": enrollmentId
|
|
91109
91335
|
}
|
|
91110
91336
|
};
|
|
91111
91337
|
}
|
|
91338
|
+
async findRecordedRunCourse({
|
|
91339
|
+
gameId,
|
|
91340
|
+
studentId,
|
|
91341
|
+
runId,
|
|
91342
|
+
activityId,
|
|
91343
|
+
courseIds
|
|
91344
|
+
}) {
|
|
91345
|
+
const recorded = await this.deps.db.query.gameTimebackActivityCompletions.findFirst({
|
|
91346
|
+
columns: { courseId: true },
|
|
91347
|
+
where: and(eq(gameTimebackActivityCompletions.gameId, gameId), eq(gameTimebackActivityCompletions.studentId, studentId), eq(gameTimebackActivityCompletions.runId, runId), eq(gameTimebackActivityCompletions.activityId, activityId), inArray(gameTimebackActivityCompletions.courseId, courseIds)),
|
|
91348
|
+
orderBy: (table, { asc }) => [asc(table.createdAt)]
|
|
91349
|
+
});
|
|
91350
|
+
return recorded?.courseId;
|
|
91351
|
+
}
|
|
91112
91352
|
static recordRuntimeContext({
|
|
91113
91353
|
operation,
|
|
91114
91354
|
studentId,
|
|
@@ -91234,19 +91474,163 @@ var init_timeback_service = __esm(async () => {
|
|
|
91234
91474
|
}
|
|
91235
91475
|
async getUserEnrollments(userId, gameId) {
|
|
91236
91476
|
return this.withClientTelemetry(async () => {
|
|
91237
|
-
const
|
|
91238
|
-
|
|
91239
|
-
|
|
91240
|
-
|
|
91241
|
-
|
|
91242
|
-
|
|
91477
|
+
const enrollments = await this.fetchEnrollments(await this.requireTimebackId(userId));
|
|
91478
|
+
return gameId ? filterEnrollmentsByGame(enrollments, gameId) : enrollments;
|
|
91479
|
+
});
|
|
91480
|
+
}
|
|
91481
|
+
async requireTimebackId(userId) {
|
|
91482
|
+
const userData = await this.deps.db.query.users.findFirst({
|
|
91483
|
+
where: eq(users.id, userId),
|
|
91484
|
+
columns: { id: true, timebackId: true }
|
|
91485
|
+
});
|
|
91486
|
+
if (!userData) {
|
|
91487
|
+
throw new NotFoundError("User", userId);
|
|
91488
|
+
}
|
|
91489
|
+
if (!userData.timebackId) {
|
|
91490
|
+
throw new NotFoundError("Timeback account not found for user");
|
|
91491
|
+
}
|
|
91492
|
+
return userData.timebackId;
|
|
91493
|
+
}
|
|
91494
|
+
async getHoleFillingContext(userId, gameId) {
|
|
91495
|
+
return this.withClientTelemetry(async () => {
|
|
91496
|
+
setAttribute("app.timeback.operation", "get_hole_filling_context");
|
|
91497
|
+
const studentId = await this.requireTimebackId(userId);
|
|
91498
|
+
const enrollments = filterEnrollmentsByGame(await this.fetchEnrollments(studentId), gameId);
|
|
91499
|
+
const holeFilling = enrollments.filter((enrollment) => enrollment.kind === HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND);
|
|
91500
|
+
setAttribute("app.timeback.hole_filling_enrollment_count", holeFilling.length);
|
|
91501
|
+
if (holeFilling.length === 0) {
|
|
91502
|
+
return { contexts: [] };
|
|
91243
91503
|
}
|
|
91244
|
-
|
|
91245
|
-
|
|
91504
|
+
const contexts = await Promise.all(holeFilling.map((enrollment) => this.buildHoleFillingContext(studentId, enrollment)));
|
|
91505
|
+
return { contexts };
|
|
91506
|
+
});
|
|
91507
|
+
}
|
|
91508
|
+
async buildHoleFillingContext(studentId, enrollment) {
|
|
91509
|
+
const client = this.requireClient();
|
|
91510
|
+
const grade = enrollment.grade;
|
|
91511
|
+
const subject = enrollment.subject;
|
|
91512
|
+
const enrollmentContext = {
|
|
91513
|
+
courseId: enrollment.courseId,
|
|
91514
|
+
baseCourseId: enrollment.baseCourseId ?? "",
|
|
91515
|
+
...enrollment.id && { enrollmentId: enrollment.id },
|
|
91516
|
+
grade,
|
|
91517
|
+
subject,
|
|
91518
|
+
beginDate: enrollment.beginDate ?? null
|
|
91519
|
+
};
|
|
91520
|
+
const stampedAssignmentId = assignmentIdOf({ metadata: enrollment.enrollmentMetadata });
|
|
91521
|
+
let parent = null;
|
|
91522
|
+
if (stampedAssignmentId) {
|
|
91523
|
+
const where = {
|
|
91524
|
+
...buildAssignmentResultWhere({
|
|
91525
|
+
studentId,
|
|
91526
|
+
assignmentId: stampedAssignmentId,
|
|
91527
|
+
resultType: "assessment"
|
|
91528
|
+
}),
|
|
91529
|
+
scoreStatus: SCORE_STATUS2.fullyGraded
|
|
91530
|
+
};
|
|
91531
|
+
const page = await client.api.oneroster.assessmentResults.list({
|
|
91532
|
+
limit: 1,
|
|
91533
|
+
where,
|
|
91534
|
+
sort: "scoreDate",
|
|
91535
|
+
orderBy: "desc"
|
|
91536
|
+
});
|
|
91537
|
+
parent = page.data[0] ?? null;
|
|
91538
|
+
}
|
|
91539
|
+
const summary = parent ? toTriggeringResult(parent) : null;
|
|
91540
|
+
let trigger = "not_stamped";
|
|
91541
|
+
if (stampedAssignmentId) {
|
|
91542
|
+
trigger = summary ? "found" : "not_found";
|
|
91543
|
+
}
|
|
91544
|
+
setAttributes({
|
|
91545
|
+
"app.timeback.hole_filling_trigger": trigger,
|
|
91546
|
+
"app.timeback.hole_filling_assignment_id": stampedAssignmentId
|
|
91547
|
+
});
|
|
91548
|
+
if (!parent || !summary) {
|
|
91549
|
+
return {
|
|
91550
|
+
enrollment: enrollmentContext,
|
|
91551
|
+
triggeringResult: null,
|
|
91552
|
+
missedStandards: [],
|
|
91553
|
+
untaggedMissedQuestionNumbers: []
|
|
91554
|
+
};
|
|
91555
|
+
}
|
|
91556
|
+
const cacheKey = `${enrollment.courseId}:${studentId}:${summary.resultId}`;
|
|
91557
|
+
const cached = this.holeFillingContextCache.get(cacheKey);
|
|
91558
|
+
if (cached && Date.now() - cached.at < TimebackService.HOLE_FILLING_CONTEXT_TTL_MS) {
|
|
91559
|
+
return { ...cached.context, enrollment: enrollmentContext };
|
|
91560
|
+
}
|
|
91561
|
+
const { context, complete } = await this.resolveMissedStandards(studentId, parent, summary, enrollmentContext);
|
|
91562
|
+
setAttribute("app.timeback.hole_filling_standards_complete", complete);
|
|
91563
|
+
if (complete) {
|
|
91564
|
+
setBounded(this.holeFillingContextCache, cacheKey, { at: Date.now(), context }, TimebackService.HOLE_FILLING_CONTEXT_CACHE_SIZE);
|
|
91565
|
+
}
|
|
91566
|
+
return context;
|
|
91567
|
+
}
|
|
91568
|
+
async resolveMissedStandards(studentId, parent, summary, enrollmentContext) {
|
|
91569
|
+
const client = this.requireClient();
|
|
91570
|
+
let complete = true;
|
|
91571
|
+
let missed = missedQuestionsFromInlineResponses(parent);
|
|
91572
|
+
if (!missed) {
|
|
91573
|
+
const where = buildAssignmentResultWhere({
|
|
91574
|
+
studentId,
|
|
91575
|
+
assignmentId: summary.assignmentId,
|
|
91576
|
+
resultType: "question"
|
|
91577
|
+
});
|
|
91578
|
+
const questionResults = await client.api.oneroster.assessmentResults.listAll({
|
|
91579
|
+
limit: 200,
|
|
91580
|
+
where,
|
|
91581
|
+
sort: "scoreDate",
|
|
91582
|
+
orderBy: "desc"
|
|
91583
|
+
});
|
|
91584
|
+
missed = missedQuestionsFromQuestionResults(questionResults);
|
|
91585
|
+
}
|
|
91586
|
+
let qtiTestId = "";
|
|
91587
|
+
let qtiQuestions = [];
|
|
91588
|
+
const standardsByItemId = new Map;
|
|
91589
|
+
if (missed.length > 0 && !client.hasQti) {
|
|
91590
|
+
complete = false;
|
|
91591
|
+
missed = missed.map(({ questionNumber }) => ({ questionNumber }));
|
|
91592
|
+
} else if (missed.some((question) => !question.qtiItemId)) {
|
|
91593
|
+
const lineItem = await client.api.oneroster.assessmentLineItems.get(summary.lineItemId).catch(() => null);
|
|
91594
|
+
qtiTestId = lineItem ? await resolveQtiTestId(client, resolveGradeLevelTestName(parent, lineItem), lineItem) : "";
|
|
91595
|
+
complete &&= Boolean(qtiTestId);
|
|
91596
|
+
if (qtiTestId) {
|
|
91597
|
+
qtiQuestions = await client.qtiApi.assessmentTests.getQuestions(qtiTestId).then((response) => response.questions ?? []).catch(() => {
|
|
91598
|
+
complete = false;
|
|
91599
|
+
return [];
|
|
91600
|
+
});
|
|
91601
|
+
for (const question of qtiQuestions) {
|
|
91602
|
+
const id = question.question.identifier || question.reference.identifier;
|
|
91603
|
+
if (id) {
|
|
91604
|
+
standardsByItemId.set(id, standardsFromQtiItem(question.question));
|
|
91605
|
+
}
|
|
91606
|
+
}
|
|
91246
91607
|
}
|
|
91247
|
-
|
|
91248
|
-
|
|
91608
|
+
}
|
|
91609
|
+
const itemIdsToFetch = [
|
|
91610
|
+
...new Set(missed.flatMap((question) => question.qtiItemId && !standardsByItemId.has(question.qtiItemId) ? [question.qtiItemId] : []))
|
|
91611
|
+
];
|
|
91612
|
+
const items = await Promise.all(itemIdsToFetch.map((id) => client.qtiApi.assessmentItems.get(id).then((item) => [id, standardsFromQtiItem(item)]).catch(() => {
|
|
91613
|
+
complete = false;
|
|
91614
|
+
return [id, []];
|
|
91615
|
+
})));
|
|
91616
|
+
for (const [id, standards] of items) {
|
|
91617
|
+
standardsByItemId.set(id, standards);
|
|
91618
|
+
}
|
|
91619
|
+
const { missedStandards, untaggedQuestionNumbers } = groupMissedStandards(missed, standardsByItemId, qtiItemIdForQuestionNumber(qtiQuestions));
|
|
91620
|
+
setAttributes({
|
|
91621
|
+
"app.timeback.hole_filling_missed_question_count": missed.length,
|
|
91622
|
+
"app.timeback.hole_filling_missed_standard_count": missedStandards.length,
|
|
91623
|
+
"app.timeback.hole_filling_untagged_question_count": untaggedQuestionNumbers.length
|
|
91249
91624
|
});
|
|
91625
|
+
return {
|
|
91626
|
+
context: {
|
|
91627
|
+
enrollment: enrollmentContext,
|
|
91628
|
+
triggeringResult: { ...summary, ...qtiTestId && { qtiTestId } },
|
|
91629
|
+
missedStandards,
|
|
91630
|
+
untaggedMissedQuestionNumbers: untaggedQuestionNumbers
|
|
91631
|
+
},
|
|
91632
|
+
complete
|
|
91633
|
+
};
|
|
91250
91634
|
}
|
|
91251
91635
|
async getUserDataByTimebackId(timebackId) {
|
|
91252
91636
|
return this.withClientTelemetry(async () => {
|
|
@@ -92090,17 +92474,21 @@ var init_timeback_service = __esm(async () => {
|
|
|
92090
92474
|
activityData
|
|
92091
92475
|
});
|
|
92092
92476
|
await this.deps.validateDeveloperAccess(user, gameId);
|
|
92093
|
-
const integration = await db.query.gameTimebackIntegrations.findFirst({
|
|
92094
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, activityData.grade), eq(gameTimebackIntegrations.subject, activityData.subject), isActiveBaseGameTimebackIntegration())
|
|
92095
|
-
});
|
|
92096
|
-
if (!integration) {
|
|
92097
|
-
throw new NotFoundError(`Timeback integration for game (grade ${activityData.grade}, subject ${activityData.subject})`);
|
|
92098
|
-
}
|
|
92099
92477
|
const {
|
|
92478
|
+
integration,
|
|
92100
92479
|
courseId: runtimeCourseId,
|
|
92480
|
+
baseCourseId,
|
|
92481
|
+
otherCourseIds,
|
|
92101
92482
|
courseName: runtimeCourseName,
|
|
92483
|
+
extensions: runtimeExtensions,
|
|
92102
92484
|
attributes: courseAttributes
|
|
92103
|
-
} =
|
|
92485
|
+
} = await this.resolveRuntimeCourse({
|
|
92486
|
+
gameId,
|
|
92487
|
+
studentId,
|
|
92488
|
+
runId,
|
|
92489
|
+
activityData,
|
|
92490
|
+
extensions: extensionsWithResumeId
|
|
92491
|
+
});
|
|
92104
92492
|
const scorePercentage = scoreData.totalQuestions > 0 ? scoreData.correctQuestions / scoreData.totalQuestions * 100 : 0;
|
|
92105
92493
|
const dedupeKey = runId !== undefined ? {
|
|
92106
92494
|
gameId,
|
|
@@ -92129,12 +92517,12 @@ var init_timeback_service = __esm(async () => {
|
|
|
92129
92517
|
courseId: runtimeCourseId,
|
|
92130
92518
|
courseName: runtimeCourseName,
|
|
92131
92519
|
studentEmail: activityData.studentEmail,
|
|
92132
|
-
extensions:
|
|
92520
|
+
extensions: runtimeExtensions,
|
|
92133
92521
|
...runId ? { runId } : {}
|
|
92134
92522
|
};
|
|
92135
|
-
const completionEventId = dedupeKey ? TimebackService.buildCompletionCaliperEventId(dedupeKey, "completion") : undefined;
|
|
92136
|
-
const sessionEndEventId = dedupeKey ? TimebackService.buildCompletionCaliperEventId(dedupeKey, "session-end", effectiveResumeId) : undefined;
|
|
92137
|
-
const completionHistoryEventId = dedupeKey ? TimebackService.buildCompletionCaliperEventId(dedupeKey, "course-completed") : undefined;
|
|
92523
|
+
const completionEventId = dedupeKey ? TimebackService.buildCompletionCaliperEventId(dedupeKey, "completion", baseCourseId) : undefined;
|
|
92524
|
+
const sessionEndEventId = dedupeKey ? TimebackService.buildCompletionCaliperEventId(dedupeKey, "session-end", baseCourseId, effectiveResumeId) : undefined;
|
|
92525
|
+
const completionHistoryEventId = dedupeKey ? TimebackService.buildCompletionCaliperEventId(dedupeKey, "course-completed", baseCourseId) : undefined;
|
|
92138
92526
|
function emitRecord() {
|
|
92139
92527
|
return client.activity.record(runtimeCourseId, studentId, {
|
|
92140
92528
|
...activityEventContext,
|
|
@@ -92158,7 +92546,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
92158
92546
|
xpAwarded: emitted.xpAwarded,
|
|
92159
92547
|
masteredUnitsApplied: emitted.masteredUnitsApplied,
|
|
92160
92548
|
pctCompleteApp: emitted.pctCompleteApp ?? null
|
|
92161
|
-
}));
|
|
92549
|
+
}), otherCourseIds);
|
|
92162
92550
|
if (outcome.status === "duplicate") {
|
|
92163
92551
|
blockedByDedupeGuard = true;
|
|
92164
92552
|
duplicateAward = outcome.award;
|
|
@@ -92206,6 +92594,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
92206
92594
|
return {
|
|
92207
92595
|
status: "ok",
|
|
92208
92596
|
courseId: runtimeCourseId,
|
|
92597
|
+
courseKind: integration.kind,
|
|
92209
92598
|
xpAwarded: duplicateAward?.xpAwarded ?? 0,
|
|
92210
92599
|
masteredUnits: duplicateAward?.masteredUnitsApplied ?? 0,
|
|
92211
92600
|
...duplicateAward?.pctCompleteApp != null ? { pctCompleteApp: duplicateAward.pctCompleteApp } : {},
|
|
@@ -92218,6 +92607,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
92218
92607
|
return {
|
|
92219
92608
|
status: "ok",
|
|
92220
92609
|
courseId: runtimeCourseId,
|
|
92610
|
+
courseKind: integration.kind,
|
|
92221
92611
|
xpAwarded: result.xpAwarded,
|
|
92222
92612
|
masteredUnits: result.masteredUnitsApplied,
|
|
92223
92613
|
pctCompleteApp: result.pctCompleteApp,
|
|
@@ -92394,7 +92784,6 @@ var init_timeback_service = __esm(async () => {
|
|
|
92394
92784
|
}) {
|
|
92395
92785
|
return this.withClientTelemetry(async () => {
|
|
92396
92786
|
const client = this.requireClient();
|
|
92397
|
-
const db = this.deps.db;
|
|
92398
92787
|
const hasWindowStartedAtMs = windowStartedAtMs !== undefined;
|
|
92399
92788
|
const hasWindowSequence = windowSequence !== undefined;
|
|
92400
92789
|
if (hasWindowStartedAtMs === hasWindowSequence) {
|
|
@@ -92422,17 +92811,17 @@ var init_timeback_service = __esm(async () => {
|
|
|
92422
92811
|
return inFlightHeartbeat;
|
|
92423
92812
|
}
|
|
92424
92813
|
const pendingHeartbeat = (async () => {
|
|
92425
|
-
const integration = await db.query.gameTimebackIntegrations.findFirst({
|
|
92426
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, activityData.grade), eq(gameTimebackIntegrations.subject, activityData.subject), isActiveBaseGameTimebackIntegration())
|
|
92427
|
-
});
|
|
92428
|
-
if (!integration) {
|
|
92429
|
-
throw new NotFoundError(`Timeback integration for game (grade ${activityData.grade}, subject ${activityData.subject})`);
|
|
92430
|
-
}
|
|
92431
92814
|
const {
|
|
92432
92815
|
courseId: runtimeCourseId,
|
|
92433
92816
|
courseName: runtimeCourseName,
|
|
92817
|
+
extensions: runtimeExtensions,
|
|
92434
92818
|
attributes: courseAttributes
|
|
92435
|
-
} =
|
|
92819
|
+
} = await this.resolveRuntimeCourse({
|
|
92820
|
+
gameId,
|
|
92821
|
+
studentId,
|
|
92822
|
+
activityData,
|
|
92823
|
+
extensions: TimebackService.addResumeIdToExtensions(undefined, effectiveResumeId)
|
|
92824
|
+
});
|
|
92436
92825
|
const activeTimeSeconds = timingData.activeMs / 1000;
|
|
92437
92826
|
const inactiveTimeSeconds = timingData.pausedMs / 1000;
|
|
92438
92827
|
if (activeTimeSeconds > 0 || inactiveTimeSeconds > 0) {
|
|
@@ -92448,7 +92837,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
92448
92837
|
courseId: runtimeCourseId,
|
|
92449
92838
|
courseName: runtimeCourseName,
|
|
92450
92839
|
studentEmail: activityData.studentEmail,
|
|
92451
|
-
extensions:
|
|
92840
|
+
extensions: runtimeExtensions,
|
|
92452
92841
|
...runId ? { runId } : {}
|
|
92453
92842
|
});
|
|
92454
92843
|
}
|
|
@@ -94801,33 +95190,33 @@ var init_auth_provider = __esm(async () => {
|
|
|
94801
95190
|
function createSandboxCacheProvider() {
|
|
94802
95191
|
return {
|
|
94803
95192
|
async get(key) {
|
|
94804
|
-
const entry =
|
|
95193
|
+
const entry = cache2.get(key);
|
|
94805
95194
|
if (!entry) {
|
|
94806
95195
|
return null;
|
|
94807
95196
|
}
|
|
94808
95197
|
if (entry.expiresAt < Date.now()) {
|
|
94809
|
-
|
|
95198
|
+
cache2.delete(key);
|
|
94810
95199
|
return null;
|
|
94811
95200
|
}
|
|
94812
95201
|
return entry.value;
|
|
94813
95202
|
},
|
|
94814
95203
|
async set(key, value, ttlSeconds = 300) {
|
|
94815
|
-
|
|
95204
|
+
cache2.set(key, {
|
|
94816
95205
|
value,
|
|
94817
95206
|
expiresAt: Date.now() + ttlSeconds * 1000
|
|
94818
95207
|
});
|
|
94819
95208
|
},
|
|
94820
95209
|
async delete(key) {
|
|
94821
|
-
|
|
95210
|
+
cache2.delete(key);
|
|
94822
95211
|
}
|
|
94823
95212
|
};
|
|
94824
95213
|
}
|
|
94825
95214
|
function clearSandboxCache() {
|
|
94826
|
-
|
|
95215
|
+
cache2.clear();
|
|
94827
95216
|
}
|
|
94828
|
-
var
|
|
95217
|
+
var cache2;
|
|
94829
95218
|
var init_cache_provider = __esm(() => {
|
|
94830
|
-
|
|
95219
|
+
cache2 = new Map;
|
|
94831
95220
|
});
|
|
94832
95221
|
|
|
94833
95222
|
// ../utils/src/keys.ts
|
|
@@ -151055,7 +151444,7 @@ function parseQtiLibraryParams(searchParams) {
|
|
|
151055
151444
|
limit
|
|
151056
151445
|
};
|
|
151057
151446
|
}
|
|
151058
|
-
var populateStudent, getUser, getUserEnrollments, getUserById, setupIntegration, getIntegrations, getRemovedIntegrations, createIntegration, getHoleFillingCourses, createHoleFillingCourse, updateIntegration, deactivateCourse, reactivateCourse, getIntegrationConfig, verifyIntegration, getConfig, deleteIntegrations, endActivity, heartbeat, advanceCourse, unenrollCourse, prepareRuntimeAssessment, startRuntimeAssessment, getRuntimeAssessment, getLatestRuntimeAssessment, resumeRuntimeAssessment, saveRuntimeAssessment, recordRuntimeAssessmentProgress, 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, reviseAssessment, synchronizeReviewBank, reorderQuestions, removeAssessment, listQuestions, listQuestionLibrary, listTestLibrary, copyAssessment, createQuestion, updateQuestion, removeQuestion, timeback2;
|
|
151447
|
+
var populateStudent, getUser, getUserEnrollments, getUserHoleFillingContext, getUserById, setupIntegration, getIntegrations, getRemovedIntegrations, createIntegration, getHoleFillingCourses, createHoleFillingCourse, updateIntegration, deactivateCourse, reactivateCourse, getIntegrationConfig, verifyIntegration, getConfig, deleteIntegrations, endActivity, heartbeat, advanceCourse, unenrollCourse, prepareRuntimeAssessment, startRuntimeAssessment, getRuntimeAssessment, getLatestRuntimeAssessment, resumeRuntimeAssessment, saveRuntimeAssessment, recordRuntimeAssessmentProgress, 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, reviseAssessment, synchronizeReviewBank, reorderQuestions, removeAssessment, listQuestions, listQuestionLibrary, listTestLibrary, copyAssessment, createQuestion, updateQuestion, removeQuestion, timeback2;
|
|
151059
151448
|
var init_timeback_controller = __esm(() => {
|
|
151060
151449
|
init_esm();
|
|
151061
151450
|
init_src();
|
|
@@ -151081,6 +151470,12 @@ var init_timeback_controller = __esm(() => {
|
|
|
151081
151470
|
});
|
|
151082
151471
|
getUser = requireNonAnonymous(async (ctx) => ctx.services.timeback.getUserData(ctx.user.id, ctx.gameId));
|
|
151083
151472
|
getUserEnrollments = requireNonAnonymous(async (ctx) => ctx.services.timeback.getUserEnrollments(ctx.user.id, ctx.gameId));
|
|
151473
|
+
getUserHoleFillingContext = requireNonAnonymous(async (ctx) => {
|
|
151474
|
+
if (!ctx.gameId) {
|
|
151475
|
+
throw ApiError.forbidden("Hole-filling context requires a game token");
|
|
151476
|
+
}
|
|
151477
|
+
return ctx.services.timeback.getHoleFillingContext(ctx.user.id, ctx.gameId);
|
|
151478
|
+
});
|
|
151084
151479
|
getUserById = requireNonAnonymous(async (ctx) => {
|
|
151085
151480
|
const timebackId = ctx.params.timebackId;
|
|
151086
151481
|
if (!timebackId) {
|
|
@@ -151934,6 +152329,7 @@ var init_timeback_controller = __esm(() => {
|
|
|
151934
152329
|
populateStudent,
|
|
151935
152330
|
getUser,
|
|
151936
152331
|
getUserEnrollments,
|
|
152332
|
+
getUserHoleFillingContext,
|
|
151937
152333
|
getUserById,
|
|
151938
152334
|
setupIntegration,
|
|
151939
152335
|
getIntegrations,
|
|
@@ -152996,6 +153392,12 @@ var init_timeback8 = __esm(async () => {
|
|
|
152996
153392
|
}
|
|
152997
153393
|
return handle2(timeback2.getUserEnrollments)(c);
|
|
152998
153394
|
});
|
|
153395
|
+
timebackRouter.get("/user/hole-filling-context", async (c) => {
|
|
153396
|
+
if (shouldMockTimeback()) {
|
|
153397
|
+
return c.json({ contexts: [] });
|
|
153398
|
+
}
|
|
153399
|
+
return handle2(timeback2.getUserHoleFillingContext)(c);
|
|
153400
|
+
});
|
|
152999
153401
|
timebackRouter.get("/user/:timebackId", async (c) => {
|
|
153000
153402
|
const user = c.get("user");
|
|
153001
153403
|
if (!user) {
|