@playcademy/vite-plugin 0.1.35 → 0.1.36

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.
Files changed (2) hide show
  1. package/dist/index.js +192 -127
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -41199,7 +41199,7 @@ var import_picocolors7 = __toESM(require_picocolors(), 1);
41199
41199
  // package.json
41200
41200
  var package_default = {
41201
41201
  name: "@playcademy/vite-plugin",
41202
- version: "0.1.34",
41202
+ version: "0.1.35",
41203
41203
  type: "module",
41204
41204
  exports: {
41205
41205
  ".": {
@@ -190550,14 +190550,30 @@ function createOneRosterNamespace(client2) {
190550
190550
  update: async (sourcedId, data) => {
190551
190551
  return client2["request"](`${ONEROSTER_ENDPOINTS.assessmentResults}/${sourcedId}`, "PUT", data);
190552
190552
  },
190553
- getLatestForStudent: async (studentId, lineItemId) => {
190553
+ getAttemptStats: async (studentId, lineItemId) => {
190554
190554
  try {
190555
190555
  const filter2 = `student.sourcedId='${studentId}' AND assessmentLineItem.sourcedId='${lineItemId}'`;
190556
- const url = `${ONEROSTER_ENDPOINTS.assessmentResults}?filter=${encodeURIComponent(filter2)}&sort=scoreDate&orderBy=desc&limit=1`;
190556
+ const url = `${ONEROSTER_ENDPOINTS.assessmentResults}?filter=${encodeURIComponent(filter2)}`;
190557
190557
  const response = await client2["request"](url, "GET");
190558
- return response.assessmentResults[0] || null;
190558
+ const results = response.assessmentResults || [];
190559
+ if (results.length === 0)
190560
+ return null;
190561
+ let maxAttemptResult = results[0];
190562
+ let maxAttemptNumber = maxAttemptResult.metadata?.attemptNumber || 0;
190563
+ let activeAttemptCount = 0;
190564
+ for (const result of results) {
190565
+ const attemptNumber = result.metadata?.attemptNumber || 0;
190566
+ if (attemptNumber > maxAttemptNumber) {
190567
+ maxAttemptNumber = attemptNumber;
190568
+ maxAttemptResult = result;
190569
+ }
190570
+ if (result.status === "active") {
190571
+ activeAttemptCount++;
190572
+ }
190573
+ }
190574
+ return { maxAttemptNumber, activeAttemptCount, maxAttemptResult };
190559
190575
  } catch (error2) {
190560
- logTimebackError("query latest assessment result", error2, {
190576
+ logTimebackError("query attempt stats", error2, {
190561
190577
  studentId,
190562
190578
  lineItemId
190563
190579
  });
@@ -190895,9 +190911,6 @@ class TimebackCacheManager {
190895
190911
  log32.debug("[TimebackCacheManager] Cache cleanup completed");
190896
190912
  }
190897
190913
  }
190898
- function kebabToTitleCase(kebabStr) {
190899
- return kebabStr.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
190900
- }
190901
190914
  init_constants();
190902
190915
  init_constants();
190903
190916
  var isObject2 = (value) => typeof value === "object" && value !== null;
@@ -190910,6 +190923,152 @@ function isPlaycademyResourceMetadata(value) {
190910
190923
  }
190911
190924
  return isObject2(value.mastery);
190912
190925
  }
190926
+
190927
+ class MasteryTracker {
190928
+ cacheManager;
190929
+ onerosterNamespace;
190930
+ edubridgeNamespace;
190931
+ constructor(cacheManager, onerosterNamespace, edubridgeNamespace) {
190932
+ this.cacheManager = cacheManager;
190933
+ this.onerosterNamespace = onerosterNamespace;
190934
+ this.edubridgeNamespace = edubridgeNamespace;
190935
+ }
190936
+ async checkProgress(input) {
190937
+ const { studentId, courseId, resourceId, masteredUnits } = input;
190938
+ if (typeof masteredUnits !== "number" || masteredUnits <= 0) {
190939
+ return;
190940
+ }
190941
+ const masterableUnits = await this.resolveMasterableUnits(resourceId);
190942
+ if (!masterableUnits || masterableUnits <= 0) {
190943
+ log32.warn("[MasteryTracker] No masterableUnits configured for course", {
190944
+ courseId,
190945
+ resourceId
190946
+ });
190947
+ return;
190948
+ }
190949
+ const facts = await this.fetchEnrollmentAnalyticsFacts(studentId, courseId);
190950
+ if (!facts) {
190951
+ log32.warn("[MasteryTracker] Unable to retrieve analytics for mastery-based completion", {
190952
+ studentId,
190953
+ courseId
190954
+ });
190955
+ return;
190956
+ }
190957
+ const historicalMasteredUnits = this.sumAnalyticsMetric(facts, "masteredUnits");
190958
+ const totalMastered = historicalMasteredUnits + masteredUnits;
190959
+ const rawPct = totalMastered / masterableUnits * 100;
190960
+ const pctCompleteApp = Math.min(100, Math.max(0, Math.round(rawPct)));
190961
+ const masteryAchieved = totalMastered >= masterableUnits;
190962
+ return { pctCompleteApp, masteryAchieved };
190963
+ }
190964
+ async createCompletionEntry(studentId, courseId, classId, appName) {
190965
+ const ids = deriveSourcedIds(courseId);
190966
+ const lineItemId = `${ids.course}-mastery-completion-assessment`;
190967
+ const resultId = `${lineItemId}:${studentId}:completion`;
190968
+ try {
190969
+ await this.onerosterNamespace.assessmentLineItems.findOrCreate(lineItemId, {
190970
+ sourcedId: lineItemId,
190971
+ title: "Mastery Completion",
190972
+ status: ONEROSTER_STATUS.active,
190973
+ ...classId ? { class: { sourcedId: classId } } : { course: { sourcedId: ids.course } },
190974
+ ...ids.componentResource ? { componentResource: { sourcedId: ids.componentResource } } : {}
190975
+ });
190976
+ await this.onerosterNamespace.assessmentResults.upsert(resultId, {
190977
+ sourcedId: resultId,
190978
+ status: ONEROSTER_STATUS.active,
190979
+ assessmentLineItem: { sourcedId: lineItemId },
190980
+ student: { sourcedId: studentId },
190981
+ score: 100,
190982
+ scoreDate: new Date().toISOString(),
190983
+ scoreStatus: SCORE_STATUS.fullyGraded,
190984
+ inProgress: "false",
190985
+ metadata: {
190986
+ isMasteryCompletion: true,
190987
+ completedAt: new Date().toISOString(),
190988
+ appName
190989
+ }
190990
+ });
190991
+ log32.info("[MasteryTracker] Created mastery completion entry", {
190992
+ studentId,
190993
+ lineItemId,
190994
+ resultId
190995
+ });
190996
+ } catch (error2) {
190997
+ log32.error("[MasteryTracker] Failed to create mastery completion entry", {
190998
+ studentId,
190999
+ lineItemId,
191000
+ error: error2
191001
+ });
191002
+ }
191003
+ }
191004
+ async resolveMasterableUnits(resourceId) {
191005
+ if (!resourceId) {
191006
+ return;
191007
+ }
191008
+ const cached = this.cacheManager.getResourceMasterableUnits(resourceId);
191009
+ if (cached !== undefined) {
191010
+ return cached === null ? undefined : cached;
191011
+ }
191012
+ try {
191013
+ const resource = await this.onerosterNamespace.resources.get(resourceId);
191014
+ const playcademyMetadata = resource.metadata?.playcademy;
191015
+ if (!playcademyMetadata) {
191016
+ return;
191017
+ }
191018
+ const masterableUnits = isPlaycademyResourceMetadata(playcademyMetadata) ? playcademyMetadata.mastery?.masterableUnits : undefined;
191019
+ this.cacheManager.setResourceMasterableUnits(resourceId, masterableUnits ?? null);
191020
+ return masterableUnits;
191021
+ } catch (error2) {
191022
+ log32.error("[MasteryTracker] Failed to fetch resource metadata for mastery config", {
191023
+ resourceId,
191024
+ error: error2
191025
+ });
191026
+ this.cacheManager.setResourceMasterableUnits(resourceId, null);
191027
+ return;
191028
+ }
191029
+ }
191030
+ async fetchEnrollmentAnalyticsFacts(studentId, courseId) {
191031
+ try {
191032
+ const enrollments = await this.edubridgeNamespace.enrollments.listByUser(studentId);
191033
+ const enrollment = enrollments.find((e2) => e2.course.id === courseId);
191034
+ if (!enrollment) {
191035
+ log32.warn("[MasteryTracker] Enrollment not found for student/course", {
191036
+ studentId,
191037
+ courseId
191038
+ });
191039
+ return;
191040
+ }
191041
+ const analytics = await this.edubridgeNamespace.analytics.getEnrollmentFacts(enrollment.id);
191042
+ return analytics.facts;
191043
+ } catch (error2) {
191044
+ log32.error("[MasteryTracker] Failed to load enrollment analytics facts", {
191045
+ studentId,
191046
+ courseId,
191047
+ error: error2
191048
+ });
191049
+ return;
191050
+ }
191051
+ }
191052
+ sumAnalyticsMetric(facts, metric) {
191053
+ if (!facts) {
191054
+ return 0;
191055
+ }
191056
+ let total = 0;
191057
+ Object.values(facts).forEach((dateFacts) => {
191058
+ Object.values(dateFacts).forEach((subjectFacts) => {
191059
+ const metrics = subjectFacts.activityMetrics;
191060
+ if (metrics && typeof metrics[metric] === "number") {
191061
+ total += metrics[metric];
191062
+ }
191063
+ });
191064
+ });
191065
+ return total;
191066
+ }
191067
+ }
191068
+ function kebabToTitleCase(kebabStr) {
191069
+ return kebabStr.split("-").map((word) => word.charAt(0).toUpperCase() + word.slice(1)).join(" ");
191070
+ }
191071
+ init_constants();
190913
191072
  function validateProgressData(progressData) {
190914
191073
  if (!progressData.subject) {
190915
191074
  throw new ConfigurationError("subject", "Subject is required for Caliper events. Provide it in progressData.subject");
@@ -190963,13 +191122,13 @@ class ProgressRecorder {
190963
191122
  cacheManager;
190964
191123
  onerosterNamespace;
190965
191124
  caliperNamespace;
190966
- edubridgeNamespace;
190967
- constructor(studentResolver, cacheManager, onerosterNamespace, caliperNamespace, edubridgeNamespace) {
191125
+ masteryTracker;
191126
+ constructor(studentResolver, cacheManager, onerosterNamespace, caliperNamespace, masteryTracker) {
190968
191127
  this.studentResolver = studentResolver;
190969
191128
  this.cacheManager = cacheManager;
190970
191129
  this.onerosterNamespace = onerosterNamespace;
190971
191130
  this.caliperNamespace = caliperNamespace;
190972
- this.edubridgeNamespace = edubridgeNamespace;
191131
+ this.masteryTracker = masteryTracker;
190973
191132
  }
190974
191133
  async record(courseId, studentIdentifier, progressData) {
190975
191134
  validateProgressData(progressData);
@@ -190978,22 +191137,22 @@ class ProgressRecorder {
190978
191137
  const { score, totalQuestions, correctQuestions, xpEarned, masteredUnits, attemptNumber } = progressData;
190979
191138
  const actualLineItemId = await this.resolveAssessmentLineItem(activityId, activityName, progressData.classId, ids);
190980
191139
  const currentAttemptNumber = await this.resolveAttemptNumber(attemptNumber, score, studentId, actualLineItemId);
190981
- const isFirstAttempt = currentAttemptNumber === 1;
190982
- const calculatedXp = this.calculateXpForProgress(progressData, totalQuestions, correctQuestions, xpEarned, isFirstAttempt);
191140
+ const isFirstActiveAttempt = currentAttemptNumber === 1;
191141
+ const calculatedXp = this.calculateXpForProgress(progressData, totalQuestions, correctQuestions, xpEarned, isFirstActiveAttempt);
190983
191142
  let extensions = progressData.extensions;
190984
- const completionProgress = await this.buildCompletionProgress({
191143
+ const masteryProgress = await this.masteryTracker.checkProgress({
190985
191144
  studentId,
190986
191145
  courseId,
190987
- progressData,
190988
- resourceId: ids.resource
191146
+ resourceId: ids.resource,
191147
+ masteredUnits: progressData.masteredUnits ?? 0
190989
191148
  });
190990
191149
  let pctCompleteApp;
190991
191150
  let masteryAchieved = false;
190992
- let scoreStatus = SCORE_STATUS.partiallyGraded;
191151
+ let scoreStatus = SCORE_STATUS.fullyGraded;
190993
191152
  const inProgress = "false";
190994
- if (completionProgress) {
190995
- masteryAchieved = completionProgress.masteryAchieved;
190996
- pctCompleteApp = completionProgress.pctCompleteApp;
191153
+ if (masteryProgress) {
191154
+ masteryAchieved = masteryProgress.masteryAchieved;
191155
+ pctCompleteApp = masteryProgress.pctCompleteApp;
190997
191156
  extensions = {
190998
191157
  ...extensions || {},
190999
191158
  ...pctCompleteApp !== undefined ? { pctCompleteApp } : {}
@@ -191011,6 +191170,9 @@ class ProgressRecorder {
191011
191170
  attemptNumber: currentAttemptNumber
191012
191171
  });
191013
191172
  }
191173
+ if (masteryAchieved) {
191174
+ await this.masteryTracker.createCompletionEntry(studentId, courseId, progressData.classId, progressData.appName);
191175
+ }
191014
191176
  await this.emitCaliperEvent(studentId, studentEmail, activityId, activityName, ids.course, courseName, totalQuestions, correctQuestions, calculatedXp, masteredUnits, currentAttemptNumber, progressData, extensions);
191015
191177
  return {
191016
191178
  xpAwarded: calculatedXp,
@@ -191039,8 +191201,9 @@ class ProgressRecorder {
191039
191201
  return actualLineItemId;
191040
191202
  }
191041
191203
  async resolveAttemptNumber(providedAttemptNumber, score, studentId, lineItemId) {
191042
- if (providedAttemptNumber)
191204
+ if (providedAttemptNumber) {
191043
191205
  return providedAttemptNumber;
191206
+ }
191044
191207
  if (score !== undefined) {
191045
191208
  return this.determineAttemptNumber(studentId, lineItemId);
191046
191209
  }
@@ -191064,106 +191227,13 @@ class ProgressRecorder {
191064
191227
  }
191065
191228
  return 0;
191066
191229
  }
191067
- async buildCompletionProgress({
191068
- studentId,
191069
- courseId,
191070
- progressData,
191071
- resourceId
191072
- }) {
191073
- if (typeof progressData.masteredUnits !== "number" || progressData.masteredUnits <= 0) {
191074
- return;
191075
- }
191076
- const masterableUnits = await this.resolveMasterableUnits(resourceId);
191077
- if (!masterableUnits || masterableUnits <= 0) {
191078
- log32.warn("[ProgressRecorder] No masterableUnits configured for course", {
191079
- courseId,
191080
- resourceId
191081
- });
191082
- return;
191083
- }
191084
- const facts = await this.fetchEnrollmentAnalyticsFacts(studentId, courseId);
191085
- if (!facts) {
191086
- log32.warn("[ProgressRecorder] Unable to retrieve analytics for mastery-based completion", { studentId, courseId });
191087
- return;
191088
- }
191089
- const historicalMasteredUnits = this.sumAnalyticsMetric(facts, "masteredUnits");
191090
- const totalMastered = historicalMasteredUnits + progressData.masteredUnits;
191091
- const rawPct = totalMastered / masterableUnits * 100;
191092
- const pctCompleteApp = Math.min(100, Math.max(0, Math.round(rawPct)));
191093
- const masteryAchieved = totalMastered >= masterableUnits;
191094
- return { pctCompleteApp, masteryAchieved };
191095
- }
191096
- async fetchEnrollmentAnalyticsFacts(studentId, courseId) {
191097
- try {
191098
- const enrollments = await this.edubridgeNamespace.enrollments.listByUser(studentId);
191099
- const enrollment = enrollments.find((e2) => e2.course.id === courseId);
191100
- if (!enrollment) {
191101
- log32.warn("[ProgressRecorder] Enrollment not found for student/course", {
191102
- studentId,
191103
- courseId
191104
- });
191105
- return;
191106
- }
191107
- const analytics = await this.edubridgeNamespace.analytics.getEnrollmentFacts(enrollment.id);
191108
- return analytics.facts;
191109
- } catch (error2) {
191110
- log32.error("[ProgressRecorder] Failed to load enrollment analytics facts", {
191111
- studentId,
191112
- courseId,
191113
- error: error2
191114
- });
191115
- return;
191116
- }
191117
- }
191118
- sumAnalyticsMetric(facts, metric) {
191119
- if (!facts) {
191120
- return 0;
191121
- }
191122
- let total = 0;
191123
- Object.values(facts).forEach((dateFacts) => {
191124
- Object.values(dateFacts).forEach((subjectFacts) => {
191125
- const metrics = subjectFacts.activityMetrics;
191126
- if (metrics && typeof metrics[metric] === "number") {
191127
- total += metrics[metric];
191128
- }
191129
- });
191130
- });
191131
- return total;
191132
- }
191133
- async resolveMasterableUnits(resourceId) {
191134
- if (!resourceId) {
191135
- return;
191136
- }
191137
- const cached = this.cacheManager.getResourceMasterableUnits(resourceId);
191138
- if (cached !== undefined) {
191139
- return cached === null ? undefined : cached;
191140
- }
191141
- try {
191142
- const resource = await this.onerosterNamespace.resources.get(resourceId);
191143
- const playcademyMetadata = resource.metadata?.playcademy;
191144
- if (!playcademyMetadata) {
191145
- return;
191146
- }
191147
- const masterableUnits = isPlaycademyResourceMetadata(playcademyMetadata) ? playcademyMetadata.mastery?.masterableUnits : undefined;
191148
- this.cacheManager.setResourceMasterableUnits(resourceId, masterableUnits ?? null);
191149
- return masterableUnits;
191150
- } catch (error2) {
191151
- log32.error("[ProgressRecorder] Failed to fetch resource metadata for mastery config", {
191152
- resourceId,
191153
- error: error2
191154
- });
191155
- this.cacheManager.setResourceMasterableUnits(resourceId, null);
191156
- return;
191157
- }
191158
- }
191159
191230
  async getOrCreateLineItem(lineItemId, activityName, classId, ids) {
191160
191231
  try {
191161
191232
  const lineItem = await this.onerosterNamespace.assessmentLineItems.findOrCreate(lineItemId, {
191162
191233
  sourcedId: lineItemId,
191163
191234
  title: activityName,
191164
191235
  status: ONEROSTER_STATUS.active,
191165
- ...classId ? { class: { sourcedId: classId } } : { course: { sourcedId: ids.course } },
191166
- ...ids.componentResource ? { componentResource: { sourcedId: ids.componentResource } } : ids.component ? { component: { sourcedId: ids.component } } : {}
191236
+ ...classId ? { class: { sourcedId: classId } } : { course: { sourcedId: ids.course } }
191167
191237
  });
191168
191238
  if (!lineItem.sourcedId) {
191169
191239
  throw new TimebackError(`Assessment line item created but has no sourcedId. This should not happen and indicates an upstream API issue.`);
@@ -191181,21 +191251,15 @@ class ProgressRecorder {
191181
191251
  }
191182
191252
  }
191183
191253
  async determineAttemptNumber(studentId, lineItemId) {
191184
- const latestAttempt = await this.onerosterNamespace.assessmentResults.getLatestForStudent(studentId, lineItemId);
191185
- if (latestAttempt) {
191186
- const previousAttemptNumber = latestAttempt.metadata?.attemptNumber || 0;
191187
- const newAttemptNumber = previousAttemptNumber + 1;
191188
- log32.debug("[ProgressRecorder] Found previous attempt, incrementing", {
191189
- previousAttemptNumber,
191190
- newAttemptNumber,
191191
- previousScore: latestAttempt.score
191192
- });
191193
- return newAttemptNumber;
191254
+ const stats = await this.onerosterNamespace.assessmentResults.getAttemptStats(studentId, lineItemId);
191255
+ if (stats) {
191256
+ return stats.activeAttemptCount + 1;
191194
191257
  }
191195
191258
  return 1;
191196
191259
  }
191197
191260
  async createGradebookEntry(lineItemId, studentId, attemptNumber, score, totalQuestions, correctQuestions, xp, masteredUnits, scoreStatus, inProgress, appName) {
191198
- const resultId = `${lineItemId}:${studentId}:attempt-${attemptNumber}`;
191261
+ const timestamp4 = Date.now().toString(36);
191262
+ const resultId = `${lineItemId}:${studentId}:${timestamp4}`;
191199
191263
  await this.onerosterNamespace.assessmentResults.upsert(resultId, {
191200
191264
  sourcedId: resultId,
191201
191265
  status: ONEROSTER_STATUS.active,
@@ -195351,7 +195415,8 @@ class TimebackClient {
195351
195415
  this.edubridge = createEduBridgeNamespace(this);
195352
195416
  this.cacheManager = new TimebackCacheManager;
195353
195417
  this.studentResolver = new StudentResolver(this.cacheManager, this.oneroster);
195354
- this.progressRecorder = new ProgressRecorder(this.studentResolver, this.cacheManager, this.oneroster, this.caliper, this.edubridge);
195418
+ const masteryTracker = new MasteryTracker(this.cacheManager, this.oneroster, this.edubridge);
195419
+ this.progressRecorder = new ProgressRecorder(this.studentResolver, this.cacheManager, this.oneroster, this.caliper, masteryTracker);
195355
195420
  this.sessionRecorder = new SessionRecorder(this.studentResolver, this.caliper);
195356
195421
  if (this.credentials) {
195357
195422
  this._ensureAuthenticated().catch((error2) => {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@playcademy/vite-plugin",
3
- "version": "0.1.35",
3
+ "version": "0.1.36",
4
4
  "type": "module",
5
5
  "exports": {
6
6
  ".": {
@@ -21,7 +21,7 @@
21
21
  "dependencies": {
22
22
  "archiver": "^7.0.1",
23
23
  "picocolors": "^1.1.1",
24
- "playcademy": "0.14.22"
24
+ "playcademy": "0.14.23"
25
25
  },
26
26
  "devDependencies": {
27
27
  "@inquirer/prompts": "^7.8.6",