@playcademy/sandbox 0.4.2-beta.2 → 0.4.2-beta.3
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 +1025 -198
- package/dist/server.js +1025 -198
- package/package.json +1 -1
package/dist/server.js
CHANGED
|
@@ -1077,7 +1077,7 @@ var package_default;
|
|
|
1077
1077
|
var init_package = __esm(() => {
|
|
1078
1078
|
package_default = {
|
|
1079
1079
|
name: "@playcademy/sandbox",
|
|
1080
|
-
version: "0.4.2-beta.
|
|
1080
|
+
version: "0.4.2-beta.3",
|
|
1081
1081
|
description: "Local development server for Playcademy game development",
|
|
1082
1082
|
type: "module",
|
|
1083
1083
|
exports: {
|
|
@@ -11319,10 +11319,11 @@ var init_table6 = __esm(() => {
|
|
|
11319
11319
|
});
|
|
11320
11320
|
|
|
11321
11321
|
// ../data/src/domains/timeback/table.ts
|
|
11322
|
-
var gameTimebackIntegrations, gameTimebackAssessmentTests;
|
|
11322
|
+
var gameTimebackIntegrations, gameTimebackAssessmentTests, gameTimebackMetricDiscrepancyVerifications;
|
|
11323
11323
|
var init_table7 = __esm(() => {
|
|
11324
11324
|
init_pg_core();
|
|
11325
11325
|
init_table5();
|
|
11326
|
+
init_table3();
|
|
11326
11327
|
gameTimebackIntegrations = pgTable("game_timeback_integrations", {
|
|
11327
11328
|
id: uuid("id").primaryKey().defaultRandom(),
|
|
11328
11329
|
gameId: uuid("game_id").notNull().references(() => games.id, { onDelete: "cascade" }),
|
|
@@ -11347,6 +11348,21 @@ var init_table7 = __esm(() => {
|
|
|
11347
11348
|
}, (table3) => [
|
|
11348
11349
|
uniqueIndex("game_timeback_assessment_tests_integration_qti_idx").on(table3.integrationId, table3.qtiTestIdentifier)
|
|
11349
11350
|
]);
|
|
11351
|
+
gameTimebackMetricDiscrepancyVerifications = pgTable("game_timeback_metric_discrepancy_verifications", {
|
|
11352
|
+
id: uuid("id").primaryKey().defaultRandom(),
|
|
11353
|
+
gameId: uuid("game_id").notNull().references(() => games.id, { onDelete: "cascade" }),
|
|
11354
|
+
courseId: text("course_id").notNull(),
|
|
11355
|
+
studentId: text("student_id").notNull(),
|
|
11356
|
+
runId: uuid("run_id").notNull(),
|
|
11357
|
+
activityId: text("activity_id"),
|
|
11358
|
+
verifiedByUserId: text("verified_by_user_id").references(() => users.id, {
|
|
11359
|
+
onDelete: "set null"
|
|
11360
|
+
}),
|
|
11361
|
+
verifiedAt: timestamp("verified_at", { withTimezone: true }).notNull().defaultNow()
|
|
11362
|
+
}, (table3) => [
|
|
11363
|
+
uniqueIndex("game_timeback_metric_discrepancy_verifications_run_idx").on(table3.gameId, table3.courseId, table3.studentId, table3.runId),
|
|
11364
|
+
index("game_timeback_metric_discrepancy_verifications_course_idx").on(table3.gameId, table3.courseId, table3.verifiedAt)
|
|
11365
|
+
]);
|
|
11350
11366
|
});
|
|
11351
11367
|
|
|
11352
11368
|
// ../data/src/tables.index.ts
|
|
@@ -11360,6 +11376,7 @@ __export(exports_tables_index, {
|
|
|
11360
11376
|
games: () => games,
|
|
11361
11377
|
gameVisibilityEnum: () => gameVisibilityEnum,
|
|
11362
11378
|
gameTypeEnum: () => gameTypeEnum,
|
|
11379
|
+
gameTimebackMetricDiscrepancyVerifications: () => gameTimebackMetricDiscrepancyVerifications,
|
|
11363
11380
|
gameTimebackIntegrations: () => gameTimebackIntegrations,
|
|
11364
11381
|
gameTimebackAssessmentTests: () => gameTimebackAssessmentTests,
|
|
11365
11382
|
gameScoresRelations: () => gameScoresRelations,
|
|
@@ -29363,6 +29380,75 @@ function formatDateYMDInTimezone(timeZone, date3 = new Date) {
|
|
|
29363
29380
|
const d = parts2.find((p) => p.type === "day").value;
|
|
29364
29381
|
return `${y}-${m}-${d}`;
|
|
29365
29382
|
}
|
|
29383
|
+
function getUtcInstantForMidnight(date3, timeZone) {
|
|
29384
|
+
const parts2 = new Intl.DateTimeFormat("en-US", {
|
|
29385
|
+
timeZone,
|
|
29386
|
+
year: "numeric",
|
|
29387
|
+
month: "2-digit",
|
|
29388
|
+
day: "2-digit"
|
|
29389
|
+
}).formatToParts(date3).reduce((acc, p) => {
|
|
29390
|
+
if (p.type !== "literal") {
|
|
29391
|
+
acc[p.type] = p.value;
|
|
29392
|
+
}
|
|
29393
|
+
return acc;
|
|
29394
|
+
}, {});
|
|
29395
|
+
const year = Number(parts2.year);
|
|
29396
|
+
const month = Number(parts2.month);
|
|
29397
|
+
const day = Number(parts2.day);
|
|
29398
|
+
for (let dayOffset = -1;dayOffset <= 1; dayOffset++) {
|
|
29399
|
+
const testYear = year;
|
|
29400
|
+
const testMonth = month;
|
|
29401
|
+
const testDay = day + dayOffset;
|
|
29402
|
+
for (let utcHour = 0;utcHour < 24; utcHour++) {
|
|
29403
|
+
const testDate = new Date(Date.UTC(testYear, testMonth - 1, testDay, utcHour, 0, 0, 0));
|
|
29404
|
+
const testParts = new Intl.DateTimeFormat("en-US", {
|
|
29405
|
+
timeZone,
|
|
29406
|
+
year: "numeric",
|
|
29407
|
+
month: "2-digit",
|
|
29408
|
+
day: "2-digit",
|
|
29409
|
+
hour: "2-digit",
|
|
29410
|
+
minute: "2-digit",
|
|
29411
|
+
hour12: false
|
|
29412
|
+
}).formatToParts(testDate).reduce((acc, p) => {
|
|
29413
|
+
if (p.type !== "literal") {
|
|
29414
|
+
acc[p.type] = p.value;
|
|
29415
|
+
}
|
|
29416
|
+
return acc;
|
|
29417
|
+
}, {});
|
|
29418
|
+
const yearMatch = Number(testParts.year) === year;
|
|
29419
|
+
const monthMatch = Number(testParts.month) === month;
|
|
29420
|
+
const dayMatch = Number(testParts.day) === day;
|
|
29421
|
+
const hourValue = Number(testParts.hour);
|
|
29422
|
+
const hourMatch = hourValue === 0 || hourValue === 24;
|
|
29423
|
+
const minuteMatch = Number(testParts.minute) === 0;
|
|
29424
|
+
if (yearMatch && monthMatch && dayMatch && hourMatch && minuteMatch) {
|
|
29425
|
+
return testDate;
|
|
29426
|
+
}
|
|
29427
|
+
}
|
|
29428
|
+
}
|
|
29429
|
+
throw new Error(`Could not find midnight for ${year}-${String(month).padStart(2, "0")}-${String(day).padStart(2, "0")} in timezone ${timeZone}`);
|
|
29430
|
+
}
|
|
29431
|
+
function getDayBoundariesInTimezone(date3, timezone) {
|
|
29432
|
+
const startOfDay = getUtcInstantForMidnight(date3, timezone);
|
|
29433
|
+
const formatter = new Intl.DateTimeFormat("en-US", {
|
|
29434
|
+
timeZone: timezone,
|
|
29435
|
+
year: "numeric",
|
|
29436
|
+
month: "2-digit",
|
|
29437
|
+
day: "2-digit"
|
|
29438
|
+
});
|
|
29439
|
+
const parts2 = formatter.formatToParts(date3).reduce((acc, p) => {
|
|
29440
|
+
if (p.type !== "literal") {
|
|
29441
|
+
acc[p.type] = p.value;
|
|
29442
|
+
}
|
|
29443
|
+
return acc;
|
|
29444
|
+
}, {});
|
|
29445
|
+
const year = Number(parts2.year);
|
|
29446
|
+
const month = Number(parts2.month);
|
|
29447
|
+
const day = Number(parts2.day);
|
|
29448
|
+
const nextDayNoon = new Date(Date.UTC(year, month - 1, day + 1, 12, 0, 0, 0));
|
|
29449
|
+
const endOfDay = getUtcInstantForMidnight(nextDayNoon, timezone);
|
|
29450
|
+
return { startOfDay, endOfDay };
|
|
29451
|
+
}
|
|
29366
29452
|
// ../utils/src/url.ts
|
|
29367
29453
|
function buildPath(path, params) {
|
|
29368
29454
|
const url = new URL(path, "http://n");
|
|
@@ -29406,6 +29492,42 @@ function formatGradeLabel(grade) {
|
|
|
29406
29492
|
}
|
|
29407
29493
|
}
|
|
29408
29494
|
}
|
|
29495
|
+
function isTimebackDiscrepancyQueueWindow(value) {
|
|
29496
|
+
return typeof value === "string" && TIMEBACK_DISCREPANCY_QUEUE_WINDOW_VALUES.has(value);
|
|
29497
|
+
}
|
|
29498
|
+
function isTimebackDiscrepancyQueueMetric(value) {
|
|
29499
|
+
return typeof value === "string" && TIMEBACK_DISCREPANCY_QUEUE_METRIC_VALUES.has(value);
|
|
29500
|
+
}
|
|
29501
|
+
function parseTimebackDiscrepancyQueueWindow(value) {
|
|
29502
|
+
const window2 = value?.trim();
|
|
29503
|
+
return isTimebackDiscrepancyQueueWindow(window2) ? window2 : DEFAULT_TIMEBACK_DISCREPANCY_QUEUE_WINDOW;
|
|
29504
|
+
}
|
|
29505
|
+
function parseTimebackDiscrepancyQueueMetrics(values) {
|
|
29506
|
+
const metrics = [];
|
|
29507
|
+
const seen = new Set;
|
|
29508
|
+
for (const value of values) {
|
|
29509
|
+
const metric = value.trim();
|
|
29510
|
+
if (isTimebackDiscrepancyQueueMetric(metric) && !seen.has(metric)) {
|
|
29511
|
+
seen.add(metric);
|
|
29512
|
+
metrics.push(metric);
|
|
29513
|
+
}
|
|
29514
|
+
}
|
|
29515
|
+
return metrics;
|
|
29516
|
+
}
|
|
29517
|
+
var TIMEBACK_DISCREPANCY_QUEUE_WINDOWS, TIMEBACK_DISCREPANCY_QUEUE_METRICS, DEFAULT_TIMEBACK_DISCREPANCY_QUEUE_WINDOW = "this-week", TIMEBACK_DISCREPANCY_QUEUE_WINDOW_VALUES, TIMEBACK_DISCREPANCY_QUEUE_METRIC_VALUES;
|
|
29518
|
+
var init_timeback3 = __esm(() => {
|
|
29519
|
+
TIMEBACK_DISCREPANCY_QUEUE_WINDOWS = [
|
|
29520
|
+
"today",
|
|
29521
|
+
"yesterday",
|
|
29522
|
+
"this-week",
|
|
29523
|
+
"last-week",
|
|
29524
|
+
"all",
|
|
29525
|
+
"custom"
|
|
29526
|
+
];
|
|
29527
|
+
TIMEBACK_DISCREPANCY_QUEUE_METRICS = ["xp", "mastery", "time", "score"];
|
|
29528
|
+
TIMEBACK_DISCREPANCY_QUEUE_WINDOW_VALUES = new Set(TIMEBACK_DISCREPANCY_QUEUE_WINDOWS);
|
|
29529
|
+
TIMEBACK_DISCREPANCY_QUEUE_METRIC_VALUES = new Set(TIMEBACK_DISCREPANCY_QUEUE_METRICS);
|
|
29530
|
+
});
|
|
29409
29531
|
|
|
29410
29532
|
// ../../node_modules/.bun/drizzle-zod@0.7.1+e9f18f9688af15ce/node_modules/drizzle-zod/index.mjs
|
|
29411
29533
|
function isColumnType(column2, columnTypes) {
|
|
@@ -29906,7 +30028,7 @@ function isValidAdminAttributionDate(value) {
|
|
|
29906
30028
|
const date3 = new Date(Date.UTC(year, month - 1, day, 12, 0, 0));
|
|
29907
30029
|
return date3.getUTCFullYear() === year && date3.getUTCMonth() + 1 === month && date3.getUTCDate() === day;
|
|
29908
30030
|
}
|
|
29909
|
-
var TIMEBACK_GRADES, TIMEBACK_SUBJECTS4, TimebackGradeSchema, TimebackSubjectSchema, CourseGoalsSchema, UpdateGameTimebackIntegrationRequestSchema, TimebackActivityDataSchema, EndActivityRequestSchema, GameRunMetricsSchema, GameCourseMetricsSchema, GameMetricsResponseSchema, AdvanceCourseRequestSchema, UnenrollCourseRequestSchema, HeartbeatRequestSchema, PopulateStudentRequestSchema, DerivedPlatformCourseConfigSchema, TimebackBaseConfigSchema, PlatformTimebackSetupRequestSchema, AdminTimebackMutationBaseSchema, AdminAttributionDateSchema, ADMIN_GRANT_XP_MIN = -1e5, ADMIN_GRANT_XP_MAX = 1e5, ADMIN_GRANT_XP_AMOUNT_RANGE_MESSAGE, GrantTimebackXpRequestSchema, AdjustTimebackTimeRequestSchema, AdjustTimebackMasteryRequestSchema, ReconcileMasteryForConfigChangeSchema, EnrollStudentRequestSchema, UnenrollStudentRequestSchema, ReactivateEnrollmentRequestSchema, InsertAssessmentTestSchema, CreateAssessmentRequestSchema, ReorderAssessmentsRequestSchema, ReorderQuestionsRequestSchema;
|
|
30031
|
+
var TIMEBACK_GRADES, TIMEBACK_SUBJECTS4, TimebackGradeSchema, TimebackSubjectSchema, CourseGoalsSchema, UpdateGameTimebackIntegrationRequestSchema, TimebackActivityDataSchema, EndActivityRequestSchema, GameRunMetricsSchema, GameCourseMetricsSchema, GameMetricsResponseSchema, AdvanceCourseRequestSchema, UnenrollCourseRequestSchema, HeartbeatRequestSchema, PopulateStudentRequestSchema, DerivedPlatformCourseConfigSchema, TimebackBaseConfigSchema, PlatformTimebackSetupRequestSchema, AdminTimebackMutationBaseSchema, AdminAttributionDateSchema, ADMIN_GRANT_XP_MIN = -1e5, ADMIN_GRANT_XP_MAX = 1e5, ADMIN_GRANT_XP_AMOUNT_RANGE_MESSAGE, GrantTimebackXpRequestSchema, AdjustTimebackTimeRequestSchema, AdjustTimebackMasteryRequestSchema, ReconcileMasteryForConfigChangeSchema, EnrollStudentRequestSchema, UnenrollStudentRequestSchema, ReactivateEnrollmentRequestSchema, VerifyTimebackMetricDiscrepancyRequestSchema, InsertAssessmentTestSchema, CreateAssessmentRequestSchema, ReorderAssessmentsRequestSchema, ReorderQuestionsRequestSchema;
|
|
29910
30032
|
var init_schemas4 = __esm(() => {
|
|
29911
30033
|
init_drizzle_zod();
|
|
29912
30034
|
init_esm();
|
|
@@ -30147,6 +30269,11 @@ var init_schemas4 = __esm(() => {
|
|
|
30147
30269
|
studentId: exports_external.string().min(1),
|
|
30148
30270
|
enrollmentId: exports_external.string().min(1)
|
|
30149
30271
|
});
|
|
30272
|
+
VerifyTimebackMetricDiscrepancyRequestSchema = exports_external.object({
|
|
30273
|
+
studentId: exports_external.string().min(1),
|
|
30274
|
+
runId: exports_external.string().uuid(),
|
|
30275
|
+
activityId: exports_external.string().min(1).optional()
|
|
30276
|
+
});
|
|
30150
30277
|
InsertAssessmentTestSchema = createInsertSchema(gameTimebackAssessmentTests).omit({
|
|
30151
30278
|
id: true,
|
|
30152
30279
|
createdAt: true
|
|
@@ -30279,183 +30406,6 @@ var init_timeback_admin_util = __esm(() => {
|
|
|
30279
30406
|
init_errors();
|
|
30280
30407
|
});
|
|
30281
30408
|
|
|
30282
|
-
// ../api-core/src/utils/timeback-game-metrics-comparison.util.ts
|
|
30283
|
-
function createMetricRow(definition) {
|
|
30284
|
-
const { gameValue, kind, metric, timebackValue, tolerance } = definition;
|
|
30285
|
-
if (timebackValue === undefined && gameValue === undefined) {
|
|
30286
|
-
return null;
|
|
30287
|
-
}
|
|
30288
|
-
if (gameValue === undefined) {
|
|
30289
|
-
return {
|
|
30290
|
-
metric,
|
|
30291
|
-
kind,
|
|
30292
|
-
status: "not_reported_by_game",
|
|
30293
|
-
...timebackValue !== undefined ? { timebackValue } : {}
|
|
30294
|
-
};
|
|
30295
|
-
}
|
|
30296
|
-
if (timebackValue === undefined) {
|
|
30297
|
-
return {
|
|
30298
|
-
metric,
|
|
30299
|
-
kind,
|
|
30300
|
-
status: "not_recorded_by_timeback",
|
|
30301
|
-
gameValue
|
|
30302
|
-
};
|
|
30303
|
-
}
|
|
30304
|
-
const delta = gameValue - timebackValue;
|
|
30305
|
-
const isDiscrepant = tolerance === 0 ? delta !== 0 : Math.abs(delta) >= tolerance;
|
|
30306
|
-
return {
|
|
30307
|
-
metric,
|
|
30308
|
-
kind,
|
|
30309
|
-
status: isDiscrepant ? "discrepant" : "matched",
|
|
30310
|
-
timebackValue,
|
|
30311
|
-
gameValue,
|
|
30312
|
-
delta
|
|
30313
|
-
};
|
|
30314
|
-
}
|
|
30315
|
-
function createRunComparison(activity, gameRun) {
|
|
30316
|
-
const runId = activity.runId ?? "";
|
|
30317
|
-
if (!gameRun) {
|
|
30318
|
-
return {
|
|
30319
|
-
runId,
|
|
30320
|
-
status: "not_reported",
|
|
30321
|
-
discrepancyCount: 0,
|
|
30322
|
-
rows: []
|
|
30323
|
-
};
|
|
30324
|
-
}
|
|
30325
|
-
const rows = [
|
|
30326
|
-
createMetricRow({
|
|
30327
|
-
metric: "xp",
|
|
30328
|
-
kind: "number",
|
|
30329
|
-
timebackValue: activity.xpDelta,
|
|
30330
|
-
gameValue: gameRun.totalXp,
|
|
30331
|
-
tolerance: TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE.xp
|
|
30332
|
-
}),
|
|
30333
|
-
createMetricRow({
|
|
30334
|
-
metric: "mastery",
|
|
30335
|
-
kind: "number",
|
|
30336
|
-
timebackValue: activity.masteredUnitsDelta,
|
|
30337
|
-
gameValue: gameRun.masteredUnits,
|
|
30338
|
-
tolerance: TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE.mastery
|
|
30339
|
-
}),
|
|
30340
|
-
createMetricRow({
|
|
30341
|
-
metric: "time",
|
|
30342
|
-
kind: "time",
|
|
30343
|
-
timebackValue: activity.timeDeltaSeconds,
|
|
30344
|
-
gameValue: gameRun.activeTimeSeconds,
|
|
30345
|
-
tolerance: TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE.time
|
|
30346
|
-
}),
|
|
30347
|
-
createMetricRow({
|
|
30348
|
-
metric: "score",
|
|
30349
|
-
kind: "percent",
|
|
30350
|
-
timebackValue: activity.score,
|
|
30351
|
-
gameValue: gameRun.score,
|
|
30352
|
-
tolerance: TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE.score
|
|
30353
|
-
})
|
|
30354
|
-
].filter((row) => row !== null);
|
|
30355
|
-
const discrepancyCount = rows.filter((row) => row.status === "discrepant").length;
|
|
30356
|
-
return {
|
|
30357
|
-
runId,
|
|
30358
|
-
status: discrepancyCount > 0 ? "discrepant" : "matched",
|
|
30359
|
-
discrepancyCount,
|
|
30360
|
-
rows
|
|
30361
|
-
};
|
|
30362
|
-
}
|
|
30363
|
-
function summarizeGameRunMetricsComparison(comparison) {
|
|
30364
|
-
return {
|
|
30365
|
-
runId: comparison.runId,
|
|
30366
|
-
status: comparison.status,
|
|
30367
|
-
discrepancyCount: comparison.discrepancyCount,
|
|
30368
|
-
...comparison.reason ? { reason: comparison.reason } : {}
|
|
30369
|
-
};
|
|
30370
|
-
}
|
|
30371
|
-
function buildGameRunMetricComparisons(activities, course, response) {
|
|
30372
|
-
const activitiesWithRunIds = activities.filter((activity) => typeof activity.runId === "string" && activity.runId.length > 0);
|
|
30373
|
-
const comparisons = new Map;
|
|
30374
|
-
if (activitiesWithRunIds.length === 0) {
|
|
30375
|
-
return comparisons;
|
|
30376
|
-
}
|
|
30377
|
-
if (!response.supported) {
|
|
30378
|
-
for (const activity of activitiesWithRunIds) {
|
|
30379
|
-
comparisons.set(activity.runId, {
|
|
30380
|
-
runId: activity.runId,
|
|
30381
|
-
status: "unavailable",
|
|
30382
|
-
discrepancyCount: 0,
|
|
30383
|
-
reason: response.reason,
|
|
30384
|
-
rows: []
|
|
30385
|
-
});
|
|
30386
|
-
}
|
|
30387
|
-
return comparisons;
|
|
30388
|
-
}
|
|
30389
|
-
const gameCourseMetrics = response.metrics.courses.find((gameCourse) => gameCourse.grade === course.grade && gameCourse.subject === course.subject);
|
|
30390
|
-
const gameRunsById = new Map(gameCourseMetrics?.activities?.map((gameRun) => [gameRun.runId.toLowerCase(), gameRun]));
|
|
30391
|
-
for (const activity of activitiesWithRunIds) {
|
|
30392
|
-
comparisons.set(activity.runId, createRunComparison(activity, gameRunsById.get(activity.runId.toLowerCase())));
|
|
30393
|
-
}
|
|
30394
|
-
return comparisons;
|
|
30395
|
-
}
|
|
30396
|
-
var init_timeback_game_metrics_comparison_util = __esm(() => {
|
|
30397
|
-
init_src();
|
|
30398
|
-
});
|
|
30399
|
-
|
|
30400
|
-
// ../api-core/src/utils/timeback-mastery-completion.util.ts
|
|
30401
|
-
async function upsertMasteryCompletionEntry(params) {
|
|
30402
|
-
const { client, courseId, studentId, appName, action } = params;
|
|
30403
|
-
const ids = deriveSourcedIds(courseId);
|
|
30404
|
-
const lineItemId = `${ids.course}-mastery-completion-assessment`;
|
|
30405
|
-
const resultId = `${lineItemId}:${studentId}:completion`;
|
|
30406
|
-
if (action === "complete") {
|
|
30407
|
-
await client.oneroster.assessmentLineItems.findOrCreate(lineItemId, {
|
|
30408
|
-
sourcedId: lineItemId,
|
|
30409
|
-
title: "Mastery Completion",
|
|
30410
|
-
status: ONEROSTER_STATUS.active,
|
|
30411
|
-
course: { sourcedId: ids.course },
|
|
30412
|
-
...ids.componentResource ? { componentResource: { sourcedId: ids.componentResource } } : {}
|
|
30413
|
-
});
|
|
30414
|
-
await client.oneroster.assessmentResults.upsert(resultId, {
|
|
30415
|
-
sourcedId: resultId,
|
|
30416
|
-
status: ONEROSTER_STATUS.active,
|
|
30417
|
-
assessmentLineItem: { sourcedId: lineItemId },
|
|
30418
|
-
student: { sourcedId: studentId },
|
|
30419
|
-
score: 100,
|
|
30420
|
-
scoreDate: new Date().toISOString(),
|
|
30421
|
-
scoreStatus: SCORE_STATUS.fullyGraded,
|
|
30422
|
-
inProgress: "false",
|
|
30423
|
-
metadata: {
|
|
30424
|
-
isMasteryCompletion: true,
|
|
30425
|
-
adminAction: true,
|
|
30426
|
-
appName
|
|
30427
|
-
}
|
|
30428
|
-
});
|
|
30429
|
-
} else {
|
|
30430
|
-
try {
|
|
30431
|
-
await client.oneroster.assessmentResults.upsert(resultId, {
|
|
30432
|
-
sourcedId: resultId,
|
|
30433
|
-
status: ONEROSTER_STATUS.active,
|
|
30434
|
-
assessmentLineItem: { sourcedId: lineItemId },
|
|
30435
|
-
student: { sourcedId: studentId },
|
|
30436
|
-
score: 0,
|
|
30437
|
-
scoreDate: new Date().toISOString(),
|
|
30438
|
-
scoreStatus: SCORE_STATUS.notSubmitted,
|
|
30439
|
-
inProgress: "true",
|
|
30440
|
-
metadata: {
|
|
30441
|
-
isMasteryCompletion: true,
|
|
30442
|
-
adminAction: true,
|
|
30443
|
-
appName
|
|
30444
|
-
}
|
|
30445
|
-
});
|
|
30446
|
-
} catch {
|
|
30447
|
-
logger16.debug("No completion entry to revoke", { studentId, courseId });
|
|
30448
|
-
}
|
|
30449
|
-
}
|
|
30450
|
-
}
|
|
30451
|
-
var logger16;
|
|
30452
|
-
var init_timeback_mastery_completion_util = __esm(() => {
|
|
30453
|
-
init_src2();
|
|
30454
|
-
init_constants4();
|
|
30455
|
-
init_utils6();
|
|
30456
|
-
logger16 = log.scope("timeback-mastery-completion");
|
|
30457
|
-
});
|
|
30458
|
-
|
|
30459
30409
|
// ../api-core/src/utils/timeback.util.ts
|
|
30460
30410
|
function isRecord2(value) {
|
|
30461
30411
|
return typeof value === "object" && value !== null;
|
|
@@ -30603,6 +30553,13 @@ function groupCaliperEventsByRun(events) {
|
|
|
30603
30553
|
}
|
|
30604
30554
|
return groups;
|
|
30605
30555
|
}
|
|
30556
|
+
function findCaliperEventGroupContainingExternalId(events, externalId) {
|
|
30557
|
+
const targetExternalId = externalId.trim();
|
|
30558
|
+
if (!targetExternalId) {
|
|
30559
|
+
return;
|
|
30560
|
+
}
|
|
30561
|
+
return [...groupCaliperEventsByRun(events).values()].find((group) => group.some((event) => event.externalId === targetExternalId));
|
|
30562
|
+
}
|
|
30606
30563
|
function mapCaliperEventGroupToActivity(events, relevantCourseIds) {
|
|
30607
30564
|
if (events.length === 0) {
|
|
30608
30565
|
return null;
|
|
@@ -30776,6 +30733,371 @@ var init_timeback_util = __esm(() => {
|
|
|
30776
30733
|
]);
|
|
30777
30734
|
});
|
|
30778
30735
|
|
|
30736
|
+
// ../api-core/src/utils/timeback-discrepancy-queue.util.ts
|
|
30737
|
+
function parseDateInputParts(value) {
|
|
30738
|
+
const parts2 = value.split("-");
|
|
30739
|
+
return {
|
|
30740
|
+
year: Number(parts2[0]),
|
|
30741
|
+
month: Number(parts2[1]),
|
|
30742
|
+
day: Number(parts2[2])
|
|
30743
|
+
};
|
|
30744
|
+
}
|
|
30745
|
+
function addDateInputDays(value, days) {
|
|
30746
|
+
const { year, month, day } = parseDateInputParts(value);
|
|
30747
|
+
const date3 = new Date(Date.UTC(year, month - 1, day + days, 12));
|
|
30748
|
+
return [
|
|
30749
|
+
String(date3.getUTCFullYear()),
|
|
30750
|
+
String(date3.getUTCMonth() + 1).padStart(2, "0"),
|
|
30751
|
+
String(date3.getUTCDate()).padStart(2, "0")
|
|
30752
|
+
].join("-");
|
|
30753
|
+
}
|
|
30754
|
+
function getDateInputDayOfWeek(value) {
|
|
30755
|
+
const { year, month, day } = parseDateInputParts(value);
|
|
30756
|
+
return new Date(Date.UTC(year, month - 1, day, 12)).getUTCDay();
|
|
30757
|
+
}
|
|
30758
|
+
function getDateInputBoundary(value, boundary, timezone2) {
|
|
30759
|
+
const { year, month, day } = parseDateInputParts(value);
|
|
30760
|
+
const anchor = new Date(Date.UTC(year, month - 1, day, 12));
|
|
30761
|
+
const { startOfDay, endOfDay } = getDayBoundariesInTimezone(anchor, timezone2);
|
|
30762
|
+
return boundary === "end" ? new Date(endOfDay.getTime() - 1) : startOfDay;
|
|
30763
|
+
}
|
|
30764
|
+
function getWeekStartDateInput(date3, timezone2) {
|
|
30765
|
+
const today = formatDateYMDInTimezone(timezone2, date3);
|
|
30766
|
+
const dayOfWeek = getDateInputDayOfWeek(today);
|
|
30767
|
+
return addDateInputDays(today, -dayOfWeek);
|
|
30768
|
+
}
|
|
30769
|
+
function parseDateBoundary(value, boundary, timezone2) {
|
|
30770
|
+
if (!value) {
|
|
30771
|
+
return null;
|
|
30772
|
+
}
|
|
30773
|
+
if (DATE_INPUT_RE.test(value)) {
|
|
30774
|
+
return getDateInputBoundary(value, boundary, timezone2);
|
|
30775
|
+
}
|
|
30776
|
+
const parsed = new Date(value);
|
|
30777
|
+
return Number.isNaN(parsed.getTime()) ? null : parsed;
|
|
30778
|
+
}
|
|
30779
|
+
function getTimebackDiscrepancyQueueDateRange(options) {
|
|
30780
|
+
const now2 = options.now ?? new Date;
|
|
30781
|
+
const timezone2 = options.timezone ?? "UTC";
|
|
30782
|
+
if (options.window === "all") {
|
|
30783
|
+
return {};
|
|
30784
|
+
}
|
|
30785
|
+
if (options.window === "today") {
|
|
30786
|
+
return { startDate: getDayBoundariesInTimezone(now2, timezone2).startOfDay.toISOString() };
|
|
30787
|
+
}
|
|
30788
|
+
if (options.window === "yesterday") {
|
|
30789
|
+
const yesterdayInput = addDateInputDays(formatDateYMDInTimezone(timezone2, now2), -1);
|
|
30790
|
+
return {
|
|
30791
|
+
startDate: getDateInputBoundary(yesterdayInput, "start", timezone2).toISOString(),
|
|
30792
|
+
endDate: getDateInputBoundary(yesterdayInput, "end", timezone2).toISOString()
|
|
30793
|
+
};
|
|
30794
|
+
}
|
|
30795
|
+
if (options.window === "this-week") {
|
|
30796
|
+
return {
|
|
30797
|
+
startDate: getDateInputBoundary(getWeekStartDateInput(now2, timezone2), "start", timezone2).toISOString()
|
|
30798
|
+
};
|
|
30799
|
+
}
|
|
30800
|
+
if (options.window === "last-week") {
|
|
30801
|
+
const thisWeekStartInput = getWeekStartDateInput(now2, timezone2);
|
|
30802
|
+
const lastWeekStartInput = addDateInputDays(thisWeekStartInput, -7);
|
|
30803
|
+
const lastWeekEndInput = addDateInputDays(thisWeekStartInput, -1);
|
|
30804
|
+
return {
|
|
30805
|
+
startDate: getDateInputBoundary(lastWeekStartInput, "start", timezone2).toISOString(),
|
|
30806
|
+
endDate: getDateInputBoundary(lastWeekEndInput, "end", timezone2).toISOString()
|
|
30807
|
+
};
|
|
30808
|
+
}
|
|
30809
|
+
const startDate = parseDateBoundary(options.startDate, "start", timezone2);
|
|
30810
|
+
const endDate = parseDateBoundary(options.endDate, "end", timezone2);
|
|
30811
|
+
return {
|
|
30812
|
+
...startDate ? { startDate: startDate.toISOString() } : {},
|
|
30813
|
+
...endDate ? { endDate: endDate.toISOString() } : {}
|
|
30814
|
+
};
|
|
30815
|
+
}
|
|
30816
|
+
function getCaliperActorSourcedId(event) {
|
|
30817
|
+
const actorId = typeof event.actor.id === "string" ? event.actor.id.trim() : "";
|
|
30818
|
+
if (!actorId) {
|
|
30819
|
+
return;
|
|
30820
|
+
}
|
|
30821
|
+
const normalized = actorId.replace(/\/$/, "");
|
|
30822
|
+
const segment = normalized.split("/").at(-1);
|
|
30823
|
+
if (!segment) {
|
|
30824
|
+
return;
|
|
30825
|
+
}
|
|
30826
|
+
try {
|
|
30827
|
+
return decodeURIComponent(segment);
|
|
30828
|
+
} catch {
|
|
30829
|
+
return segment;
|
|
30830
|
+
}
|
|
30831
|
+
}
|
|
30832
|
+
function getTimebackDiscrepancyVerificationKey(params) {
|
|
30833
|
+
return `${params.studentId}:${params.runId.toLowerCase()}`;
|
|
30834
|
+
}
|
|
30835
|
+
function getTimebackMetricDiscrepancyVerificationForActivity(params) {
|
|
30836
|
+
if (params.comparison?.status !== "discrepant" || !params.activity.runId) {
|
|
30837
|
+
return;
|
|
30838
|
+
}
|
|
30839
|
+
return params.verificationsByKey.get(getTimebackDiscrepancyVerificationKey({
|
|
30840
|
+
studentId: params.studentId,
|
|
30841
|
+
runId: params.activity.runId
|
|
30842
|
+
}));
|
|
30843
|
+
}
|
|
30844
|
+
function mapCaliperEventsToStudentGameplayActivities(events, relevantCourseIds) {
|
|
30845
|
+
if (relevantCourseIds.size === 0) {
|
|
30846
|
+
return [];
|
|
30847
|
+
}
|
|
30848
|
+
const gameplayEventsByStudent = new Map;
|
|
30849
|
+
for (const event of events) {
|
|
30850
|
+
const isGameplayEvent = event.type === "ActivityEvent" || event.type === "TimeSpentEvent";
|
|
30851
|
+
const studentId = isGameplayEvent && !isCaliperRemediationOrCompletionEvent(event) ? getCaliperActorSourcedId(event) : undefined;
|
|
30852
|
+
if (studentId) {
|
|
30853
|
+
const existing = gameplayEventsByStudent.get(studentId);
|
|
30854
|
+
if (existing) {
|
|
30855
|
+
existing.push(event);
|
|
30856
|
+
} else {
|
|
30857
|
+
gameplayEventsByStudent.set(studentId, [event]);
|
|
30858
|
+
}
|
|
30859
|
+
}
|
|
30860
|
+
}
|
|
30861
|
+
const groups = [];
|
|
30862
|
+
for (const [studentId, studentEvents] of gameplayEventsByStudent) {
|
|
30863
|
+
const activities = [...groupCaliperEventsByRun(studentEvents).values()].map((group) => mapCaliperEventGroupToActivity(group, relevantCourseIds)).filter((activity) => Boolean(activity)).toSorted((a, b) => b.occurredAt.localeCompare(a.occurredAt));
|
|
30864
|
+
if (activities.length > 0) {
|
|
30865
|
+
groups.push({ studentId, activities });
|
|
30866
|
+
}
|
|
30867
|
+
}
|
|
30868
|
+
return groups.toSorted((a, b) => {
|
|
30869
|
+
const aLatest = a.activities[0]?.occurredAt ?? "";
|
|
30870
|
+
const bLatest = b.activities[0]?.occurredAt ?? "";
|
|
30871
|
+
return bLatest.localeCompare(aLatest);
|
|
30872
|
+
});
|
|
30873
|
+
}
|
|
30874
|
+
function mapCaliperEventsToCompletedStudentGameplayActivities(events, relevantCourseIds, options = {}) {
|
|
30875
|
+
const runIds = options.runIds ? new Set([...options.runIds].map((runId) => runId.toLowerCase())) : undefined;
|
|
30876
|
+
return mapCaliperEventsToStudentGameplayActivities(events, relevantCourseIds).filter((group) => !options.studentId || group.studentId === options.studentId).map((group) => ({
|
|
30877
|
+
studentId: group.studentId,
|
|
30878
|
+
activities: group.activities.filter((activity) => {
|
|
30879
|
+
const runId = activity.runId?.toLowerCase();
|
|
30880
|
+
return activity.kind === "activity" && runId !== undefined && isValidUUID(runId) && (!runIds || runIds.has(runId));
|
|
30881
|
+
})
|
|
30882
|
+
})).filter((group) => group.activities.length > 0);
|
|
30883
|
+
}
|
|
30884
|
+
function getTimebackActivityRunIds(groups) {
|
|
30885
|
+
const runIds = [];
|
|
30886
|
+
const seen = new Set;
|
|
30887
|
+
for (const group of groups) {
|
|
30888
|
+
for (const activity of group.activities) {
|
|
30889
|
+
const runId = activity.runId?.toLowerCase();
|
|
30890
|
+
if (runId && isValidUUID(runId) && !seen.has(runId)) {
|
|
30891
|
+
seen.add(runId);
|
|
30892
|
+
runIds.push(runId);
|
|
30893
|
+
}
|
|
30894
|
+
}
|
|
30895
|
+
}
|
|
30896
|
+
return runIds;
|
|
30897
|
+
}
|
|
30898
|
+
function mergeHydratedMetricDiscrepancyQueueItems(options) {
|
|
30899
|
+
const hydratedRunIds = new Set([...options.hydratedRunIds].map((runId) => runId.toLowerCase()));
|
|
30900
|
+
const hydratedItems = options.hydratedItems.map((item) => ({
|
|
30901
|
+
...item,
|
|
30902
|
+
comparisonSource: "hydrated"
|
|
30903
|
+
}));
|
|
30904
|
+
const preliminaryFallbackItems = options.preliminaryItems.filter((item) => {
|
|
30905
|
+
const runId = item.activity.runId?.toLowerCase();
|
|
30906
|
+
return runId === undefined || !hydratedRunIds.has(runId);
|
|
30907
|
+
}).map((item) => ({
|
|
30908
|
+
...item,
|
|
30909
|
+
comparisonSource: "preliminary"
|
|
30910
|
+
}));
|
|
30911
|
+
return [...hydratedItems, ...preliminaryFallbackItems].toSorted((a, b) => b.activity.occurredAt.localeCompare(a.activity.occurredAt));
|
|
30912
|
+
}
|
|
30913
|
+
function selectTimebackMetricDiscrepancyQueueItems(candidates, options) {
|
|
30914
|
+
const discrepancyMetricScopesSet = new Set(options.discrepancyMetricScopes);
|
|
30915
|
+
return candidates.filter((candidate) => candidate.activity.kind === "activity").filter((candidate) => candidate.gameMetricsComparison.status === "discrepant").filter((candidate) => !options.studentId || candidate.student.studentId === options.studentId).filter((candidate) => discrepancyMetricScopesSet.size === 0 || candidate.gameMetricsComparison.rows.some((row) => row.status === "discrepant" && discrepancyMetricScopesSet.has(row.metric))).filter((candidate) => options.includeVerified || !candidate.verification).toSorted((a, b) => b.activity.occurredAt.localeCompare(a.activity.occurredAt));
|
|
30916
|
+
}
|
|
30917
|
+
var DATE_INPUT_RE;
|
|
30918
|
+
var init_timeback_discrepancy_queue_util = __esm(() => {
|
|
30919
|
+
init_src4();
|
|
30920
|
+
init_timeback_util();
|
|
30921
|
+
DATE_INPUT_RE = /^\d{4}-\d{2}-\d{2}$/;
|
|
30922
|
+
});
|
|
30923
|
+
|
|
30924
|
+
// ../api-core/src/utils/timeback-game-metrics-comparison.util.ts
|
|
30925
|
+
function createMetricRow(definition) {
|
|
30926
|
+
const { gameValue, kind, metric, timebackValue, tolerance } = definition;
|
|
30927
|
+
if (timebackValue === undefined && gameValue === undefined) {
|
|
30928
|
+
return null;
|
|
30929
|
+
}
|
|
30930
|
+
if (gameValue === undefined) {
|
|
30931
|
+
return {
|
|
30932
|
+
metric,
|
|
30933
|
+
kind,
|
|
30934
|
+
status: "not_reported_by_game",
|
|
30935
|
+
...timebackValue !== undefined ? { timebackValue } : {}
|
|
30936
|
+
};
|
|
30937
|
+
}
|
|
30938
|
+
if (timebackValue === undefined) {
|
|
30939
|
+
return {
|
|
30940
|
+
metric,
|
|
30941
|
+
kind,
|
|
30942
|
+
status: "not_recorded_by_timeback",
|
|
30943
|
+
gameValue
|
|
30944
|
+
};
|
|
30945
|
+
}
|
|
30946
|
+
const delta = gameValue - timebackValue;
|
|
30947
|
+
const isDiscrepant = tolerance === 0 ? delta !== 0 : Math.abs(delta) >= tolerance;
|
|
30948
|
+
return {
|
|
30949
|
+
metric,
|
|
30950
|
+
kind,
|
|
30951
|
+
status: isDiscrepant ? "discrepant" : "matched",
|
|
30952
|
+
timebackValue,
|
|
30953
|
+
gameValue,
|
|
30954
|
+
delta
|
|
30955
|
+
};
|
|
30956
|
+
}
|
|
30957
|
+
function createRunComparison(activity, gameRun) {
|
|
30958
|
+
const runId = activity.runId ?? "";
|
|
30959
|
+
if (!gameRun) {
|
|
30960
|
+
return {
|
|
30961
|
+
runId,
|
|
30962
|
+
status: "not_reported",
|
|
30963
|
+
discrepancyCount: 0,
|
|
30964
|
+
rows: []
|
|
30965
|
+
};
|
|
30966
|
+
}
|
|
30967
|
+
const rows = [
|
|
30968
|
+
createMetricRow({
|
|
30969
|
+
metric: "xp",
|
|
30970
|
+
kind: "number",
|
|
30971
|
+
timebackValue: activity.xpDelta,
|
|
30972
|
+
gameValue: gameRun.totalXp,
|
|
30973
|
+
tolerance: TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE.xp
|
|
30974
|
+
}),
|
|
30975
|
+
createMetricRow({
|
|
30976
|
+
metric: "time",
|
|
30977
|
+
kind: "time",
|
|
30978
|
+
timebackValue: activity.timeDeltaSeconds,
|
|
30979
|
+
gameValue: gameRun.activeTimeSeconds,
|
|
30980
|
+
tolerance: TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE.time
|
|
30981
|
+
}),
|
|
30982
|
+
createMetricRow({
|
|
30983
|
+
metric: "score",
|
|
30984
|
+
kind: "percent",
|
|
30985
|
+
timebackValue: activity.score,
|
|
30986
|
+
gameValue: gameRun.score,
|
|
30987
|
+
tolerance: TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE.score
|
|
30988
|
+
}),
|
|
30989
|
+
createMetricRow({
|
|
30990
|
+
metric: "mastery",
|
|
30991
|
+
kind: "number",
|
|
30992
|
+
timebackValue: activity.masteredUnitsDelta ?? 0,
|
|
30993
|
+
gameValue: gameRun.masteredUnits,
|
|
30994
|
+
tolerance: TIMEBACK_GAME_METRIC_COMPARISON_TOLERANCE.mastery
|
|
30995
|
+
})
|
|
30996
|
+
].filter((row) => row !== null);
|
|
30997
|
+
const discrepancyCount = rows.filter((row) => row.status === "discrepant").length;
|
|
30998
|
+
return {
|
|
30999
|
+
runId,
|
|
31000
|
+
status: discrepancyCount > 0 ? "discrepant" : "matched",
|
|
31001
|
+
discrepancyCount,
|
|
31002
|
+
rows
|
|
31003
|
+
};
|
|
31004
|
+
}
|
|
31005
|
+
function summarizeGameRunMetricsComparison(comparison) {
|
|
31006
|
+
return {
|
|
31007
|
+
runId: comparison.runId,
|
|
31008
|
+
status: comparison.status,
|
|
31009
|
+
discrepancyCount: comparison.discrepancyCount,
|
|
31010
|
+
...comparison.reason ? { reason: comparison.reason } : {}
|
|
31011
|
+
};
|
|
31012
|
+
}
|
|
31013
|
+
function buildGameRunMetricComparisons(activities, course, response) {
|
|
31014
|
+
const activitiesWithRunIds = activities.filter((activity) => typeof activity.runId === "string" && activity.runId.length > 0);
|
|
31015
|
+
const comparisons = new Map;
|
|
31016
|
+
if (activitiesWithRunIds.length === 0) {
|
|
31017
|
+
return comparisons;
|
|
31018
|
+
}
|
|
31019
|
+
if (!response.supported) {
|
|
31020
|
+
for (const activity of activitiesWithRunIds) {
|
|
31021
|
+
comparisons.set(activity.runId, {
|
|
31022
|
+
runId: activity.runId,
|
|
31023
|
+
status: "unavailable",
|
|
31024
|
+
discrepancyCount: 0,
|
|
31025
|
+
reason: response.reason,
|
|
31026
|
+
rows: []
|
|
31027
|
+
});
|
|
31028
|
+
}
|
|
31029
|
+
return comparisons;
|
|
31030
|
+
}
|
|
31031
|
+
const gameCourseMetrics = response.metrics.courses.find((gameCourse) => gameCourse.grade === course.grade && gameCourse.subject === course.subject);
|
|
31032
|
+
const gameRunsById = new Map(gameCourseMetrics?.activities?.map((gameRun) => [gameRun.runId.toLowerCase(), gameRun]));
|
|
31033
|
+
for (const activity of activitiesWithRunIds) {
|
|
31034
|
+
comparisons.set(activity.runId, createRunComparison(activity, gameRunsById.get(activity.runId.toLowerCase())));
|
|
31035
|
+
}
|
|
31036
|
+
return comparisons;
|
|
31037
|
+
}
|
|
31038
|
+
var init_timeback_game_metrics_comparison_util = __esm(() => {
|
|
31039
|
+
init_src();
|
|
31040
|
+
});
|
|
31041
|
+
|
|
31042
|
+
// ../api-core/src/utils/timeback-mastery-completion.util.ts
|
|
31043
|
+
async function upsertMasteryCompletionEntry(params) {
|
|
31044
|
+
const { client, courseId, studentId, appName, action } = params;
|
|
31045
|
+
const ids = deriveSourcedIds(courseId);
|
|
31046
|
+
const lineItemId = `${ids.course}-mastery-completion-assessment`;
|
|
31047
|
+
const resultId = `${lineItemId}:${studentId}:completion`;
|
|
31048
|
+
if (action === "complete") {
|
|
31049
|
+
await client.oneroster.assessmentLineItems.findOrCreate(lineItemId, {
|
|
31050
|
+
sourcedId: lineItemId,
|
|
31051
|
+
title: "Mastery Completion",
|
|
31052
|
+
status: ONEROSTER_STATUS.active,
|
|
31053
|
+
course: { sourcedId: ids.course },
|
|
31054
|
+
...ids.componentResource ? { componentResource: { sourcedId: ids.componentResource } } : {}
|
|
31055
|
+
});
|
|
31056
|
+
await client.oneroster.assessmentResults.upsert(resultId, {
|
|
31057
|
+
sourcedId: resultId,
|
|
31058
|
+
status: ONEROSTER_STATUS.active,
|
|
31059
|
+
assessmentLineItem: { sourcedId: lineItemId },
|
|
31060
|
+
student: { sourcedId: studentId },
|
|
31061
|
+
score: 100,
|
|
31062
|
+
scoreDate: new Date().toISOString(),
|
|
31063
|
+
scoreStatus: SCORE_STATUS.fullyGraded,
|
|
31064
|
+
inProgress: "false",
|
|
31065
|
+
metadata: {
|
|
31066
|
+
isMasteryCompletion: true,
|
|
31067
|
+
adminAction: true,
|
|
31068
|
+
appName
|
|
31069
|
+
}
|
|
31070
|
+
});
|
|
31071
|
+
} else {
|
|
31072
|
+
try {
|
|
31073
|
+
await client.oneroster.assessmentResults.upsert(resultId, {
|
|
31074
|
+
sourcedId: resultId,
|
|
31075
|
+
status: ONEROSTER_STATUS.active,
|
|
31076
|
+
assessmentLineItem: { sourcedId: lineItemId },
|
|
31077
|
+
student: { sourcedId: studentId },
|
|
31078
|
+
score: 0,
|
|
31079
|
+
scoreDate: new Date().toISOString(),
|
|
31080
|
+
scoreStatus: SCORE_STATUS.notSubmitted,
|
|
31081
|
+
inProgress: "true",
|
|
31082
|
+
metadata: {
|
|
31083
|
+
isMasteryCompletion: true,
|
|
31084
|
+
adminAction: true,
|
|
31085
|
+
appName
|
|
31086
|
+
}
|
|
31087
|
+
});
|
|
31088
|
+
} catch {
|
|
31089
|
+
logger16.debug("No completion entry to revoke", { studentId, courseId });
|
|
31090
|
+
}
|
|
31091
|
+
}
|
|
31092
|
+
}
|
|
31093
|
+
var logger16;
|
|
31094
|
+
var init_timeback_mastery_completion_util = __esm(() => {
|
|
31095
|
+
init_src2();
|
|
31096
|
+
init_constants4();
|
|
31097
|
+
init_utils6();
|
|
31098
|
+
logger16 = log.scope("timeback-mastery-completion");
|
|
31099
|
+
});
|
|
31100
|
+
|
|
30779
31101
|
// ../api-core/src/services/timeback-admin.service.ts
|
|
30780
31102
|
class TimebackAdminService {
|
|
30781
31103
|
deps;
|
|
@@ -30784,6 +31106,10 @@ class TimebackAdminService {
|
|
|
30784
31106
|
static MAX_STUDENT_ACTIVITY_LIMIT = 200;
|
|
30785
31107
|
static MAX_STUDENT_ACTIVITY_OFFSET = 1000;
|
|
30786
31108
|
static MAX_RECENT_ACTIVITY_EVENT_FETCH = 4000;
|
|
31109
|
+
static DISCREPANCY_QUEUE_EVENT_FETCH_LIMIT = 4000;
|
|
31110
|
+
static DISCREPANCY_QUEUE_RUN_EVENT_FETCH_LIMIT = 4000;
|
|
31111
|
+
static DISCREPANCY_QUEUE_RUN_HYDRATION_CONCURRENCY = 4;
|
|
31112
|
+
static DISCREPANCY_QUEUE_COMPARISON_CONCURRENCY = 4;
|
|
30787
31113
|
static ANALYTICS_CONCURRENCY = 8;
|
|
30788
31114
|
static MASTERABLE_UNITS_CONCURRENCY = 4;
|
|
30789
31115
|
static GAME_METRICS_FETCH_TIMEOUT_MS = 1e4;
|
|
@@ -31031,16 +31357,53 @@ class TimebackAdminService {
|
|
|
31031
31357
|
}));
|
|
31032
31358
|
return comparisons;
|
|
31033
31359
|
}
|
|
31360
|
+
async getMetricDiscrepancyVerificationsByKey(options) {
|
|
31361
|
+
const runIds = TimebackAdminService.normalizeRunIds(options.runIds);
|
|
31362
|
+
if (runIds.length === 0) {
|
|
31363
|
+
return new Map;
|
|
31364
|
+
}
|
|
31365
|
+
const conditions2 = [
|
|
31366
|
+
eq(gameTimebackMetricDiscrepancyVerifications.gameId, options.gameId),
|
|
31367
|
+
eq(gameTimebackMetricDiscrepancyVerifications.courseId, options.courseId),
|
|
31368
|
+
inArray(gameTimebackMetricDiscrepancyVerifications.runId, runIds)
|
|
31369
|
+
];
|
|
31370
|
+
if (options.studentId) {
|
|
31371
|
+
conditions2.push(eq(gameTimebackMetricDiscrepancyVerifications.studentId, options.studentId));
|
|
31372
|
+
}
|
|
31373
|
+
const rows = await this.deps.db.query.gameTimebackMetricDiscrepancyVerifications.findMany({
|
|
31374
|
+
where: and(...conditions2)
|
|
31375
|
+
});
|
|
31376
|
+
return new Map(rows.map((row) => [
|
|
31377
|
+
getTimebackDiscrepancyVerificationKey({
|
|
31378
|
+
studentId: row.studentId,
|
|
31379
|
+
runId: row.runId
|
|
31380
|
+
}),
|
|
31381
|
+
TimebackAdminService.mapMetricDiscrepancyVerification(row)
|
|
31382
|
+
]));
|
|
31383
|
+
}
|
|
31034
31384
|
async attachGameMetricSummariesToActivities(user, options) {
|
|
31035
31385
|
const comparisons = await this.getGameMetricComparisonsForActivities(user, options);
|
|
31036
31386
|
if (comparisons.size === 0) {
|
|
31037
31387
|
return [...options.activities];
|
|
31038
31388
|
}
|
|
31389
|
+
const verificationsByKey = await this.getMetricDiscrepancyVerificationsByKey({
|
|
31390
|
+
gameId: options.gameId,
|
|
31391
|
+
courseId: options.courseId,
|
|
31392
|
+
studentId: options.studentId,
|
|
31393
|
+
runIds: [...comparisons.values()].filter((comparison) => comparison.status === "discrepant").map((comparison) => comparison.runId)
|
|
31394
|
+
});
|
|
31039
31395
|
return options.activities.map((activity) => {
|
|
31040
31396
|
const comparison = activity.runId ? comparisons.get(activity.runId) : undefined;
|
|
31397
|
+
const verification2 = getTimebackMetricDiscrepancyVerificationForActivity({
|
|
31398
|
+
studentId: options.studentId,
|
|
31399
|
+
activity,
|
|
31400
|
+
comparison,
|
|
31401
|
+
verificationsByKey
|
|
31402
|
+
});
|
|
31041
31403
|
return comparison ? {
|
|
31042
31404
|
...activity,
|
|
31043
|
-
gameMetricsComparison: summarizeGameRunMetricsComparison(comparison)
|
|
31405
|
+
gameMetricsComparison: summarizeGameRunMetricsComparison(comparison),
|
|
31406
|
+
...verification2 ? { gameMetricsVerification: verification2 } : {}
|
|
31044
31407
|
} : activity;
|
|
31045
31408
|
});
|
|
31046
31409
|
}
|
|
@@ -31131,6 +31494,165 @@ class TimebackAdminService {
|
|
|
31131
31494
|
});
|
|
31132
31495
|
return events;
|
|
31133
31496
|
}
|
|
31497
|
+
async fetchCaliperEventsForGame(client, source, options) {
|
|
31498
|
+
const actorId = options.studentId ? `${client.getBaseUrl().replace(/\/$/, "")}/ims/oneroster/rostering/v1p2/users/${options.studentId}` : undefined;
|
|
31499
|
+
return client.caliper.events.list({
|
|
31500
|
+
limit: options.limit,
|
|
31501
|
+
offset: options.offset,
|
|
31502
|
+
...actorId ? { actorId } : {},
|
|
31503
|
+
...options.startDate ? { startDate: options.startDate } : {},
|
|
31504
|
+
...options.endDate ? { endDate: options.endDate } : {},
|
|
31505
|
+
...options.sessionId ? { sessionId: options.sessionId } : {},
|
|
31506
|
+
...source.sourceMode === "production" ? { sensor: source.sensorUrl } : {},
|
|
31507
|
+
extensions: {
|
|
31508
|
+
gameId: source.gameId
|
|
31509
|
+
}
|
|
31510
|
+
});
|
|
31511
|
+
}
|
|
31512
|
+
static getActivityRunOwners(activityGroups) {
|
|
31513
|
+
const runOwnersById = new Map;
|
|
31514
|
+
for (const group of activityGroups) {
|
|
31515
|
+
for (const activity of group.activities) {
|
|
31516
|
+
const runId = activity.runId?.toLowerCase();
|
|
31517
|
+
if (runId && !runOwnersById.has(runId)) {
|
|
31518
|
+
runOwnersById.set(runId, group.studentId);
|
|
31519
|
+
}
|
|
31520
|
+
}
|
|
31521
|
+
}
|
|
31522
|
+
return runOwnersById;
|
|
31523
|
+
}
|
|
31524
|
+
static dedupeCaliperEvents(events) {
|
|
31525
|
+
const deduped = [];
|
|
31526
|
+
const seen = new Set;
|
|
31527
|
+
for (const event of events) {
|
|
31528
|
+
const key = `${event.id}:${event.externalId}`;
|
|
31529
|
+
if (!seen.has(key)) {
|
|
31530
|
+
seen.add(key);
|
|
31531
|
+
deduped.push(event);
|
|
31532
|
+
}
|
|
31533
|
+
}
|
|
31534
|
+
return deduped;
|
|
31535
|
+
}
|
|
31536
|
+
async fetchCaliperEventsForRun(client, source, options) {
|
|
31537
|
+
const runId = options.runId.toLowerCase();
|
|
31538
|
+
const events = [];
|
|
31539
|
+
let offset = 0;
|
|
31540
|
+
while (true) {
|
|
31541
|
+
const result = await this.fetchCaliperEventsForGame(client, source, {
|
|
31542
|
+
limit: TimebackAdminService.DISCREPANCY_QUEUE_RUN_EVENT_FETCH_LIMIT,
|
|
31543
|
+
offset,
|
|
31544
|
+
studentId: options.studentId
|
|
31545
|
+
});
|
|
31546
|
+
const runEvents = result.events.filter((event) => getCanonicalRunId(event.session)?.toLowerCase() === runId);
|
|
31547
|
+
events.push(...runEvents);
|
|
31548
|
+
const pageStep = result.pagination.limit > 0 ? result.pagination.limit : result.events.length;
|
|
31549
|
+
const nextOffset = offset + pageStep;
|
|
31550
|
+
if (pageStep === 0 || result.events.length === 0 || nextOffset >= result.pagination.total) {
|
|
31551
|
+
break;
|
|
31552
|
+
}
|
|
31553
|
+
offset = nextOffset;
|
|
31554
|
+
}
|
|
31555
|
+
return TimebackAdminService.dedupeCaliperEvents(events);
|
|
31556
|
+
}
|
|
31557
|
+
async hydrateDiscrepancyQueueRunEvents(client, source, options) {
|
|
31558
|
+
if (options.runOwnersById.size === 0) {
|
|
31559
|
+
return { events: [], runIds: new Set };
|
|
31560
|
+
}
|
|
31561
|
+
const hydratedRuns = await TimebackAdminService.runWithConcurrency([...options.runOwnersById.entries()], TimebackAdminService.DISCREPANCY_QUEUE_RUN_HYDRATION_CONCURRENCY, async ([runId, studentId]) => {
|
|
31562
|
+
try {
|
|
31563
|
+
return {
|
|
31564
|
+
runId,
|
|
31565
|
+
events: await this.fetchCaliperEventsForRun(client, source, {
|
|
31566
|
+
runId,
|
|
31567
|
+
studentId
|
|
31568
|
+
})
|
|
31569
|
+
};
|
|
31570
|
+
} catch (error) {
|
|
31571
|
+
logger17.warn("Failed to hydrate Caliper events for discrepancy queue run", {
|
|
31572
|
+
runId,
|
|
31573
|
+
studentId,
|
|
31574
|
+
error: error instanceof Error ? error.message : String(error)
|
|
31575
|
+
});
|
|
31576
|
+
return { runId, events: [] };
|
|
31577
|
+
}
|
|
31578
|
+
});
|
|
31579
|
+
const hydratedRunIds = new Set(hydratedRuns.filter((result) => result.events.length > 0).map((result) => result.runId.toLowerCase()));
|
|
31580
|
+
const baseEventsForHydratedRuns = options.baseEvents.filter((event) => {
|
|
31581
|
+
const runId = getCanonicalRunId(event.session)?.toLowerCase();
|
|
31582
|
+
return runId !== undefined && hydratedRunIds.has(runId);
|
|
31583
|
+
});
|
|
31584
|
+
return {
|
|
31585
|
+
events: TimebackAdminService.dedupeCaliperEvents([
|
|
31586
|
+
...baseEventsForHydratedRuns,
|
|
31587
|
+
...hydratedRuns.flatMap((result) => result.events)
|
|
31588
|
+
]),
|
|
31589
|
+
runIds: hydratedRunIds
|
|
31590
|
+
};
|
|
31591
|
+
}
|
|
31592
|
+
async buildMetricDiscrepancyQueueCandidates(user, options) {
|
|
31593
|
+
const comparisonResults = await TimebackAdminService.runWithConcurrency(options.activityGroups, TimebackAdminService.DISCREPANCY_QUEUE_COMPARISON_CONCURRENCY, async (group) => {
|
|
31594
|
+
try {
|
|
31595
|
+
return {
|
|
31596
|
+
group,
|
|
31597
|
+
comparisons: await this.getGameMetricComparisonsForActivities(user, {
|
|
31598
|
+
gameId: options.gameId,
|
|
31599
|
+
studentId: group.studentId,
|
|
31600
|
+
course: options.course,
|
|
31601
|
+
activities: group.activities,
|
|
31602
|
+
timeoutMs: TimebackAdminService.GAME_METRICS_LIST_FETCH_TIMEOUT_MS
|
|
31603
|
+
})
|
|
31604
|
+
};
|
|
31605
|
+
} catch (error) {
|
|
31606
|
+
logger17.warn("Failed to compare game metrics for discrepancy queue student", {
|
|
31607
|
+
gameId: options.gameId,
|
|
31608
|
+
courseId: options.courseId,
|
|
31609
|
+
studentId: group.studentId,
|
|
31610
|
+
error: error instanceof Error ? error.message : String(error)
|
|
31611
|
+
});
|
|
31612
|
+
return { group, comparisons: new Map };
|
|
31613
|
+
}
|
|
31614
|
+
});
|
|
31615
|
+
return comparisonResults.flatMap(({ group, comparisons }) => {
|
|
31616
|
+
const student = options.studentsById.get(group.studentId) ?? {
|
|
31617
|
+
studentId: group.studentId,
|
|
31618
|
+
name: "No name specified",
|
|
31619
|
+
email: null
|
|
31620
|
+
};
|
|
31621
|
+
return group.activities.flatMap((activity) => {
|
|
31622
|
+
const runId = activity.runId;
|
|
31623
|
+
if (!runId) {
|
|
31624
|
+
return [];
|
|
31625
|
+
}
|
|
31626
|
+
const gameMetricsComparison = comparisons.get(runId);
|
|
31627
|
+
if (!gameMetricsComparison) {
|
|
31628
|
+
return [];
|
|
31629
|
+
}
|
|
31630
|
+
const verification2 = options.verificationsByKey.get(getTimebackDiscrepancyVerificationKey({
|
|
31631
|
+
studentId: group.studentId,
|
|
31632
|
+
runId
|
|
31633
|
+
}));
|
|
31634
|
+
return [
|
|
31635
|
+
{
|
|
31636
|
+
student,
|
|
31637
|
+
activity,
|
|
31638
|
+
gameMetricsComparison,
|
|
31639
|
+
...verification2 ? { verification: verification2 } : {}
|
|
31640
|
+
}
|
|
31641
|
+
];
|
|
31642
|
+
});
|
|
31643
|
+
});
|
|
31644
|
+
}
|
|
31645
|
+
static mapMetricDiscrepancyVerification(row) {
|
|
31646
|
+
return {
|
|
31647
|
+
gameId: row.gameId,
|
|
31648
|
+
courseId: row.courseId,
|
|
31649
|
+
studentId: row.studentId,
|
|
31650
|
+
runId: row.runId,
|
|
31651
|
+
activityId: row.activityId,
|
|
31652
|
+
verifiedAt: row.verifiedAt.toISOString(),
|
|
31653
|
+
verifiedByUserId: row.verifiedByUserId
|
|
31654
|
+
};
|
|
31655
|
+
}
|
|
31134
31656
|
async listRecentActivityForStudent(client, studentId, source, relevantCourseIds, maxResults = TimebackAdminService.RECENT_ACTIVITY_LIMIT) {
|
|
31135
31657
|
if (relevantCourseIds.size === 0) {
|
|
31136
31658
|
return [];
|
|
@@ -31380,12 +31902,215 @@ class TimebackAdminService {
|
|
|
31380
31902
|
const activitiesWithGameMetrics = await this.attachGameMetricSummariesToActivities(user, {
|
|
31381
31903
|
gameId,
|
|
31382
31904
|
studentId,
|
|
31905
|
+
courseId,
|
|
31383
31906
|
course: { grade: integration.grade, subject: integration.subject },
|
|
31384
31907
|
activities,
|
|
31385
31908
|
timeoutMs: TimebackAdminService.GAME_METRICS_LIST_FETCH_TIMEOUT_MS
|
|
31386
31909
|
});
|
|
31387
31910
|
return { activities: activitiesWithGameMetrics, hasMore };
|
|
31388
31911
|
}
|
|
31912
|
+
async listMetricDiscrepancies(user, options) {
|
|
31913
|
+
const { gameId, courseId, window: window2, includeVerified } = options;
|
|
31914
|
+
const selectedStudentId = options.studentId?.trim() || undefined;
|
|
31915
|
+
const client = this.requireClient();
|
|
31916
|
+
const dateRange = getTimebackDiscrepancyQueueDateRange({
|
|
31917
|
+
window: window2,
|
|
31918
|
+
startDate: options.startDate,
|
|
31919
|
+
endDate: options.endDate,
|
|
31920
|
+
timezone: PLATFORM_TIMEZONE
|
|
31921
|
+
});
|
|
31922
|
+
const safeEventOffset = Math.max(0, Math.trunc(options.eventOffset));
|
|
31923
|
+
await this.deps.validateGameManagementAccess(user, gameId);
|
|
31924
|
+
const [integration, gameSource, roster] = await Promise.all([
|
|
31925
|
+
this.deps.db.query.gameTimebackIntegrations.findFirst({
|
|
31926
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId))
|
|
31927
|
+
}),
|
|
31928
|
+
this.getGameActivitySource(gameId),
|
|
31929
|
+
client.oneroster.enrollments.listByCourse(courseId, {
|
|
31930
|
+
role: "student",
|
|
31931
|
+
includeInactive: true
|
|
31932
|
+
})
|
|
31933
|
+
]);
|
|
31934
|
+
if (!integration) {
|
|
31935
|
+
throw new NotFoundError("Timeback integration", `${gameId}:${courseId}`);
|
|
31936
|
+
}
|
|
31937
|
+
const studentsById = new Map;
|
|
31938
|
+
for (const entry of roster) {
|
|
31939
|
+
const studentId = entry.enrollment.user.sourcedId;
|
|
31940
|
+
if (studentId && !studentsById.has(studentId)) {
|
|
31941
|
+
const name3 = entry.user ? `${entry.user.givenName} ${entry.user.familyName}`.trim() || "No name specified" : "No name specified";
|
|
31942
|
+
studentsById.set(studentId, {
|
|
31943
|
+
studentId,
|
|
31944
|
+
name: name3,
|
|
31945
|
+
email: entry.user?.email || null
|
|
31946
|
+
});
|
|
31947
|
+
}
|
|
31948
|
+
}
|
|
31949
|
+
const students = [...studentsById.values()].toSorted((a, b) => a.name.localeCompare(b.name) || a.studentId.localeCompare(b.studentId));
|
|
31950
|
+
if (selectedStudentId && !studentsById.has(selectedStudentId)) {
|
|
31951
|
+
students.push({
|
|
31952
|
+
studentId: selectedStudentId,
|
|
31953
|
+
name: selectedStudentId,
|
|
31954
|
+
email: null
|
|
31955
|
+
});
|
|
31956
|
+
}
|
|
31957
|
+
const eventResult = await this.fetchCaliperEventsForGame(client, gameSource, {
|
|
31958
|
+
limit: TimebackAdminService.DISCREPANCY_QUEUE_EVENT_FETCH_LIMIT,
|
|
31959
|
+
offset: safeEventOffset,
|
|
31960
|
+
...selectedStudentId ? { studentId: selectedStudentId } : {},
|
|
31961
|
+
...dateRange
|
|
31962
|
+
});
|
|
31963
|
+
const { events, pagination } = eventResult;
|
|
31964
|
+
let effectiveEventPageLimit = pagination.limit;
|
|
31965
|
+
if (effectiveEventPageLimit <= 0) {
|
|
31966
|
+
effectiveEventPageLimit = events.length > 0 ? events.length : TimebackAdminService.DISCREPANCY_QUEUE_EVENT_FETCH_LIMIT;
|
|
31967
|
+
}
|
|
31968
|
+
const relevantCourseIds = new Set([courseId]);
|
|
31969
|
+
const preliminaryActivityGroups = mapCaliperEventsToCompletedStudentGameplayActivities(events, relevantCourseIds, selectedStudentId ? { studentId: selectedStudentId } : {});
|
|
31970
|
+
const preliminaryRunIds = getTimebackActivityRunIds(preliminaryActivityGroups);
|
|
31971
|
+
const verificationsByKey = await this.getMetricDiscrepancyVerificationsByKey({
|
|
31972
|
+
gameId,
|
|
31973
|
+
courseId,
|
|
31974
|
+
runIds: preliminaryRunIds
|
|
31975
|
+
});
|
|
31976
|
+
const queueFilterOptions = {
|
|
31977
|
+
...selectedStudentId ? { studentId: selectedStudentId } : {},
|
|
31978
|
+
discrepancyMetricScopes: options.discrepancyMetricScopes,
|
|
31979
|
+
includeVerified
|
|
31980
|
+
};
|
|
31981
|
+
const preliminaryCandidates = await this.buildMetricDiscrepancyQueueCandidates(user, {
|
|
31982
|
+
gameId,
|
|
31983
|
+
courseId,
|
|
31984
|
+
course: { grade: integration.grade, subject: integration.subject },
|
|
31985
|
+
activityGroups: preliminaryActivityGroups,
|
|
31986
|
+
studentsById,
|
|
31987
|
+
verificationsByKey
|
|
31988
|
+
});
|
|
31989
|
+
const preliminaryItems = selectTimebackMetricDiscrepancyQueueItems(preliminaryCandidates, queueFilterOptions);
|
|
31990
|
+
const runOwnersById = TimebackAdminService.getActivityRunOwners(preliminaryItems.map((item) => ({
|
|
31991
|
+
studentId: item.student.studentId,
|
|
31992
|
+
activities: [item.activity]
|
|
31993
|
+
})));
|
|
31994
|
+
const hydratedRunEvents = await this.hydrateDiscrepancyQueueRunEvents(client, gameSource, {
|
|
31995
|
+
baseEvents: events,
|
|
31996
|
+
runOwnersById
|
|
31997
|
+
});
|
|
31998
|
+
const finalActivityGroups = mapCaliperEventsToCompletedStudentGameplayActivities(hydratedRunEvents.events, relevantCourseIds, {
|
|
31999
|
+
...selectedStudentId ? { studentId: selectedStudentId } : {},
|
|
32000
|
+
runIds: hydratedRunEvents.runIds
|
|
32001
|
+
});
|
|
32002
|
+
const finalCandidates = await this.buildMetricDiscrepancyQueueCandidates(user, {
|
|
32003
|
+
gameId,
|
|
32004
|
+
courseId,
|
|
32005
|
+
course: { grade: integration.grade, subject: integration.subject },
|
|
32006
|
+
activityGroups: finalActivityGroups,
|
|
32007
|
+
studentsById,
|
|
32008
|
+
verificationsByKey
|
|
32009
|
+
});
|
|
32010
|
+
const hydratedItems = selectTimebackMetricDiscrepancyQueueItems(finalCandidates, queueFilterOptions);
|
|
32011
|
+
const items = mergeHydratedMetricDiscrepancyQueueItems({
|
|
32012
|
+
preliminaryItems,
|
|
32013
|
+
hydratedItems,
|
|
32014
|
+
hydratedRunIds: hydratedRunEvents.runIds
|
|
32015
|
+
});
|
|
32016
|
+
const eventPage = {
|
|
32017
|
+
offset: safeEventOffset,
|
|
32018
|
+
limit: effectiveEventPageLimit,
|
|
32019
|
+
total: pagination.total,
|
|
32020
|
+
hasPrevious: safeEventOffset > 0,
|
|
32021
|
+
hasNext: safeEventOffset + effectiveEventPageLimit < pagination.total
|
|
32022
|
+
};
|
|
32023
|
+
return {
|
|
32024
|
+
gameId,
|
|
32025
|
+
courseId,
|
|
32026
|
+
window: window2,
|
|
32027
|
+
dateRange,
|
|
32028
|
+
...selectedStudentId ? { studentId: selectedStudentId } : {},
|
|
32029
|
+
students,
|
|
32030
|
+
discrepancyMetricScopes: [...options.discrepancyMetricScopes ?? []],
|
|
32031
|
+
eventPage,
|
|
32032
|
+
includeVerified,
|
|
32033
|
+
items
|
|
32034
|
+
};
|
|
32035
|
+
}
|
|
32036
|
+
async verifyMetricDiscrepancy(user, options) {
|
|
32037
|
+
const { gameId, courseId, data } = options;
|
|
32038
|
+
const client = this.requireClient();
|
|
32039
|
+
await this.deps.validateDeveloperAccess(user, gameId);
|
|
32040
|
+
const [integration, gameSource] = await Promise.all([
|
|
32041
|
+
this.deps.db.query.gameTimebackIntegrations.findFirst({
|
|
32042
|
+
where: and(eq(gameTimebackIntegrations.gameId, gameId), eq(gameTimebackIntegrations.courseId, courseId))
|
|
32043
|
+
}),
|
|
32044
|
+
this.getGameActivitySource(gameId)
|
|
32045
|
+
]);
|
|
32046
|
+
if (!integration) {
|
|
32047
|
+
throw new NotFoundError("Timeback integration", `${gameId}:${courseId}`);
|
|
32048
|
+
}
|
|
32049
|
+
await this.assertStudentHasEnrollmentInCourse(client, data.studentId, courseId);
|
|
32050
|
+
const runId = data.runId.toLowerCase();
|
|
32051
|
+
let runEvents;
|
|
32052
|
+
try {
|
|
32053
|
+
runEvents = await this.fetchCaliperEventsForRun(client, gameSource, {
|
|
32054
|
+
runId,
|
|
32055
|
+
studentId: data.studentId
|
|
32056
|
+
});
|
|
32057
|
+
} catch (error) {
|
|
32058
|
+
logger17.warn("Failed to hydrate Caliper events for discrepancy verification", {
|
|
32059
|
+
gameId,
|
|
32060
|
+
courseId,
|
|
32061
|
+
studentId: data.studentId,
|
|
32062
|
+
runId,
|
|
32063
|
+
error: error instanceof Error ? error.message : String(error)
|
|
32064
|
+
});
|
|
32065
|
+
throw new ValidationError("Cannot verify discrepancy until full run data is available");
|
|
32066
|
+
}
|
|
32067
|
+
const activity = mapCaliperEventsToCompletedStudentGameplayActivities(runEvents, new Set([courseId]), {
|
|
32068
|
+
studentId: data.studentId,
|
|
32069
|
+
runIds: new Set([runId])
|
|
32070
|
+
}).flatMap((group) => group.activities).find((runActivity) => runActivity.runId?.toLowerCase() === runId);
|
|
32071
|
+
if (!activity) {
|
|
32072
|
+
throw new ValidationError("Cannot verify discrepancy until full run data is available");
|
|
32073
|
+
}
|
|
32074
|
+
const comparisons = await this.getGameMetricComparisonsForActivities(user, {
|
|
32075
|
+
gameId,
|
|
32076
|
+
studentId: data.studentId,
|
|
32077
|
+
course: { grade: integration.grade, subject: integration.subject },
|
|
32078
|
+
activities: [activity]
|
|
32079
|
+
});
|
|
32080
|
+
const comparison = comparisons.get(activity.runId ?? runId);
|
|
32081
|
+
if (comparison?.status !== "discrepant") {
|
|
32082
|
+
throw new ValidationError("Cannot verify discrepancy because the hydrated run is not discrepant");
|
|
32083
|
+
}
|
|
32084
|
+
const verifiedAt = new Date;
|
|
32085
|
+
const [row] = await this.deps.db.insert(gameTimebackMetricDiscrepancyVerifications).values({
|
|
32086
|
+
gameId,
|
|
32087
|
+
courseId,
|
|
32088
|
+
studentId: data.studentId,
|
|
32089
|
+
runId,
|
|
32090
|
+
activityId: data.activityId ?? null,
|
|
32091
|
+
verifiedByUserId: user.id,
|
|
32092
|
+
verifiedAt
|
|
32093
|
+
}).onConflictDoUpdate({
|
|
32094
|
+
target: [
|
|
32095
|
+
gameTimebackMetricDiscrepancyVerifications.gameId,
|
|
32096
|
+
gameTimebackMetricDiscrepancyVerifications.courseId,
|
|
32097
|
+
gameTimebackMetricDiscrepancyVerifications.studentId,
|
|
32098
|
+
gameTimebackMetricDiscrepancyVerifications.runId
|
|
32099
|
+
],
|
|
32100
|
+
set: {
|
|
32101
|
+
activityId: data.activityId ?? null,
|
|
32102
|
+
verifiedByUserId: user.id,
|
|
32103
|
+
verifiedAt
|
|
32104
|
+
}
|
|
32105
|
+
}).returning();
|
|
32106
|
+
if (!row) {
|
|
32107
|
+
throw new ValidationError("Failed to verify metric discrepancy");
|
|
32108
|
+
}
|
|
32109
|
+
return {
|
|
32110
|
+
status: "ok",
|
|
32111
|
+
verification: TimebackAdminService.mapMetricDiscrepancyVerification(row)
|
|
32112
|
+
};
|
|
32113
|
+
}
|
|
31389
32114
|
async getActivityDetail(user, options) {
|
|
31390
32115
|
const { gameId, studentId, courseId, activityId, runId } = options;
|
|
31391
32116
|
const client = this.requireClient();
|
|
@@ -31400,16 +32125,37 @@ class TimebackAdminService {
|
|
|
31400
32125
|
throw new NotFoundError("Timeback integration", `${gameId}:${courseId}`);
|
|
31401
32126
|
}
|
|
31402
32127
|
await this.assertStudentHasEnrollmentInCourse(client, studentId, courseId);
|
|
31403
|
-
const events = await this.fetchCaliperEventsForStudent(client, studentId, gameSource, TimebackAdminService.MAX_RECENT_ACTIVITY_EVENT_FETCH);
|
|
31404
32128
|
const relevantCourseIds = new Set([courseId]);
|
|
31405
32129
|
let matchedEvents;
|
|
31406
32130
|
let activity;
|
|
31407
32131
|
if (runId) {
|
|
31408
|
-
const
|
|
31409
|
-
|
|
31410
|
-
|
|
31411
|
-
|
|
32132
|
+
const normalizedRunId = TimebackAdminService.normalizeRunIds([runId], 1)[0];
|
|
32133
|
+
if (!normalizedRunId) {
|
|
32134
|
+
matchedEvents = [];
|
|
32135
|
+
activity = null;
|
|
32136
|
+
} else {
|
|
32137
|
+
let runEvents;
|
|
32138
|
+
try {
|
|
32139
|
+
runEvents = await this.fetchCaliperEventsForRun(client, gameSource, {
|
|
32140
|
+
runId: normalizedRunId,
|
|
32141
|
+
studentId
|
|
32142
|
+
});
|
|
32143
|
+
} catch (error) {
|
|
32144
|
+
logger17.warn("Failed to load Caliper events for activity detail run", {
|
|
32145
|
+
runId: normalizedRunId,
|
|
32146
|
+
studentId,
|
|
32147
|
+
activityId,
|
|
32148
|
+
error: error instanceof Error ? error.message : String(error)
|
|
32149
|
+
});
|
|
32150
|
+
runEvents = [];
|
|
32151
|
+
}
|
|
32152
|
+
const gameplayEvents = runEvents.filter((event) => (event.type === "ActivityEvent" || event.type === "TimeSpentEvent") && !isCaliperRemediationOrCompletionEvent(event));
|
|
32153
|
+
const matchingActivityEvents = findCaliperEventGroupContainingExternalId(gameplayEvents, activityId);
|
|
32154
|
+
matchedEvents = matchingActivityEvents ?? [];
|
|
32155
|
+
activity = mapCaliperEventGroupToActivity(matchedEvents, relevantCourseIds);
|
|
32156
|
+
}
|
|
31412
32157
|
} else {
|
|
32158
|
+
const events = await this.fetchCaliperEventsForStudent(client, studentId, gameSource, TimebackAdminService.MAX_RECENT_ACTIVITY_EVENT_FETCH);
|
|
31413
32159
|
matchedEvents = events.filter((event) => event.externalId === activityId);
|
|
31414
32160
|
if (matchedEvents.length > 0) {
|
|
31415
32161
|
activity = mapCaliperEventToRemediationActivity(matchedEvents[0], relevantCourseIds) ?? mapCaliperEventGroupToActivity(matchedEvents, relevantCourseIds);
|
|
@@ -31427,9 +32173,22 @@ class TimebackAdminService {
|
|
|
31427
32173
|
activities: [activity]
|
|
31428
32174
|
});
|
|
31429
32175
|
const gameMetricsComparison = activity.runId ? comparisons.get(activity.runId) : undefined;
|
|
32176
|
+
const verificationsByKey = gameMetricsComparison?.status === "discrepant" && activity.runId ? await this.getMetricDiscrepancyVerificationsByKey({
|
|
32177
|
+
gameId,
|
|
32178
|
+
courseId,
|
|
32179
|
+
studentId,
|
|
32180
|
+
runIds: [activity.runId]
|
|
32181
|
+
}) : new Map;
|
|
32182
|
+
const verification2 = getTimebackMetricDiscrepancyVerificationForActivity({
|
|
32183
|
+
studentId,
|
|
32184
|
+
activity,
|
|
32185
|
+
comparison: gameMetricsComparison,
|
|
32186
|
+
verificationsByKey
|
|
32187
|
+
});
|
|
31430
32188
|
const activityWithGameMetrics = gameMetricsComparison ? {
|
|
31431
32189
|
...activity,
|
|
31432
|
-
gameMetricsComparison: summarizeGameRunMetricsComparison(gameMetricsComparison)
|
|
32190
|
+
gameMetricsComparison: summarizeGameRunMetricsComparison(gameMetricsComparison),
|
|
32191
|
+
...verification2 ? { gameMetricsVerification: verification2 } : {}
|
|
31433
32192
|
} : activity;
|
|
31434
32193
|
return {
|
|
31435
32194
|
activity: activityWithGameMetrics,
|
|
@@ -31720,9 +32479,11 @@ var init_timeback_admin_service = __esm(() => {
|
|
|
31720
32479
|
init_types4();
|
|
31721
32480
|
init_utils6();
|
|
31722
32481
|
init_src4();
|
|
32482
|
+
init_timeback3();
|
|
31723
32483
|
init_errors();
|
|
31724
32484
|
init_timeback_admin_metrics_util();
|
|
31725
32485
|
init_timeback_admin_util();
|
|
32486
|
+
init_timeback_discrepancy_queue_util();
|
|
31726
32487
|
init_timeback_game_metrics_comparison_util();
|
|
31727
32488
|
init_timeback_mastery_completion_util();
|
|
31728
32489
|
init_timeback_util();
|
|
@@ -34995,6 +35756,9 @@ function createCaliperNamespace(client) {
|
|
|
34995
35756
|
if (params.actorEmail) {
|
|
34996
35757
|
query.set("actorEmail", params.actorEmail);
|
|
34997
35758
|
}
|
|
35759
|
+
if (params.sessionId) {
|
|
35760
|
+
query.set("sessionId", params.sessionId);
|
|
35761
|
+
}
|
|
34998
35762
|
if (params.extensions) {
|
|
34999
35763
|
for (const [key, value] of Object.entries(params.extensions)) {
|
|
35000
35764
|
query.set(`extensions.${key}`, value);
|
|
@@ -37363,7 +38127,7 @@ function buildTimebackClient() {
|
|
|
37363
38127
|
}
|
|
37364
38128
|
return;
|
|
37365
38129
|
}
|
|
37366
|
-
var
|
|
38130
|
+
var init_timeback4 = __esm(() => {
|
|
37367
38131
|
init_src2();
|
|
37368
38132
|
init_dist3();
|
|
37369
38133
|
init_config();
|
|
@@ -37371,7 +38135,7 @@ var init_timeback3 = __esm(() => {
|
|
|
37371
38135
|
|
|
37372
38136
|
// src/infrastructure/api/clients/index.ts
|
|
37373
38137
|
var init_clients = __esm(() => {
|
|
37374
|
-
|
|
38138
|
+
init_timeback4();
|
|
37375
38139
|
});
|
|
37376
38140
|
|
|
37377
38141
|
// src/infrastructure/api/providers/auth.provider.ts
|
|
@@ -92417,7 +93181,7 @@ async function seedTimebackIntegrations(db2, gameId, courses) {
|
|
|
92417
93181
|
}
|
|
92418
93182
|
return seededCount;
|
|
92419
93183
|
}
|
|
92420
|
-
var
|
|
93184
|
+
var init_timeback5 = __esm(() => {
|
|
92421
93185
|
init_tables_index();
|
|
92422
93186
|
init_config();
|
|
92423
93187
|
});
|
|
@@ -92508,7 +93272,7 @@ var init_games = __esm(() => {
|
|
|
92508
93272
|
init_tables_index();
|
|
92509
93273
|
init_constants();
|
|
92510
93274
|
init_logging();
|
|
92511
|
-
|
|
93275
|
+
init_timeback5();
|
|
92512
93276
|
});
|
|
92513
93277
|
|
|
92514
93278
|
// src/database/seed/index.ts
|
|
@@ -92534,7 +93298,7 @@ var init_seed = __esm(() => {
|
|
|
92534
93298
|
init_tables_index();
|
|
92535
93299
|
init_constants();
|
|
92536
93300
|
init_games();
|
|
92537
|
-
|
|
93301
|
+
init_timeback5();
|
|
92538
93302
|
init_games();
|
|
92539
93303
|
});
|
|
92540
93304
|
|
|
@@ -94237,12 +95001,13 @@ var init_session_controller = __esm(() => {
|
|
|
94237
95001
|
});
|
|
94238
95002
|
|
|
94239
95003
|
// ../api-core/src/controllers/timeback.controller.ts
|
|
94240
|
-
var logger45, populateStudent, getUser, getUserEnrollments, getUserById, setupIntegration, getIntegrations, updateIntegration, getIntegrationConfig, verifyIntegration, getConfig, deleteIntegrations, endActivity, heartbeat, advanceCourse, unenrollCourse, getStudentXp, getStudentMastery, getStudentHighestGradeMastered, getRoster, getStudentOverview, getGameMetrics, getStudentActivity, getActivityDetail, grantXp, adjustTime, adjustMastery, reconcileMasteryForConfigChange, searchStudents, enrollStudent, unenrollStudent, reactivateEnrollment, listAssessments, createAssessment, deleteAssessment, reorderAssessments, reorderQuestions, activateAssessment, deactivateAssessment, listQuestions, createQuestion, updateQuestion, deleteQuestion, getAssessmentBankStatus, destroyAssessmentBank, timeback2;
|
|
95004
|
+
var logger45, populateStudent, getUser, getUserEnrollments, getUserById, setupIntegration, getIntegrations, updateIntegration, getIntegrationConfig, verifyIntegration, getConfig, deleteIntegrations, endActivity, heartbeat, advanceCourse, unenrollCourse, getStudentXp, getStudentMastery, getStudentHighestGradeMastered, getRoster, getStudentOverview, getGameMetrics, getStudentActivity, getActivityDetail, listMetricDiscrepancies, verifyMetricDiscrepancy, grantXp, adjustTime, adjustMastery, reconcileMasteryForConfigChange, searchStudents, enrollStudent, unenrollStudent, reactivateEnrollment, listAssessments, createAssessment, deleteAssessment, reorderAssessments, reorderQuestions, activateAssessment, deactivateAssessment, listQuestions, createQuestion, updateQuestion, deleteQuestion, getAssessmentBankStatus, destroyAssessmentBank, timeback2;
|
|
94241
95005
|
var init_timeback_controller = __esm(() => {
|
|
94242
95006
|
init_esm();
|
|
94243
95007
|
init_schemas_index();
|
|
94244
95008
|
init_src2();
|
|
94245
95009
|
init_src4();
|
|
95010
|
+
init_timeback3();
|
|
94246
95011
|
init_errors();
|
|
94247
95012
|
init_utils11();
|
|
94248
95013
|
logger45 = log.scope("TimebackController");
|
|
@@ -94709,6 +95474,65 @@ var init_timeback_controller = __esm(() => {
|
|
|
94709
95474
|
runId
|
|
94710
95475
|
});
|
|
94711
95476
|
});
|
|
95477
|
+
listMetricDiscrepancies = requireGameManagementAccess(async (ctx) => {
|
|
95478
|
+
const gameId = ctx.params.gameId;
|
|
95479
|
+
const courseId = ctx.params.courseId;
|
|
95480
|
+
const requestedEventOffset = Number(ctx.url.searchParams.get("eventOffset"));
|
|
95481
|
+
const requestedWindow = ctx.url.searchParams.get("window");
|
|
95482
|
+
const startDate = ctx.url.searchParams.get("startDate") ?? undefined;
|
|
95483
|
+
const endDate = ctx.url.searchParams.get("endDate") ?? undefined;
|
|
95484
|
+
const studentId = ctx.url.searchParams.get("studentId")?.trim() || undefined;
|
|
95485
|
+
const discrepancyMetricScopes = parseTimebackDiscrepancyQueueMetrics(ctx.url.searchParams.getAll("discrepancyMetric"));
|
|
95486
|
+
const eventOffset = Number.isFinite(requestedEventOffset) && requestedEventOffset > 0 ? Math.trunc(requestedEventOffset) : 0;
|
|
95487
|
+
const window2 = parseTimebackDiscrepancyQueueWindow(requestedWindow);
|
|
95488
|
+
const includeVerified = ctx.url.searchParams.get("includeVerified") === "true";
|
|
95489
|
+
if (!gameId || !courseId) {
|
|
95490
|
+
throw ApiError.badRequest("Missing gameId or courseId path parameter");
|
|
95491
|
+
}
|
|
95492
|
+
logger45.debug("Listing metric discrepancies", {
|
|
95493
|
+
requesterId: ctx.user.id,
|
|
95494
|
+
gameId,
|
|
95495
|
+
courseId,
|
|
95496
|
+
window: window2,
|
|
95497
|
+
startDate,
|
|
95498
|
+
endDate,
|
|
95499
|
+
studentId,
|
|
95500
|
+
discrepancyMetricScopes,
|
|
95501
|
+
includeVerified,
|
|
95502
|
+
eventOffset
|
|
95503
|
+
});
|
|
95504
|
+
return ctx.services.timebackAdmin.listMetricDiscrepancies(ctx.user, {
|
|
95505
|
+
gameId,
|
|
95506
|
+
courseId,
|
|
95507
|
+
window: window2,
|
|
95508
|
+
startDate,
|
|
95509
|
+
endDate,
|
|
95510
|
+
studentId,
|
|
95511
|
+
discrepancyMetricScopes,
|
|
95512
|
+
includeVerified,
|
|
95513
|
+
eventOffset
|
|
95514
|
+
});
|
|
95515
|
+
});
|
|
95516
|
+
verifyMetricDiscrepancy = requireDeveloper(async (ctx) => {
|
|
95517
|
+
const gameId = ctx.params.gameId;
|
|
95518
|
+
const courseId = ctx.params.courseId;
|
|
95519
|
+
const body2 = await parseRequestBody(ctx.request, VerifyTimebackMetricDiscrepancyRequestSchema);
|
|
95520
|
+
if (!gameId || !courseId) {
|
|
95521
|
+
throw ApiError.badRequest("Missing gameId or courseId path parameter");
|
|
95522
|
+
}
|
|
95523
|
+
logger45.debug("Verifying metric discrepancy", {
|
|
95524
|
+
requesterId: ctx.user.id,
|
|
95525
|
+
gameId,
|
|
95526
|
+
courseId,
|
|
95527
|
+
studentId: body2.studentId,
|
|
95528
|
+
runId: body2.runId
|
|
95529
|
+
});
|
|
95530
|
+
return ctx.services.timebackAdmin.verifyMetricDiscrepancy(ctx.user, {
|
|
95531
|
+
gameId,
|
|
95532
|
+
courseId,
|
|
95533
|
+
data: body2
|
|
95534
|
+
});
|
|
95535
|
+
});
|
|
94712
95536
|
grantXp = requireDeveloper(async (ctx) => {
|
|
94713
95537
|
const body2 = await parseRequestBody(ctx.request, GrantTimebackXpRequestSchema);
|
|
94714
95538
|
logger45.debug("Granting manual XP", {
|
|
@@ -94952,6 +95776,8 @@ var init_timeback_controller = __esm(() => {
|
|
|
94952
95776
|
getGameMetrics,
|
|
94953
95777
|
getStudentActivity,
|
|
94954
95778
|
getActivityDetail,
|
|
95779
|
+
listMetricDiscrepancies,
|
|
95780
|
+
verifyMetricDiscrepancy,
|
|
94955
95781
|
grantXp,
|
|
94956
95782
|
adjustTime,
|
|
94957
95783
|
adjustMastery,
|
|
@@ -95189,7 +96015,7 @@ async function buildMockUserResponse(db2, user, gameId) {
|
|
|
95189
96015
|
timeback: timeback3
|
|
95190
96016
|
};
|
|
95191
96017
|
}
|
|
95192
|
-
var
|
|
96018
|
+
var init_timeback6 = __esm(() => {
|
|
95193
96019
|
init_drizzle_orm();
|
|
95194
96020
|
init_utils11();
|
|
95195
96021
|
init_tables_index();
|
|
@@ -95207,7 +96033,7 @@ var init_users = __esm(() => {
|
|
|
95207
96033
|
init_tables_index();
|
|
95208
96034
|
init_api();
|
|
95209
96035
|
init_error_handler();
|
|
95210
|
-
|
|
96036
|
+
init_timeback6();
|
|
95211
96037
|
usersRouter = new Hono2;
|
|
95212
96038
|
usersRouter.get("/me", async (c2) => {
|
|
95213
96039
|
const user = c2.get("user");
|
|
@@ -95832,15 +96658,16 @@ function hashCode(str) {
|
|
|
95832
96658
|
return Math.abs(hash);
|
|
95833
96659
|
}
|
|
95834
96660
|
var timebackRouter;
|
|
95835
|
-
var
|
|
96661
|
+
var init_timeback7 = __esm(() => {
|
|
95836
96662
|
init_dist4();
|
|
95837
96663
|
init_controllers();
|
|
95838
96664
|
init_errors();
|
|
95839
96665
|
init_utils11();
|
|
95840
96666
|
init_schemas_index();
|
|
96667
|
+
init_timeback3();
|
|
95841
96668
|
init_api();
|
|
95842
96669
|
init_error_handler();
|
|
95843
|
-
|
|
96670
|
+
init_timeback6();
|
|
95844
96671
|
timebackRouter = new Hono2;
|
|
95845
96672
|
timebackRouter.post("/populate-student", async (c2) => c2.json({
|
|
95846
96673
|
status: "no_record"
|
|
@@ -96163,7 +96990,7 @@ var init_routes = __esm(() => {
|
|
|
96163
96990
|
init_users();
|
|
96164
96991
|
init_games2();
|
|
96165
96992
|
init_leaderboard();
|
|
96166
|
-
|
|
96993
|
+
init_timeback7();
|
|
96167
96994
|
init_lti();
|
|
96168
96995
|
});
|
|
96169
96996
|
|