@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/cli.js
CHANGED
|
@@ -2517,7 +2517,7 @@ var package_default;
|
|
|
2517
2517
|
var init_package = __esm(() => {
|
|
2518
2518
|
package_default = {
|
|
2519
2519
|
name: "@playcademy/sandbox",
|
|
2520
|
-
version: "0.12.2-beta.
|
|
2520
|
+
version: "0.12.2-beta.4",
|
|
2521
2521
|
description: "Local development server for Playcademy game development",
|
|
2522
2522
|
type: "module",
|
|
2523
2523
|
exports: {
|
|
@@ -22888,6 +22888,9 @@ var init_helpers = __esm(() => {
|
|
|
22888
22888
|
function isBaseGameTimebackIntegration() {
|
|
22889
22889
|
return sql`${gameTimebackIntegrations.kind} = ${BASE_GAME_TIMEBACK_INTEGRATION_KIND}`;
|
|
22890
22890
|
}
|
|
22891
|
+
function isBaseGameTimebackIntegrationRow(integration) {
|
|
22892
|
+
return integration.kind === BASE_GAME_TIMEBACK_INTEGRATION_KIND;
|
|
22893
|
+
}
|
|
22891
22894
|
function isActiveGameTimebackIntegrationStatus(statusColumn = gameTimebackIntegrations.status) {
|
|
22892
22895
|
return sql`${statusColumn} IS DISTINCT FROM ${DEACTIVATED_GAME_TIMEBACK_INTEGRATION_STATUS}`;
|
|
22893
22896
|
}
|
|
@@ -50496,6 +50499,7 @@ var init_schemas4 = __esm(() => {
|
|
|
50496
50499
|
appName: stringType().optional(),
|
|
50497
50500
|
sensorUrl: stringType().url().optional(),
|
|
50498
50501
|
courseId: stringType().min(1).optional(),
|
|
50502
|
+
enrollmentId: stringType().min(1).optional(),
|
|
50499
50503
|
courseName: stringType().optional(),
|
|
50500
50504
|
studentEmail: stringType().email().optional()
|
|
50501
50505
|
});
|
|
@@ -81861,6 +81865,9 @@ function normalizedIdentityCase2(value) {
|
|
|
81861
81865
|
function frameworkAliasKey2(value) {
|
|
81862
81866
|
return value.normalize("NFKC").toLocaleLowerCase("en-US").replace(/[^a-z0-9]+/g, "");
|
|
81863
81867
|
}
|
|
81868
|
+
function isUuidShaped2(value) {
|
|
81869
|
+
return UUID_PATTERN2.test(value.trim());
|
|
81870
|
+
}
|
|
81864
81871
|
function isCommonCoreMathIdentifier2(identifier) {
|
|
81865
81872
|
const canonical = normalizedIdentityCase2(normalizedWhitespace2(identifier));
|
|
81866
81873
|
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);
|
|
@@ -81892,6 +81899,14 @@ function canonicalIdentifier2(framework, authoredIdentifier) {
|
|
|
81892
81899
|
}
|
|
81893
81900
|
return identifier;
|
|
81894
81901
|
}
|
|
81902
|
+
function readableAlignmentIdentifier2(alignment) {
|
|
81903
|
+
const identifier = alignment.identifier?.trim() ?? "";
|
|
81904
|
+
if (identifier && !isUuidShaped2(identifier)) {
|
|
81905
|
+
return identifier;
|
|
81906
|
+
}
|
|
81907
|
+
const id = alignment.id.trim();
|
|
81908
|
+
return id && !isUuidShaped2(id) ? id : "";
|
|
81909
|
+
}
|
|
81895
81910
|
function canonicalAssessmentStandardRef2(standard) {
|
|
81896
81911
|
const authoredFramework = normalizedWhitespace2(standard.framework);
|
|
81897
81912
|
const framework = canonicalFramework2(authoredFramework, standard.identifier);
|
|
@@ -81998,7 +82013,23 @@ function qtiStandardsFromMetadata(metadata2) {
|
|
|
81998
82013
|
standards.push(...qtiAlignmentStandardsFromMetadata2(metadata2));
|
|
81999
82014
|
return dedupeStandards2(standards);
|
|
82000
82015
|
}
|
|
82001
|
-
|
|
82016
|
+
function qtiAssessmentStandardRefsFromMetadata2(metadata2) {
|
|
82017
|
+
const refs = new Map;
|
|
82018
|
+
for (const alignment of qtiAlignmentStandardsFromMetadata2(metadata2)) {
|
|
82019
|
+
const identifier = readableAlignmentIdentifier2(alignment);
|
|
82020
|
+
if (identifier) {
|
|
82021
|
+
const canonical = canonicalAssessmentStandardRef2({
|
|
82022
|
+
framework: alignment.source,
|
|
82023
|
+
identifier
|
|
82024
|
+
});
|
|
82025
|
+
if (canonical.framework && canonical.identifier) {
|
|
82026
|
+
refs.set(assessmentStandardRefKey2(canonical), canonical);
|
|
82027
|
+
}
|
|
82028
|
+
}
|
|
82029
|
+
}
|
|
82030
|
+
return [...refs.values()];
|
|
82031
|
+
}
|
|
82032
|
+
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;
|
|
82002
82033
|
var init_qti = __esm(() => {
|
|
82003
82034
|
init_timeback4();
|
|
82004
82035
|
init_timeback4();
|
|
@@ -82360,6 +82391,7 @@ var init_qti = __esm(() => {
|
|
|
82360
82391
|
this.name = "QtiScoringArtifactValidationError";
|
|
82361
82392
|
}
|
|
82362
82393
|
};
|
|
82394
|
+
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;
|
|
82363
82395
|
COMMON_CORE_MATH_FRAMEWORK_ALIASES2 = new Set([
|
|
82364
82396
|
"ccssm",
|
|
82365
82397
|
"ccssmath",
|
|
@@ -82569,7 +82601,7 @@ function pruneUnreadableStandards(standards) {
|
|
|
82569
82601
|
return standards.filter(hasReadableStandardLabel);
|
|
82570
82602
|
}
|
|
82571
82603
|
function parseQuestionIndex(title) {
|
|
82572
|
-
const match = title
|
|
82604
|
+
const match = title?.match(/\bquestion[\s_-]*(\d+)\b/i);
|
|
82573
82605
|
const index = match ? Number(match[1]) : Number.NaN;
|
|
82574
82606
|
return Number.isFinite(index) && index > 0 ? index : null;
|
|
82575
82607
|
}
|
|
@@ -82603,14 +82635,15 @@ function qtiQuestionsByIndex(qtiQuestions) {
|
|
|
82603
82635
|
}
|
|
82604
82636
|
return questionsByIndex;
|
|
82605
82637
|
}
|
|
82638
|
+
function normalizedQuestionScore(score, scorePercentile) {
|
|
82639
|
+
const rawValue = numberField(score) ?? numberField(scorePercentile);
|
|
82640
|
+
return rawValue !== null && rawValue > 1 ? rawValue / 100 : rawValue;
|
|
82641
|
+
}
|
|
82606
82642
|
function getQuestionCorrectness(result) {
|
|
82607
82643
|
if (!result) {
|
|
82608
82644
|
return "unknown";
|
|
82609
82645
|
}
|
|
82610
|
-
const
|
|
82611
|
-
const scorePercentile = numberField(result.scorePercentile);
|
|
82612
|
-
const rawValue = score ?? scorePercentile;
|
|
82613
|
-
const value = rawValue !== null && rawValue > 1 ? rawValue / 100 : rawValue;
|
|
82646
|
+
const value = normalizedQuestionScore(result.score, result.scorePercentile);
|
|
82614
82647
|
if (value === null) {
|
|
82615
82648
|
return "unknown";
|
|
82616
82649
|
}
|
|
@@ -82874,10 +82907,71 @@ var init_timeback_mastery_completion_util = __esm(async () => {
|
|
|
82874
82907
|
await init_errors8();
|
|
82875
82908
|
});
|
|
82876
82909
|
|
|
82910
|
+
// ../api-core/src/utils/bounded-map.util.ts
|
|
82911
|
+
function setBounded(map, key, value, maxSize) {
|
|
82912
|
+
map.delete(key);
|
|
82913
|
+
if (map.size >= maxSize) {
|
|
82914
|
+
const oldestKey = map.keys().next().value;
|
|
82915
|
+
if (oldestKey !== undefined) {
|
|
82916
|
+
map.delete(oldestKey);
|
|
82917
|
+
}
|
|
82918
|
+
}
|
|
82919
|
+
map.set(key, value);
|
|
82920
|
+
}
|
|
82921
|
+
|
|
82922
|
+
// ../api-core/src/utils/timeback-qti-test-lookup.util.ts
|
|
82923
|
+
async function resolveQtiTestId(client, testName, parentLineItem) {
|
|
82924
|
+
const cacheKey = `${parentLineItem.sourcedId ?? ""}:${testName}`;
|
|
82925
|
+
const cached = cache.get(cacheKey);
|
|
82926
|
+
if (cached) {
|
|
82927
|
+
setBounded(cache, cacheKey, cached, MAX_CACHE_SIZE);
|
|
82928
|
+
return cached;
|
|
82929
|
+
}
|
|
82930
|
+
let resource = null;
|
|
82931
|
+
const resourceId = sourceIdFromRef(parentLineItem.componentResource);
|
|
82932
|
+
if (resourceId) {
|
|
82933
|
+
resource = await client.api.oneroster.resources.get(resourceId).catch(() => null);
|
|
82934
|
+
}
|
|
82935
|
+
for (const candidate of qtiIdCandidates(parentLineItem, resource)) {
|
|
82936
|
+
const test = await client.qtiApi.assessmentTests.get(candidate).catch(() => null);
|
|
82937
|
+
if (test?.identifier) {
|
|
82938
|
+
setBounded(cache, cacheKey, test.identifier, MAX_CACHE_SIZE);
|
|
82939
|
+
return test.identifier;
|
|
82940
|
+
}
|
|
82941
|
+
}
|
|
82942
|
+
const normalizedTestName = testName.trim();
|
|
82943
|
+
if (!normalizedTestName) {
|
|
82944
|
+
return "";
|
|
82945
|
+
}
|
|
82946
|
+
for (let page = 1;page <= TITLE_LOOKUP_MAX_PAGES; page++) {
|
|
82947
|
+
const list = await client.qtiApi.assessmentTests.list({
|
|
82948
|
+
query: normalizedTestName,
|
|
82949
|
+
limit: TITLE_LOOKUP_LIMIT,
|
|
82950
|
+
page,
|
|
82951
|
+
sort: "title",
|
|
82952
|
+
order: "asc"
|
|
82953
|
+
}).catch(() => null);
|
|
82954
|
+
const qtiTestId = findQtiAssessmentTestIdByTitle(list?.items ?? [], normalizedTestName);
|
|
82955
|
+
if (qtiTestId) {
|
|
82956
|
+
setBounded(cache, cacheKey, qtiTestId, MAX_CACHE_SIZE);
|
|
82957
|
+
return qtiTestId;
|
|
82958
|
+
}
|
|
82959
|
+
const itemCount = list?.items?.length ?? 0;
|
|
82960
|
+
if (!list || itemCount < TITLE_LOOKUP_LIMIT || typeof list.pages === "number" && page >= list.pages) {
|
|
82961
|
+
break;
|
|
82962
|
+
}
|
|
82963
|
+
}
|
|
82964
|
+
return "";
|
|
82965
|
+
}
|
|
82966
|
+
var MAX_CACHE_SIZE = 500, TITLE_LOOKUP_LIMIT = 50, TITLE_LOOKUP_MAX_PAGES = 5, cache;
|
|
82967
|
+
var init_timeback_qti_test_lookup_util = __esm(() => {
|
|
82968
|
+
init_timeback_grade_level_results_util();
|
|
82969
|
+
cache = new Map;
|
|
82970
|
+
});
|
|
82971
|
+
|
|
82877
82972
|
// ../api-core/src/services/timeback-admin.service.ts
|
|
82878
82973
|
class TimebackAdminService {
|
|
82879
82974
|
deps;
|
|
82880
|
-
qtiTestIdCache = new Map;
|
|
82881
82975
|
static XP_PRECISION_FACTOR = 10;
|
|
82882
82976
|
static RECENT_ACTIVITY_LIMIT = 20;
|
|
82883
82977
|
static MAX_STUDENT_ACTIVITY_LIMIT = 200;
|
|
@@ -82892,9 +82986,6 @@ class TimebackAdminService {
|
|
|
82892
82986
|
static GAME_METRICS_FETCH_TIMEOUT_MS = 1e4;
|
|
82893
82987
|
static GAME_METRICS_LIST_FETCH_TIMEOUT_MS = 3000;
|
|
82894
82988
|
static GAME_METRICS_RUN_IDS_PER_REQUEST = 50;
|
|
82895
|
-
static MAX_QTI_TEST_ID_CACHE_SIZE = 500;
|
|
82896
|
-
static QTI_TEST_TITLE_LOOKUP_LIMIT = 50;
|
|
82897
|
-
static QTI_TEST_TITLE_LOOKUP_MAX_PAGES = 5;
|
|
82898
82989
|
constructor(deps) {
|
|
82899
82990
|
this.deps = deps;
|
|
82900
82991
|
}
|
|
@@ -83754,68 +83845,6 @@ class TimebackAdminService {
|
|
|
83754
83845
|
return { activities: activitiesWithGameMetrics, hasMore };
|
|
83755
83846
|
});
|
|
83756
83847
|
}
|
|
83757
|
-
rememberQtiTestId(cacheKey, qtiTestId) {
|
|
83758
|
-
if (!qtiTestId) {
|
|
83759
|
-
this.qtiTestIdCache.delete(cacheKey);
|
|
83760
|
-
return;
|
|
83761
|
-
}
|
|
83762
|
-
if (this.qtiTestIdCache.has(cacheKey)) {
|
|
83763
|
-
this.qtiTestIdCache.delete(cacheKey);
|
|
83764
|
-
} else if (this.qtiTestIdCache.size >= TimebackAdminService.MAX_QTI_TEST_ID_CACHE_SIZE) {
|
|
83765
|
-
const oldestKey = this.qtiTestIdCache.keys().next().value;
|
|
83766
|
-
if (oldestKey !== undefined) {
|
|
83767
|
-
this.qtiTestIdCache.delete(oldestKey);
|
|
83768
|
-
}
|
|
83769
|
-
}
|
|
83770
|
-
this.qtiTestIdCache.set(cacheKey, qtiTestId);
|
|
83771
|
-
}
|
|
83772
|
-
async resolveQtiTestIdForGradeLevelResult(client, testName, parentLineItem) {
|
|
83773
|
-
const cacheKey = `${parentLineItem.sourcedId ?? ""}:${testName}`;
|
|
83774
|
-
const cachedQtiTestId = cacheKey ? this.qtiTestIdCache.get(cacheKey) : undefined;
|
|
83775
|
-
if (cachedQtiTestId) {
|
|
83776
|
-
this.rememberQtiTestId(cacheKey, cachedQtiTestId);
|
|
83777
|
-
return cachedQtiTestId;
|
|
83778
|
-
}
|
|
83779
|
-
let resource = null;
|
|
83780
|
-
const resourceId = sourceIdFromRef(parentLineItem.componentResource);
|
|
83781
|
-
if (resourceId) {
|
|
83782
|
-
resource = await client.api.oneroster.resources.get(resourceId).catch(() => null);
|
|
83783
|
-
}
|
|
83784
|
-
for (const candidate of qtiIdCandidates(parentLineItem, resource)) {
|
|
83785
|
-
const test = await client.qtiApi.assessmentTests.get(candidate).catch(() => null);
|
|
83786
|
-
if (test?.identifier) {
|
|
83787
|
-
if (cacheKey) {
|
|
83788
|
-
this.rememberQtiTestId(cacheKey, test.identifier);
|
|
83789
|
-
}
|
|
83790
|
-
return test.identifier;
|
|
83791
|
-
}
|
|
83792
|
-
}
|
|
83793
|
-
const normalizedTestName = testName.trim();
|
|
83794
|
-
if (!normalizedTestName) {
|
|
83795
|
-
return "";
|
|
83796
|
-
}
|
|
83797
|
-
for (let page = 1;page <= TimebackAdminService.QTI_TEST_TITLE_LOOKUP_MAX_PAGES; page++) {
|
|
83798
|
-
const list = await client.qtiApi.assessmentTests.list({
|
|
83799
|
-
query: normalizedTestName,
|
|
83800
|
-
limit: TimebackAdminService.QTI_TEST_TITLE_LOOKUP_LIMIT,
|
|
83801
|
-
page,
|
|
83802
|
-
sort: "title",
|
|
83803
|
-
order: "asc"
|
|
83804
|
-
}).catch(() => null);
|
|
83805
|
-
const qtiTestId = findQtiAssessmentTestIdByTitle(list?.items ?? [], normalizedTestName);
|
|
83806
|
-
if (qtiTestId) {
|
|
83807
|
-
if (cacheKey) {
|
|
83808
|
-
this.rememberQtiTestId(cacheKey, qtiTestId);
|
|
83809
|
-
}
|
|
83810
|
-
return qtiTestId;
|
|
83811
|
-
}
|
|
83812
|
-
const itemCount = list?.items?.length ?? 0;
|
|
83813
|
-
if (!list || itemCount < TimebackAdminService.QTI_TEST_TITLE_LOOKUP_LIMIT || typeof list.pages === "number" && page >= list.pages) {
|
|
83814
|
-
break;
|
|
83815
|
-
}
|
|
83816
|
-
}
|
|
83817
|
-
return "";
|
|
83818
|
-
}
|
|
83819
83848
|
async getGradeLevelTestResults(user, options) {
|
|
83820
83849
|
return this.withClientTelemetry(async () => {
|
|
83821
83850
|
const { gameId, courseId, studentId } = options;
|
|
@@ -83918,7 +83947,7 @@ class TimebackAdminService {
|
|
|
83918
83947
|
}).then((page) => page.data) : [];
|
|
83919
83948
|
const latestResultsByLineItem = latestAssessmentResultsByLineItem(questionResults);
|
|
83920
83949
|
const testName = resolveGradeLevelTestName(parentResult, parentLineItem);
|
|
83921
|
-
const qtiTestId = await
|
|
83950
|
+
const qtiTestId = await resolveQtiTestId(client, testName, parentLineItem);
|
|
83922
83951
|
const qtiQuestions = qtiTestId ? await client.qtiApi.assessmentTests.getQuestions(qtiTestId).then((response) => response.questions ?? []).catch(() => []) : [];
|
|
83923
83952
|
const questions = mapGradeLevelReviewQuestions({
|
|
83924
83953
|
questionLineItems,
|
|
@@ -84612,6 +84641,7 @@ var init_timeback_admin_service = __esm(async () => {
|
|
|
84612
84641
|
init_timeback_discrepancy_queue_util();
|
|
84613
84642
|
init_timeback_game_metrics_comparison_util();
|
|
84614
84643
|
init_timeback_grade_level_results_util();
|
|
84644
|
+
init_timeback_qti_test_lookup_util();
|
|
84615
84645
|
init_timeback_util();
|
|
84616
84646
|
await __promiseAll([
|
|
84617
84647
|
init_dist5(),
|
|
@@ -92510,6 +92540,22 @@ var init_timeback_assessments_service = __esm(async () => {
|
|
|
92510
92540
|
await init_errors8();
|
|
92511
92541
|
});
|
|
92512
92542
|
|
|
92543
|
+
// ../api-core/src/utils/timeback-attribution.util.ts
|
|
92544
|
+
function resolveRuntimeAttribution(base, holeFilling, requestedCourseId, recordedCourseId) {
|
|
92545
|
+
const recorded = [base, ...holeFilling].find((integration) => integration.courseId === recordedCourseId);
|
|
92546
|
+
if (recorded) {
|
|
92547
|
+
return { integration: recorded, requestedCourseIdStatus: "pinned_by_run" };
|
|
92548
|
+
}
|
|
92549
|
+
if (requestedCourseId === undefined) {
|
|
92550
|
+
return { integration: base, requestedCourseIdStatus: "absent" };
|
|
92551
|
+
}
|
|
92552
|
+
if (requestedCourseId === base.courseId) {
|
|
92553
|
+
return { integration: base, requestedCourseIdStatus: "accepted" };
|
|
92554
|
+
}
|
|
92555
|
+
const requestedHoleFilling = holeFilling.find((integration) => integration.courseId === requestedCourseId);
|
|
92556
|
+
return requestedHoleFilling ? { integration: requestedHoleFilling, requestedCourseIdStatus: "accepted_hole_filling" } : { integration: base, requestedCourseIdStatus: "ignored_mismatch" };
|
|
92557
|
+
}
|
|
92558
|
+
|
|
92513
92559
|
// ../api-core/src/utils/timeback-create-integration.util.ts
|
|
92514
92560
|
function buildTimebackBaseConfigFromExistingConfig(config) {
|
|
92515
92561
|
return {
|
|
@@ -92602,6 +92648,128 @@ var init_timeback_create_integration_util = __esm(() => {
|
|
|
92602
92648
|
init_timeback_util();
|
|
92603
92649
|
});
|
|
92604
92650
|
|
|
92651
|
+
// ../api-core/src/utils/timeback-hole-filling-context.util.ts
|
|
92652
|
+
function buildAssignmentResultWhere(options) {
|
|
92653
|
+
return {
|
|
92654
|
+
status: "active",
|
|
92655
|
+
"student.sourcedId": options.studentId,
|
|
92656
|
+
"metadata.resultType": options.resultType,
|
|
92657
|
+
"metadata.assignmentId": options.assignmentId
|
|
92658
|
+
};
|
|
92659
|
+
}
|
|
92660
|
+
function assignmentIdOf(record) {
|
|
92661
|
+
const value = metadataOf(record).assignmentId;
|
|
92662
|
+
if (typeof value === "number" && Number.isFinite(value)) {
|
|
92663
|
+
return String(value);
|
|
92664
|
+
}
|
|
92665
|
+
const text = stringField3(value).trim();
|
|
92666
|
+
return text || undefined;
|
|
92667
|
+
}
|
|
92668
|
+
function toTriggeringResult(result) {
|
|
92669
|
+
const metadata2 = metadataOf(result);
|
|
92670
|
+
const testType = normalizeTestType(metadata2.testType);
|
|
92671
|
+
const assignmentId = assignmentIdOf(result);
|
|
92672
|
+
const lineItemId = sourceIdFromRef(result.assessmentLineItem);
|
|
92673
|
+
if (!assignmentId || !lineItemId || result.scoreStatus !== SCORE_STATUS2.fullyGraded || !isAllowedGradeLevelTestType(testType)) {
|
|
92674
|
+
return null;
|
|
92675
|
+
}
|
|
92676
|
+
return {
|
|
92677
|
+
resultId: result.sourcedId ?? "",
|
|
92678
|
+
assignmentId,
|
|
92679
|
+
lineItemId,
|
|
92680
|
+
testName: stringField3(metadata2.testName) || lineItemId,
|
|
92681
|
+
testType,
|
|
92682
|
+
score: numberField(result.score),
|
|
92683
|
+
scoreDate: result.scoreDate ?? null,
|
|
92684
|
+
totalQuestions: numberField(metadata2.totalQuestions),
|
|
92685
|
+
correctQuestions: numberField(metadata2.correctQuestions)
|
|
92686
|
+
};
|
|
92687
|
+
}
|
|
92688
|
+
function isMissed(resultText, score, scorePercentile) {
|
|
92689
|
+
const marking = stringField3(resultText).trim().toLowerCase();
|
|
92690
|
+
if (marking) {
|
|
92691
|
+
return marking !== "correct";
|
|
92692
|
+
}
|
|
92693
|
+
const value = normalizedQuestionScore(score, scorePercentile);
|
|
92694
|
+
return value !== null && value < 1;
|
|
92695
|
+
}
|
|
92696
|
+
function missedQuestionsFromInlineResponses(result) {
|
|
92697
|
+
const responses = metadataOf(result).responses;
|
|
92698
|
+
const questions = isRecord6(responses) && Array.isArray(responses.questions) ? responses.questions : undefined;
|
|
92699
|
+
if (!questions?.length) {
|
|
92700
|
+
return;
|
|
92701
|
+
}
|
|
92702
|
+
return questions.flatMap((question) => {
|
|
92703
|
+
if (!isRecord6(question) || !isMissed(question.result, question.score)) {
|
|
92704
|
+
return [];
|
|
92705
|
+
}
|
|
92706
|
+
const questionNumber = numberField(question.questionNumber);
|
|
92707
|
+
const qtiItemId = stringField3(question.questionQtiIdentifier).trim();
|
|
92708
|
+
return questionNumber === null ? [] : [{ questionNumber, ...qtiItemId && { qtiItemId } }];
|
|
92709
|
+
});
|
|
92710
|
+
}
|
|
92711
|
+
function questionNumberFromLineItemId(lineItemId) {
|
|
92712
|
+
const match = lineItemId.match(/_q(\d+)$/);
|
|
92713
|
+
const value = match ? Number(match[1]) : Number.NaN;
|
|
92714
|
+
return Number.isInteger(value) && value > 0 ? value : null;
|
|
92715
|
+
}
|
|
92716
|
+
function missedQuestionsFromQuestionResults(questionResults) {
|
|
92717
|
+
return [...latestAssessmentResultsByLineItem(questionResults)].flatMap(([lineItemId, result]) => {
|
|
92718
|
+
const questionNumber = questionNumberFromLineItemId(lineItemId);
|
|
92719
|
+
const responses = metadataOf(result).responses;
|
|
92720
|
+
const response = isRecord6(responses) && isRecord6(responses.response) ? responses.response : undefined;
|
|
92721
|
+
if (questionNumber === null || !isMissed(response?.result, result.score, result.scorePercentile)) {
|
|
92722
|
+
return [];
|
|
92723
|
+
}
|
|
92724
|
+
const qtiItemId = stringField3(response?.questionQtiIdentifier).trim();
|
|
92725
|
+
return [{ questionNumber, ...qtiItemId && { qtiItemId } }];
|
|
92726
|
+
}).toSorted((a, b) => a.questionNumber - b.questionNumber);
|
|
92727
|
+
}
|
|
92728
|
+
function qtiItemIdForQuestionNumber(qtiQuestions) {
|
|
92729
|
+
const byNumber = qtiQuestionsByIndex(qtiQuestions);
|
|
92730
|
+
return (questionNumber) => {
|
|
92731
|
+
const question = byNumber.get(questionNumber) ?? qtiQuestions[questionNumber - 1];
|
|
92732
|
+
return question ? question.question.identifier || question.reference.identifier : undefined;
|
|
92733
|
+
};
|
|
92734
|
+
}
|
|
92735
|
+
function groupMissedStandards(missed, standardsByItemId, itemIdFor) {
|
|
92736
|
+
const byKey = new Map;
|
|
92737
|
+
const untagged = [];
|
|
92738
|
+
for (const question of missed) {
|
|
92739
|
+
const itemId = question.qtiItemId ?? itemIdFor(question.questionNumber);
|
|
92740
|
+
const standards = itemId ? standardsByItemId.get(itemId) ?? [] : [];
|
|
92741
|
+
if (!standards.length) {
|
|
92742
|
+
untagged.push(question.questionNumber);
|
|
92743
|
+
}
|
|
92744
|
+
for (const standard of standards) {
|
|
92745
|
+
const key = `${standard.framework}\x00${standard.identifier}`;
|
|
92746
|
+
const entry = byKey.get(key) ?? {
|
|
92747
|
+
framework: standard.framework,
|
|
92748
|
+
code: standard.identifier,
|
|
92749
|
+
questionNumbers: [],
|
|
92750
|
+
qtiItemIds: []
|
|
92751
|
+
};
|
|
92752
|
+
entry.questionNumbers.push(question.questionNumber);
|
|
92753
|
+
if (itemId && !entry.qtiItemIds.includes(itemId)) {
|
|
92754
|
+
entry.qtiItemIds.push(itemId);
|
|
92755
|
+
}
|
|
92756
|
+
byKey.set(key, entry);
|
|
92757
|
+
}
|
|
92758
|
+
}
|
|
92759
|
+
return {
|
|
92760
|
+
missedStandards: [...byKey.values()].toSorted((a, b) => b.questionNumbers.length - a.questionNumbers.length || a.code.localeCompare(b.code, undefined, { numeric: true })),
|
|
92761
|
+
untaggedQuestionNumbers: untagged.toSorted((a, b) => a - b)
|
|
92762
|
+
};
|
|
92763
|
+
}
|
|
92764
|
+
function standardsFromQtiItem(item) {
|
|
92765
|
+
return qtiAssessmentStandardRefsFromMetadata2(metadataOf(item));
|
|
92766
|
+
}
|
|
92767
|
+
var init_timeback_hole_filling_context_util = __esm(() => {
|
|
92768
|
+
init_constants2();
|
|
92769
|
+
init_qti();
|
|
92770
|
+
init_timeback_grade_level_results_util();
|
|
92771
|
+
});
|
|
92772
|
+
|
|
92605
92773
|
// ../api-core/src/utils/timeback-promotion.util.ts
|
|
92606
92774
|
async function promoteCompletedCourse({
|
|
92607
92775
|
db,
|
|
@@ -92731,14 +92899,20 @@ var init_timeback_service = __esm(async () => {
|
|
|
92731
92899
|
init_helpers_index();
|
|
92732
92900
|
init_tables_index();
|
|
92733
92901
|
init_spans();
|
|
92902
|
+
init_constants2();
|
|
92734
92903
|
init_types2();
|
|
92735
92904
|
init_errors2();
|
|
92736
92905
|
init_timeback_create_integration_util();
|
|
92906
|
+
init_timeback_grade_level_results_util();
|
|
92907
|
+
init_timeback_hole_filling_context_util();
|
|
92737
92908
|
init_timeback_promotion_util();
|
|
92909
|
+
init_timeback_qti_test_lookup_util();
|
|
92738
92910
|
init_timeback_removed_integration_util();
|
|
92739
92911
|
init_timeback_util();
|
|
92740
92912
|
await init_dist5();
|
|
92741
92913
|
TimebackService = class TimebackService {
|
|
92914
|
+
static HOLE_FILLING_CONTEXT_TTL_MS = 10 * 60 * 1000;
|
|
92915
|
+
static HOLE_FILLING_CONTEXT_CACHE_SIZE = 1000;
|
|
92742
92916
|
static HEARTBEAT_DEDUPE_TTL_MS = 5 * 60 * 1000;
|
|
92743
92917
|
static processedHeartbeatWindows = new Map;
|
|
92744
92918
|
static inFlightHeartbeatWindows = new Map;
|
|
@@ -92746,6 +92920,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
92746
92920
|
static COMPLETION_IN_FLIGHT_SETTLE_DELAY_MS = 400;
|
|
92747
92921
|
static COMPLETION_IN_FLIGHT_SETTLE_ATTEMPTS = 5;
|
|
92748
92922
|
deps;
|
|
92923
|
+
holeFillingContextCache = new Map;
|
|
92749
92924
|
static cleanHeartbeatDedupeCache(now = Date.now()) {
|
|
92750
92925
|
for (const [key, timestamp] of this.processedHeartbeatWindows) {
|
|
92751
92926
|
if (now - timestamp > this.HEARTBEAT_DEDUPE_TTL_MS) {
|
|
@@ -92769,13 +92944,13 @@ var init_timeback_service = __esm(async () => {
|
|
|
92769
92944
|
static clearInFlightHeartbeatWindow(key) {
|
|
92770
92945
|
this.inFlightHeartbeatWindows.delete(key);
|
|
92771
92946
|
}
|
|
92772
|
-
static buildCompletionCaliperEventId(key, kind, discriminator = "completion") {
|
|
92947
|
+
static buildCompletionCaliperEventId(key, kind, baseCourseId, discriminator = "completion") {
|
|
92773
92948
|
const hash = createHash3("sha256").update([
|
|
92774
92949
|
"playcademy",
|
|
92775
92950
|
"timeback",
|
|
92776
92951
|
kind,
|
|
92777
92952
|
key.gameId,
|
|
92778
|
-
|
|
92953
|
+
baseCourseId,
|
|
92779
92954
|
key.studentId,
|
|
92780
92955
|
key.runId,
|
|
92781
92956
|
key.activityId,
|
|
@@ -92841,11 +93016,14 @@ var init_timeback_service = __esm(async () => {
|
|
|
92841
93016
|
} : null
|
|
92842
93017
|
};
|
|
92843
93018
|
}
|
|
92844
|
-
static async acquireCompletionReservation(db, key) {
|
|
92845
|
-
|
|
93019
|
+
static async acquireCompletionReservation(db, key, otherCourseIds = []) {
|
|
93020
|
+
function reserve() {
|
|
93021
|
+
return otherCourseIds.length > 0 ? TimebackService.reserveCompletionAcrossCourses(db, key, otherCourseIds) : TimebackService.reserveCompletion(db, key);
|
|
93022
|
+
}
|
|
93023
|
+
let reserveOutcome = await reserve();
|
|
92846
93024
|
for (let settleAttempt = 0;reserveOutcome.status === "in_flight" && settleAttempt < this.COMPLETION_IN_FLIGHT_SETTLE_ATTEMPTS; settleAttempt++) {
|
|
92847
93025
|
await sleep(this.COMPLETION_IN_FLIGHT_SETTLE_DELAY_MS);
|
|
92848
|
-
reserveOutcome = await
|
|
93026
|
+
reserveOutcome = await reserve();
|
|
92849
93027
|
}
|
|
92850
93028
|
if (reserveOutcome.status === "duplicate") {
|
|
92851
93029
|
setAttribute("app.timeback.end_activity_status", "blocked_by_dedupe_guard");
|
|
@@ -92864,8 +93042,24 @@ var init_timeback_service = __esm(async () => {
|
|
|
92864
93042
|
}
|
|
92865
93043
|
return { status: "reserved", token: reserveOutcome.token };
|
|
92866
93044
|
}
|
|
92867
|
-
static async
|
|
92868
|
-
|
|
93045
|
+
static async reserveCompletionAcrossCourses(db, key, otherCourseIds) {
|
|
93046
|
+
return db.transaction(async (tx) => {
|
|
93047
|
+
const database = tx;
|
|
93048
|
+
const runKey = `timeback-completion:${key.gameId.toLowerCase()}:${key.studentId}:${key.runId.toLowerCase()}:${key.activityId}`;
|
|
93049
|
+
await database.execute(sql`SELECT pg_advisory_xact_lock(hashtextextended(${runKey}, 0))`);
|
|
93050
|
+
const onOtherCourse = await database.query.gameTimebackActivityCompletions.findFirst({
|
|
93051
|
+
columns: { id: true },
|
|
93052
|
+
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]))
|
|
93053
|
+
});
|
|
93054
|
+
if (onOtherCourse) {
|
|
93055
|
+
setAttribute("app.timeback.end_activity_status", "completion_reserved_on_other_course");
|
|
93056
|
+
throw new ServiceUnavailableError("An end-activity submission for this run is already in flight — retry shortly");
|
|
93057
|
+
}
|
|
93058
|
+
return this.reserveCompletion(database, key);
|
|
93059
|
+
});
|
|
93060
|
+
}
|
|
93061
|
+
static async runWithCompletionReservation(db, key, emit, awardFromResult, otherCourseIds = []) {
|
|
93062
|
+
const acquired = await this.acquireCompletionReservation(db, key, otherCourseIds);
|
|
92869
93063
|
if (acquired.status === "duplicate") {
|
|
92870
93064
|
return acquired;
|
|
92871
93065
|
}
|
|
@@ -92900,6 +93094,9 @@ var init_timeback_service = __esm(async () => {
|
|
|
92900
93094
|
await db.delete(gameTimebackActivityCompletions).where(and(eq(gameTimebackActivityCompletions.id, token.id), eq(gameTimebackActivityCompletions.reservedAt, token.reservedAt), isNull(gameTimebackActivityCompletions.completedAt)));
|
|
92901
93095
|
}
|
|
92902
93096
|
static addResumeIdToExtensions(extensions, resumeId) {
|
|
93097
|
+
return TimebackService.addPlaycademyExtensions(extensions, { resumeId });
|
|
93098
|
+
}
|
|
93099
|
+
static addPlaycademyExtensions(extensions, fields) {
|
|
92903
93100
|
const base = extensions ?? {};
|
|
92904
93101
|
const existingPlaycademy = base.playcademy;
|
|
92905
93102
|
const playcademy = typeof existingPlaycademy === "object" && existingPlaycademy !== null && !Array.isArray(existingPlaycademy) ? existingPlaycademy : {};
|
|
@@ -92907,28 +93104,71 @@ var init_timeback_service = __esm(async () => {
|
|
|
92907
93104
|
...base,
|
|
92908
93105
|
playcademy: {
|
|
92909
93106
|
...playcademy,
|
|
92910
|
-
|
|
93107
|
+
...fields
|
|
92911
93108
|
}
|
|
92912
93109
|
};
|
|
92913
93110
|
}
|
|
92914
|
-
|
|
92915
|
-
|
|
92916
|
-
|
|
92917
|
-
|
|
92918
|
-
|
|
92919
|
-
|
|
93111
|
+
async resolveRuntimeCourse({
|
|
93112
|
+
gameId,
|
|
93113
|
+
studentId,
|
|
93114
|
+
runId,
|
|
93115
|
+
activityData,
|
|
93116
|
+
extensions
|
|
93117
|
+
}) {
|
|
93118
|
+
const integrations = await this.deps.db.query.gameTimebackIntegrations.findMany({
|
|
93119
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, activityData.grade), eq(gameTimebackIntegrations.subject, activityData.subject), isActiveGameTimebackIntegrationStatus())
|
|
93120
|
+
});
|
|
93121
|
+
const base = integrations.find(isBaseGameTimebackIntegrationRow);
|
|
93122
|
+
if (!base) {
|
|
93123
|
+
throw new NotFoundError(`Timeback integration for game (grade ${activityData.grade}, subject ${activityData.subject})`);
|
|
92920
93124
|
}
|
|
93125
|
+
const holeFilling = integrations.filter((integration) => !isBaseGameTimebackIntegrationRow(integration));
|
|
93126
|
+
const recordedCourseId = holeFilling.length > 0 && runId ? await this.findRecordedRunCourse({
|
|
93127
|
+
gameId,
|
|
93128
|
+
studentId,
|
|
93129
|
+
runId,
|
|
93130
|
+
activityId: activityData.activityId,
|
|
93131
|
+
courseIds: integrations.map((i2) => i2.courseId)
|
|
93132
|
+
}) : undefined;
|
|
93133
|
+
const { integration, requestedCourseIdStatus } = resolveRuntimeAttribution(base, holeFilling, activityData.courseId, recordedCourseId);
|
|
93134
|
+
const hintUsed = activityData.courseId === integration.courseId;
|
|
93135
|
+
const enrollmentId = hintUsed ? activityData.enrollmentId : undefined;
|
|
93136
|
+
const isHoleFilling = !isBaseGameTimebackIntegrationRow(integration);
|
|
92921
93137
|
return {
|
|
92922
|
-
|
|
92923
|
-
|
|
93138
|
+
integration,
|
|
93139
|
+
courseId: integration.courseId,
|
|
93140
|
+
baseCourseId: base.courseId,
|
|
93141
|
+
otherCourseIds: holeFilling.length > 0 ? integrations.map((other) => other.courseId).filter((courseId) => courseId !== integration.courseId) : [],
|
|
93142
|
+
courseName: activityData.courseId === undefined || hintUsed ? activityData.courseName : undefined,
|
|
93143
|
+
extensions: isHoleFilling ? TimebackService.addPlaycademyExtensions(extensions, {
|
|
93144
|
+
courseKind: HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND,
|
|
93145
|
+
baseCourseId: base.courseId,
|
|
93146
|
+
...enrollmentId && { enrollmentId }
|
|
93147
|
+
}) : extensions,
|
|
92924
93148
|
attributes: {
|
|
92925
|
-
"app.timeback.course_id":
|
|
92926
|
-
"app.timeback.resolved_course_id":
|
|
92927
|
-
"app.timeback.requested_course_id":
|
|
92928
|
-
"app.timeback.requested_course_id_status": requestedCourseIdStatus
|
|
93149
|
+
"app.timeback.course_id": integration.courseId,
|
|
93150
|
+
"app.timeback.resolved_course_id": integration.courseId,
|
|
93151
|
+
"app.timeback.requested_course_id": activityData.courseId,
|
|
93152
|
+
"app.timeback.requested_course_id_status": requestedCourseIdStatus,
|
|
93153
|
+
"app.timeback.course_kind": integration.kind,
|
|
93154
|
+
"app.timeback.enrollment_id": enrollmentId
|
|
92929
93155
|
}
|
|
92930
93156
|
};
|
|
92931
93157
|
}
|
|
93158
|
+
async findRecordedRunCourse({
|
|
93159
|
+
gameId,
|
|
93160
|
+
studentId,
|
|
93161
|
+
runId,
|
|
93162
|
+
activityId,
|
|
93163
|
+
courseIds
|
|
93164
|
+
}) {
|
|
93165
|
+
const recorded = await this.deps.db.query.gameTimebackActivityCompletions.findFirst({
|
|
93166
|
+
columns: { courseId: true },
|
|
93167
|
+
where: and(eq(gameTimebackActivityCompletions.gameId, gameId), eq(gameTimebackActivityCompletions.studentId, studentId), eq(gameTimebackActivityCompletions.runId, runId), eq(gameTimebackActivityCompletions.activityId, activityId), inArray(gameTimebackActivityCompletions.courseId, courseIds)),
|
|
93168
|
+
orderBy: (table, { asc }) => [asc(table.createdAt)]
|
|
93169
|
+
});
|
|
93170
|
+
return recorded?.courseId;
|
|
93171
|
+
}
|
|
92932
93172
|
static recordRuntimeContext({
|
|
92933
93173
|
operation,
|
|
92934
93174
|
studentId,
|
|
@@ -93054,19 +93294,163 @@ var init_timeback_service = __esm(async () => {
|
|
|
93054
93294
|
}
|
|
93055
93295
|
async getUserEnrollments(userId, gameId) {
|
|
93056
93296
|
return this.withClientTelemetry(async () => {
|
|
93057
|
-
const
|
|
93058
|
-
|
|
93059
|
-
|
|
93060
|
-
|
|
93061
|
-
|
|
93062
|
-
|
|
93297
|
+
const enrollments = await this.fetchEnrollments(await this.requireTimebackId(userId));
|
|
93298
|
+
return gameId ? filterEnrollmentsByGame(enrollments, gameId) : enrollments;
|
|
93299
|
+
});
|
|
93300
|
+
}
|
|
93301
|
+
async requireTimebackId(userId) {
|
|
93302
|
+
const userData = await this.deps.db.query.users.findFirst({
|
|
93303
|
+
where: eq(users.id, userId),
|
|
93304
|
+
columns: { id: true, timebackId: true }
|
|
93305
|
+
});
|
|
93306
|
+
if (!userData) {
|
|
93307
|
+
throw new NotFoundError("User", userId);
|
|
93308
|
+
}
|
|
93309
|
+
if (!userData.timebackId) {
|
|
93310
|
+
throw new NotFoundError("Timeback account not found for user");
|
|
93311
|
+
}
|
|
93312
|
+
return userData.timebackId;
|
|
93313
|
+
}
|
|
93314
|
+
async getHoleFillingContext(userId, gameId) {
|
|
93315
|
+
return this.withClientTelemetry(async () => {
|
|
93316
|
+
setAttribute("app.timeback.operation", "get_hole_filling_context");
|
|
93317
|
+
const studentId = await this.requireTimebackId(userId);
|
|
93318
|
+
const enrollments = filterEnrollmentsByGame(await this.fetchEnrollments(studentId), gameId);
|
|
93319
|
+
const holeFilling = enrollments.filter((enrollment) => enrollment.kind === HOLE_FILLING_GAME_TIMEBACK_INTEGRATION_KIND);
|
|
93320
|
+
setAttribute("app.timeback.hole_filling_enrollment_count", holeFilling.length);
|
|
93321
|
+
if (holeFilling.length === 0) {
|
|
93322
|
+
return { contexts: [] };
|
|
93063
93323
|
}
|
|
93064
|
-
|
|
93065
|
-
|
|
93324
|
+
const contexts = await Promise.all(holeFilling.map((enrollment) => this.buildHoleFillingContext(studentId, enrollment)));
|
|
93325
|
+
return { contexts };
|
|
93326
|
+
});
|
|
93327
|
+
}
|
|
93328
|
+
async buildHoleFillingContext(studentId, enrollment) {
|
|
93329
|
+
const client = this.requireClient();
|
|
93330
|
+
const grade = enrollment.grade;
|
|
93331
|
+
const subject = enrollment.subject;
|
|
93332
|
+
const enrollmentContext = {
|
|
93333
|
+
courseId: enrollment.courseId,
|
|
93334
|
+
baseCourseId: enrollment.baseCourseId ?? "",
|
|
93335
|
+
...enrollment.id && { enrollmentId: enrollment.id },
|
|
93336
|
+
grade,
|
|
93337
|
+
subject,
|
|
93338
|
+
beginDate: enrollment.beginDate ?? null
|
|
93339
|
+
};
|
|
93340
|
+
const stampedAssignmentId = assignmentIdOf({ metadata: enrollment.enrollmentMetadata });
|
|
93341
|
+
let parent = null;
|
|
93342
|
+
if (stampedAssignmentId) {
|
|
93343
|
+
const where = {
|
|
93344
|
+
...buildAssignmentResultWhere({
|
|
93345
|
+
studentId,
|
|
93346
|
+
assignmentId: stampedAssignmentId,
|
|
93347
|
+
resultType: "assessment"
|
|
93348
|
+
}),
|
|
93349
|
+
scoreStatus: SCORE_STATUS2.fullyGraded
|
|
93350
|
+
};
|
|
93351
|
+
const page = await client.api.oneroster.assessmentResults.list({
|
|
93352
|
+
limit: 1,
|
|
93353
|
+
where,
|
|
93354
|
+
sort: "scoreDate",
|
|
93355
|
+
orderBy: "desc"
|
|
93356
|
+
});
|
|
93357
|
+
parent = page.data[0] ?? null;
|
|
93358
|
+
}
|
|
93359
|
+
const summary = parent ? toTriggeringResult(parent) : null;
|
|
93360
|
+
let trigger = "not_stamped";
|
|
93361
|
+
if (stampedAssignmentId) {
|
|
93362
|
+
trigger = summary ? "found" : "not_found";
|
|
93363
|
+
}
|
|
93364
|
+
setAttributes({
|
|
93365
|
+
"app.timeback.hole_filling_trigger": trigger,
|
|
93366
|
+
"app.timeback.hole_filling_assignment_id": stampedAssignmentId
|
|
93367
|
+
});
|
|
93368
|
+
if (!parent || !summary) {
|
|
93369
|
+
return {
|
|
93370
|
+
enrollment: enrollmentContext,
|
|
93371
|
+
triggeringResult: null,
|
|
93372
|
+
missedStandards: [],
|
|
93373
|
+
untaggedMissedQuestionNumbers: []
|
|
93374
|
+
};
|
|
93375
|
+
}
|
|
93376
|
+
const cacheKey = `${enrollment.courseId}:${studentId}:${summary.resultId}`;
|
|
93377
|
+
const cached = this.holeFillingContextCache.get(cacheKey);
|
|
93378
|
+
if (cached && Date.now() - cached.at < TimebackService.HOLE_FILLING_CONTEXT_TTL_MS) {
|
|
93379
|
+
return { ...cached.context, enrollment: enrollmentContext };
|
|
93380
|
+
}
|
|
93381
|
+
const { context, complete } = await this.resolveMissedStandards(studentId, parent, summary, enrollmentContext);
|
|
93382
|
+
setAttribute("app.timeback.hole_filling_standards_complete", complete);
|
|
93383
|
+
if (complete) {
|
|
93384
|
+
setBounded(this.holeFillingContextCache, cacheKey, { at: Date.now(), context }, TimebackService.HOLE_FILLING_CONTEXT_CACHE_SIZE);
|
|
93385
|
+
}
|
|
93386
|
+
return context;
|
|
93387
|
+
}
|
|
93388
|
+
async resolveMissedStandards(studentId, parent, summary, enrollmentContext) {
|
|
93389
|
+
const client = this.requireClient();
|
|
93390
|
+
let complete = true;
|
|
93391
|
+
let missed = missedQuestionsFromInlineResponses(parent);
|
|
93392
|
+
if (!missed) {
|
|
93393
|
+
const where = buildAssignmentResultWhere({
|
|
93394
|
+
studentId,
|
|
93395
|
+
assignmentId: summary.assignmentId,
|
|
93396
|
+
resultType: "question"
|
|
93397
|
+
});
|
|
93398
|
+
const questionResults = await client.api.oneroster.assessmentResults.listAll({
|
|
93399
|
+
limit: 200,
|
|
93400
|
+
where,
|
|
93401
|
+
sort: "scoreDate",
|
|
93402
|
+
orderBy: "desc"
|
|
93403
|
+
});
|
|
93404
|
+
missed = missedQuestionsFromQuestionResults(questionResults);
|
|
93405
|
+
}
|
|
93406
|
+
let qtiTestId = "";
|
|
93407
|
+
let qtiQuestions = [];
|
|
93408
|
+
const standardsByItemId = new Map;
|
|
93409
|
+
if (missed.length > 0 && !client.hasQti) {
|
|
93410
|
+
complete = false;
|
|
93411
|
+
missed = missed.map(({ questionNumber }) => ({ questionNumber }));
|
|
93412
|
+
} else if (missed.some((question) => !question.qtiItemId)) {
|
|
93413
|
+
const lineItem = await client.api.oneroster.assessmentLineItems.get(summary.lineItemId).catch(() => null);
|
|
93414
|
+
qtiTestId = lineItem ? await resolveQtiTestId(client, resolveGradeLevelTestName(parent, lineItem), lineItem) : "";
|
|
93415
|
+
complete &&= Boolean(qtiTestId);
|
|
93416
|
+
if (qtiTestId) {
|
|
93417
|
+
qtiQuestions = await client.qtiApi.assessmentTests.getQuestions(qtiTestId).then((response) => response.questions ?? []).catch(() => {
|
|
93418
|
+
complete = false;
|
|
93419
|
+
return [];
|
|
93420
|
+
});
|
|
93421
|
+
for (const question of qtiQuestions) {
|
|
93422
|
+
const id = question.question.identifier || question.reference.identifier;
|
|
93423
|
+
if (id) {
|
|
93424
|
+
standardsByItemId.set(id, standardsFromQtiItem(question.question));
|
|
93425
|
+
}
|
|
93426
|
+
}
|
|
93066
93427
|
}
|
|
93067
|
-
|
|
93068
|
-
|
|
93428
|
+
}
|
|
93429
|
+
const itemIdsToFetch = [
|
|
93430
|
+
...new Set(missed.flatMap((question) => question.qtiItemId && !standardsByItemId.has(question.qtiItemId) ? [question.qtiItemId] : []))
|
|
93431
|
+
];
|
|
93432
|
+
const items = await Promise.all(itemIdsToFetch.map((id) => client.qtiApi.assessmentItems.get(id).then((item) => [id, standardsFromQtiItem(item)]).catch(() => {
|
|
93433
|
+
complete = false;
|
|
93434
|
+
return [id, []];
|
|
93435
|
+
})));
|
|
93436
|
+
for (const [id, standards] of items) {
|
|
93437
|
+
standardsByItemId.set(id, standards);
|
|
93438
|
+
}
|
|
93439
|
+
const { missedStandards, untaggedQuestionNumbers } = groupMissedStandards(missed, standardsByItemId, qtiItemIdForQuestionNumber(qtiQuestions));
|
|
93440
|
+
setAttributes({
|
|
93441
|
+
"app.timeback.hole_filling_missed_question_count": missed.length,
|
|
93442
|
+
"app.timeback.hole_filling_missed_standard_count": missedStandards.length,
|
|
93443
|
+
"app.timeback.hole_filling_untagged_question_count": untaggedQuestionNumbers.length
|
|
93069
93444
|
});
|
|
93445
|
+
return {
|
|
93446
|
+
context: {
|
|
93447
|
+
enrollment: enrollmentContext,
|
|
93448
|
+
triggeringResult: { ...summary, ...qtiTestId && { qtiTestId } },
|
|
93449
|
+
missedStandards,
|
|
93450
|
+
untaggedMissedQuestionNumbers: untaggedQuestionNumbers
|
|
93451
|
+
},
|
|
93452
|
+
complete
|
|
93453
|
+
};
|
|
93070
93454
|
}
|
|
93071
93455
|
async getUserDataByTimebackId(timebackId) {
|
|
93072
93456
|
return this.withClientTelemetry(async () => {
|
|
@@ -93910,17 +94294,21 @@ var init_timeback_service = __esm(async () => {
|
|
|
93910
94294
|
activityData
|
|
93911
94295
|
});
|
|
93912
94296
|
await this.deps.validateDeveloperAccess(user, gameId);
|
|
93913
|
-
const integration = await db.query.gameTimebackIntegrations.findFirst({
|
|
93914
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, activityData.grade), eq(gameTimebackIntegrations.subject, activityData.subject), isActiveBaseGameTimebackIntegration())
|
|
93915
|
-
});
|
|
93916
|
-
if (!integration) {
|
|
93917
|
-
throw new NotFoundError(`Timeback integration for game (grade ${activityData.grade}, subject ${activityData.subject})`);
|
|
93918
|
-
}
|
|
93919
94297
|
const {
|
|
94298
|
+
integration,
|
|
93920
94299
|
courseId: runtimeCourseId,
|
|
94300
|
+
baseCourseId,
|
|
94301
|
+
otherCourseIds,
|
|
93921
94302
|
courseName: runtimeCourseName,
|
|
94303
|
+
extensions: runtimeExtensions,
|
|
93922
94304
|
attributes: courseAttributes
|
|
93923
|
-
} =
|
|
94305
|
+
} = await this.resolveRuntimeCourse({
|
|
94306
|
+
gameId,
|
|
94307
|
+
studentId,
|
|
94308
|
+
runId,
|
|
94309
|
+
activityData,
|
|
94310
|
+
extensions: extensionsWithResumeId
|
|
94311
|
+
});
|
|
93924
94312
|
const scorePercentage = scoreData.totalQuestions > 0 ? scoreData.correctQuestions / scoreData.totalQuestions * 100 : 0;
|
|
93925
94313
|
const dedupeKey = runId !== undefined ? {
|
|
93926
94314
|
gameId,
|
|
@@ -93949,12 +94337,12 @@ var init_timeback_service = __esm(async () => {
|
|
|
93949
94337
|
courseId: runtimeCourseId,
|
|
93950
94338
|
courseName: runtimeCourseName,
|
|
93951
94339
|
studentEmail: activityData.studentEmail,
|
|
93952
|
-
extensions:
|
|
94340
|
+
extensions: runtimeExtensions,
|
|
93953
94341
|
...runId ? { runId } : {}
|
|
93954
94342
|
};
|
|
93955
|
-
const completionEventId = dedupeKey ? TimebackService.buildCompletionCaliperEventId(dedupeKey, "completion") : undefined;
|
|
93956
|
-
const sessionEndEventId = dedupeKey ? TimebackService.buildCompletionCaliperEventId(dedupeKey, "session-end", effectiveResumeId) : undefined;
|
|
93957
|
-
const completionHistoryEventId = dedupeKey ? TimebackService.buildCompletionCaliperEventId(dedupeKey, "course-completed") : undefined;
|
|
94343
|
+
const completionEventId = dedupeKey ? TimebackService.buildCompletionCaliperEventId(dedupeKey, "completion", baseCourseId) : undefined;
|
|
94344
|
+
const sessionEndEventId = dedupeKey ? TimebackService.buildCompletionCaliperEventId(dedupeKey, "session-end", baseCourseId, effectiveResumeId) : undefined;
|
|
94345
|
+
const completionHistoryEventId = dedupeKey ? TimebackService.buildCompletionCaliperEventId(dedupeKey, "course-completed", baseCourseId) : undefined;
|
|
93958
94346
|
function emitRecord() {
|
|
93959
94347
|
return client.activity.record(runtimeCourseId, studentId, {
|
|
93960
94348
|
...activityEventContext,
|
|
@@ -93978,7 +94366,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
93978
94366
|
xpAwarded: emitted.xpAwarded,
|
|
93979
94367
|
masteredUnitsApplied: emitted.masteredUnitsApplied,
|
|
93980
94368
|
pctCompleteApp: emitted.pctCompleteApp ?? null
|
|
93981
|
-
}));
|
|
94369
|
+
}), otherCourseIds);
|
|
93982
94370
|
if (outcome.status === "duplicate") {
|
|
93983
94371
|
blockedByDedupeGuard = true;
|
|
93984
94372
|
duplicateAward = outcome.award;
|
|
@@ -94026,6 +94414,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
94026
94414
|
return {
|
|
94027
94415
|
status: "ok",
|
|
94028
94416
|
courseId: runtimeCourseId,
|
|
94417
|
+
courseKind: integration.kind,
|
|
94029
94418
|
xpAwarded: duplicateAward?.xpAwarded ?? 0,
|
|
94030
94419
|
masteredUnits: duplicateAward?.masteredUnitsApplied ?? 0,
|
|
94031
94420
|
...duplicateAward?.pctCompleteApp != null ? { pctCompleteApp: duplicateAward.pctCompleteApp } : {},
|
|
@@ -94038,6 +94427,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
94038
94427
|
return {
|
|
94039
94428
|
status: "ok",
|
|
94040
94429
|
courseId: runtimeCourseId,
|
|
94430
|
+
courseKind: integration.kind,
|
|
94041
94431
|
xpAwarded: result.xpAwarded,
|
|
94042
94432
|
masteredUnits: result.masteredUnitsApplied,
|
|
94043
94433
|
pctCompleteApp: result.pctCompleteApp,
|
|
@@ -94214,7 +94604,6 @@ var init_timeback_service = __esm(async () => {
|
|
|
94214
94604
|
}) {
|
|
94215
94605
|
return this.withClientTelemetry(async () => {
|
|
94216
94606
|
const client = this.requireClient();
|
|
94217
|
-
const db = this.deps.db;
|
|
94218
94607
|
const hasWindowStartedAtMs = windowStartedAtMs !== undefined;
|
|
94219
94608
|
const hasWindowSequence = windowSequence !== undefined;
|
|
94220
94609
|
if (hasWindowStartedAtMs === hasWindowSequence) {
|
|
@@ -94242,17 +94631,17 @@ var init_timeback_service = __esm(async () => {
|
|
|
94242
94631
|
return inFlightHeartbeat;
|
|
94243
94632
|
}
|
|
94244
94633
|
const pendingHeartbeat = (async () => {
|
|
94245
|
-
const integration = await db.query.gameTimebackIntegrations.findFirst({
|
|
94246
|
-
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.grade, activityData.grade), eq(gameTimebackIntegrations.subject, activityData.subject), isActiveBaseGameTimebackIntegration())
|
|
94247
|
-
});
|
|
94248
|
-
if (!integration) {
|
|
94249
|
-
throw new NotFoundError(`Timeback integration for game (grade ${activityData.grade}, subject ${activityData.subject})`);
|
|
94250
|
-
}
|
|
94251
94634
|
const {
|
|
94252
94635
|
courseId: runtimeCourseId,
|
|
94253
94636
|
courseName: runtimeCourseName,
|
|
94637
|
+
extensions: runtimeExtensions,
|
|
94254
94638
|
attributes: courseAttributes
|
|
94255
|
-
} =
|
|
94639
|
+
} = await this.resolveRuntimeCourse({
|
|
94640
|
+
gameId,
|
|
94641
|
+
studentId,
|
|
94642
|
+
activityData,
|
|
94643
|
+
extensions: TimebackService.addResumeIdToExtensions(undefined, effectiveResumeId)
|
|
94644
|
+
});
|
|
94256
94645
|
const activeTimeSeconds = timingData.activeMs / 1000;
|
|
94257
94646
|
const inactiveTimeSeconds = timingData.pausedMs / 1000;
|
|
94258
94647
|
if (activeTimeSeconds > 0 || inactiveTimeSeconds > 0) {
|
|
@@ -94268,7 +94657,7 @@ var init_timeback_service = __esm(async () => {
|
|
|
94268
94657
|
courseId: runtimeCourseId,
|
|
94269
94658
|
courseName: runtimeCourseName,
|
|
94270
94659
|
studentEmail: activityData.studentEmail,
|
|
94271
|
-
extensions:
|
|
94660
|
+
extensions: runtimeExtensions,
|
|
94272
94661
|
...runId ? { runId } : {}
|
|
94273
94662
|
});
|
|
94274
94663
|
}
|
|
@@ -96621,33 +97010,33 @@ var init_auth_provider = __esm(async () => {
|
|
|
96621
97010
|
function createSandboxCacheProvider() {
|
|
96622
97011
|
return {
|
|
96623
97012
|
async get(key) {
|
|
96624
|
-
const entry =
|
|
97013
|
+
const entry = cache2.get(key);
|
|
96625
97014
|
if (!entry) {
|
|
96626
97015
|
return null;
|
|
96627
97016
|
}
|
|
96628
97017
|
if (entry.expiresAt < Date.now()) {
|
|
96629
|
-
|
|
97018
|
+
cache2.delete(key);
|
|
96630
97019
|
return null;
|
|
96631
97020
|
}
|
|
96632
97021
|
return entry.value;
|
|
96633
97022
|
},
|
|
96634
97023
|
async set(key, value, ttlSeconds = 300) {
|
|
96635
|
-
|
|
97024
|
+
cache2.set(key, {
|
|
96636
97025
|
value,
|
|
96637
97026
|
expiresAt: Date.now() + ttlSeconds * 1000
|
|
96638
97027
|
});
|
|
96639
97028
|
},
|
|
96640
97029
|
async delete(key) {
|
|
96641
|
-
|
|
97030
|
+
cache2.delete(key);
|
|
96642
97031
|
}
|
|
96643
97032
|
};
|
|
96644
97033
|
}
|
|
96645
97034
|
function clearSandboxCache() {
|
|
96646
|
-
|
|
97035
|
+
cache2.clear();
|
|
96647
97036
|
}
|
|
96648
|
-
var
|
|
97037
|
+
var cache2;
|
|
96649
97038
|
var init_cache_provider = __esm(() => {
|
|
96650
|
-
|
|
97039
|
+
cache2 = new Map;
|
|
96651
97040
|
});
|
|
96652
97041
|
|
|
96653
97042
|
// ../utils/src/keys.ts
|
|
@@ -152875,7 +153264,7 @@ function parseQtiLibraryParams(searchParams) {
|
|
|
152875
153264
|
limit
|
|
152876
153265
|
};
|
|
152877
153266
|
}
|
|
152878
|
-
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;
|
|
153267
|
+
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;
|
|
152879
153268
|
var init_timeback_controller = __esm(() => {
|
|
152880
153269
|
init_esm();
|
|
152881
153270
|
init_src();
|
|
@@ -152901,6 +153290,12 @@ var init_timeback_controller = __esm(() => {
|
|
|
152901
153290
|
});
|
|
152902
153291
|
getUser = requireNonAnonymous(async (ctx) => ctx.services.timeback.getUserData(ctx.user.id, ctx.gameId));
|
|
152903
153292
|
getUserEnrollments = requireNonAnonymous(async (ctx) => ctx.services.timeback.getUserEnrollments(ctx.user.id, ctx.gameId));
|
|
153293
|
+
getUserHoleFillingContext = requireNonAnonymous(async (ctx) => {
|
|
153294
|
+
if (!ctx.gameId) {
|
|
153295
|
+
throw ApiError.forbidden("Hole-filling context requires a game token");
|
|
153296
|
+
}
|
|
153297
|
+
return ctx.services.timeback.getHoleFillingContext(ctx.user.id, ctx.gameId);
|
|
153298
|
+
});
|
|
152904
153299
|
getUserById = requireNonAnonymous(async (ctx) => {
|
|
152905
153300
|
const timebackId = ctx.params.timebackId;
|
|
152906
153301
|
if (!timebackId) {
|
|
@@ -153754,6 +154149,7 @@ var init_timeback_controller = __esm(() => {
|
|
|
153754
154149
|
populateStudent,
|
|
153755
154150
|
getUser,
|
|
153756
154151
|
getUserEnrollments,
|
|
154152
|
+
getUserHoleFillingContext,
|
|
153757
154153
|
getUserById,
|
|
153758
154154
|
setupIntegration,
|
|
153759
154155
|
getIntegrations,
|
|
@@ -154816,6 +155212,12 @@ var init_timeback8 = __esm(async () => {
|
|
|
154816
155212
|
}
|
|
154817
155213
|
return handle2(timeback2.getUserEnrollments)(c);
|
|
154818
155214
|
});
|
|
155215
|
+
timebackRouter.get("/user/hole-filling-context", async (c) => {
|
|
155216
|
+
if (shouldMockTimeback()) {
|
|
155217
|
+
return c.json({ contexts: [] });
|
|
155218
|
+
}
|
|
155219
|
+
return handle2(timeback2.getUserHoleFillingContext)(c);
|
|
155220
|
+
});
|
|
154819
155221
|
timebackRouter.get("/user/:timebackId", async (c) => {
|
|
154820
155222
|
const user = c.get("user");
|
|
154821
155223
|
if (!user) {
|