@vue-skuilder/common 0.1.24 → 0.1.26

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.
@@ -10,8 +10,59 @@ export interface Evaluation {
10
10
  isCorrect: boolean;
11
11
  performance: Performance;
12
12
  }
13
- type Performance = number | {
14
- [dimension: string]: Performance;
15
- };
16
- export {};
13
+ /**
14
+ * Performance can be a simple number (0-1) for overall score,
15
+ * or a structured object with per-tag granularity.
16
+ */
17
+ export type Performance = number | TaggedPerformance;
18
+ /**
19
+ * Structured performance with per-tag scoring.
20
+ *
21
+ * Questions that exercise multiple skills (e.g., spelling with multiple GPCs)
22
+ * can provide individual scores per tag for granular ELO updates.
23
+ *
24
+ * Tags can have scores (for exercise tags) or `null` (for count-only exposure tags).
25
+ * Count-only tags increment their count but maintain a sentinel score of -1,
26
+ * making them easily identifiable and preventing them from polluting real ELO data.
27
+ *
28
+ * @example
29
+ * // Spelling "cat" as "kat" - got 'a' and 't' right, but 'c' wrong
30
+ * {
31
+ * _global: 0.67, // 2/3 correct, used for SRS and global ELO
32
+ * 'gpc:exercise:c-K': 0, // incorrect
33
+ * 'gpc:exercise:a-AE': 1, // correct
34
+ * 'gpc:exercise:t-T': 1, // correct
35
+ * }
36
+ *
37
+ * @example
38
+ * // WhoSaidThat card exercising 'sh' while exposing distractors
39
+ * {
40
+ * _global: 1.0,
41
+ * 'gpc:exercise:sh-SH': 1.0, // exercised and correct
42
+ * 'gpc:expose:s-S': null, // count-only exposure (no score)
43
+ * 'gpc:expose:ch-CH': null, // count-only exposure (no score)
44
+ * }
45
+ */
46
+ export interface TaggedPerformance {
47
+ /**
48
+ * Overall score for SRS scheduling and global ELO updates.
49
+ * Required when using structured performance.
50
+ * Range: [0, 1]
51
+ */
52
+ _global: number;
53
+ /**
54
+ * Per-tag scores or count-only markers.
55
+ *
56
+ * - **Number (0-1)**: Tag is exercised; score updates via ELO formula
57
+ * - **null**: Count-only tag (e.g., exposure); increments count, score stays -1 (sentinel)
58
+ *
59
+ * Tags not present on the card will be created dynamically.
60
+ * Count-only tags (null) do not update card ELO.
61
+ */
62
+ [tag: string]: number | null;
63
+ }
64
+ /**
65
+ * Type guard to check if performance is structured (TaggedPerformance).
66
+ */
67
+ export declare function isTaggedPerformance(p: Performance): p is TaggedPerformance;
17
68
  //# sourceMappingURL=course-data.d.ts.map
@@ -1 +1 @@
1
- {"version":3,"file":"course-data.d.ts","sourceRoot":"","sources":["../src/course-data.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAW,MAAM,SAAS,CAAC;AAEnD,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAKtD,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,SAAS,EAGhB,IAAI,EAAE,GAAG,EACT,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EAAE,EACf,OAAO,CAAC,EAAE;IAAE,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,cAAc,CAAA;CAAE,GACrD,eAAe,CAsFjB;AAED;;GAEG;AAEH,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,OAAO,CAAC;IACnB,WAAW,EAAE,WAAW,CAAC;CAC1B;AAED,KAAK,WAAW,GACZ,MAAM,GACN;IACE,CAAC,SAAS,EAAE,MAAM,GAAG,WAAW,CAAC;CAClC,CAAC"}
1
+ {"version":3,"file":"course-data.d.ts","sourceRoot":"","sources":["../src/course-data.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,eAAe,EAAW,MAAM,SAAS,CAAC;AAEnD,OAAO,EAAE,SAAS,EAAE,MAAM,2BAA2B,CAAC;AAKtD,wBAAgB,aAAa,CAC3B,QAAQ,EAAE,MAAM,EAChB,UAAU,EAAE,MAAM,EAClB,KAAK,EAAE,SAAS,EAGhB,IAAI,EAAE,GAAG,EACT,MAAM,EAAE,MAAM,EACd,KAAK,EAAE,MAAM,EAAE,EACf,OAAO,CAAC,EAAE;IAAE,CAAC,CAAC,EAAE,MAAM,GAAG,OAAO,CAAC,IAAI,CAAC,cAAc,CAAA;CAAE,GACrD,eAAe,CAsFjB;AAED;;GAEG;AAEH,MAAM,WAAW,UAAU;IACzB,SAAS,EAAE,OAAO,CAAC;IACnB,WAAW,EAAE,WAAW,CAAC;CAC1B;AAED;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,iBAAiB,CAAC;AAErD;;;;;;;;;;;;;;;;;;;;;;;;;;;GA2BG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,OAAO,EAAE,MAAM,CAAC;IAEhB;;;;;;;;OAQG;IACH,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,GAAG,IAAI,CAAC;CAC9B;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,WAAW,GAAG,CAAC,IAAI,iBAAiB,CAE1E"}
@@ -1,6 +1,7 @@
1
1
  "use strict";
2
2
  Object.defineProperty(exports, "__esModule", { value: true });
3
3
  exports.prepareNote55 = prepareNote55;
4
+ exports.isTaggedPerformance = isTaggedPerformance;
4
5
  const db_js_1 = require("./db.js");
5
6
  const namespacer_js_1 = require("./namespacer.js");
6
7
  const FieldType_js_1 = require("./enums/FieldType.js");
@@ -84,4 +85,10 @@ data, author, _tags, uploads) {
84
85
  });
85
86
  return payload;
86
87
  }
88
+ /**
89
+ * Type guard to check if performance is structured (TaggedPerformance).
90
+ */
91
+ function isTaggedPerformance(p) {
92
+ return typeof p === 'object' && p !== null && '_global' in p;
93
+ }
87
94
  //# sourceMappingURL=course-data.js.map
@@ -1 +1 @@
1
- {"version":3,"file":"course-data.js","sourceRoot":"","sources":["../src/course-data.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"course-data.js","sourceRoot":"","sources":["../src/course-data.ts"],"names":[],"mappings":"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"}
@@ -81,4 +81,10 @@ data, author, _tags, uploads) {
81
81
  });
82
82
  return payload;
83
83
  }
84
+ /**
85
+ * Type guard to check if performance is structured (TaggedPerformance).
86
+ */
87
+ export function isTaggedPerformance(p) {
88
+ return typeof p === 'object' && p !== null && '_global' in p;
89
+ }
84
90
  //# sourceMappingURL=course-data.js.map
package/dist/elo.d.ts CHANGED
@@ -1,3 +1,4 @@
1
+ import { TaggedPerformance } from './course-data.js';
1
2
  export declare class EloRanker {
2
3
  k: number;
3
4
  constructor(k?: number);
@@ -40,5 +41,42 @@ export declare function adjustCourseScores(aElo: Eloish, bElo: Eloish, userScore
40
41
  userElo: CourseElo;
41
42
  cardElo: CourseElo;
42
43
  };
44
+ /**
45
+ * Adjusts ELO scores with per-tag granularity.
46
+ *
47
+ * Unlike adjustCourseScores which applies the same score to all tags,
48
+ * this function allows different scores per tag for granular skill tracking.
49
+ *
50
+ * Tags can be scored (number 0-1) or count-only (null). Count-only tags are
51
+ * useful for exposure tracking (e.g., gpc:expose:*) where we only care about
52
+ * "how many times has the user seen this?" without measuring performance.
53
+ *
54
+ * @param aElo - User's current ELO (will be converted to CourseElo)
55
+ * @param bElo - Card's current ELO (will be converted to CourseElo)
56
+ * @param taggedPerformance - Object with _global score and per-tag scores/null
57
+ * @returns Updated user and card ELOs
58
+ *
59
+ * @example
60
+ * // Spelling "cat" as "kat" - got 'a' and 't' right, but 'c' wrong
61
+ * adjustCourseScoresPerTag(userElo, cardElo, {
62
+ * _global: 0.67,
63
+ * 'gpc:exercise:c-K': 0,
64
+ * 'gpc:exercise:a-AE': 1,
65
+ * 'gpc:exercise:t-T': 1,
66
+ * });
67
+ *
68
+ * @example
69
+ * // WhoSaidThat - exercise target, expose distractors (count-only)
70
+ * adjustCourseScoresPerTag(userElo, cardElo, {
71
+ * _global: 1.0,
72
+ * 'gpc:exercise:sh-SH': 1.0,
73
+ * 'gpc:expose:s-S': null, // count-only
74
+ * 'gpc:expose:ch-CH': null, // count-only
75
+ * });
76
+ */
77
+ export declare function adjustCourseScoresPerTag(aElo: Eloish, bElo: Eloish, taggedPerformance: TaggedPerformance): {
78
+ userElo: CourseElo;
79
+ cardElo: CourseElo;
80
+ };
43
81
  export {};
44
82
  //# sourceMappingURL=elo.d.ts.map
package/dist/elo.d.ts.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"elo.d.ts","sourceRoot":"","sources":["../src/elo.ts"],"names":[],"mappings":"AAAA,qBAAa,SAAS;IACD,CAAC,EAAE,MAAM;gBAAT,CAAC,GAAE,MAAW;IAEjC,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI;IAG3B,UAAU,IAAI,MAAM;IAIpB,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM;IAGzC,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM;CAGxE;AAED,MAAM,MAAM,SAAS,GAAG;IACtB,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE;QACJ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;KAC1B,CAAC;IACF,IAAI,EAAE;QACJ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;KAC1B,CAAC;CACH,CAAC;AAEF,KAAK,OAAO,GAAG;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,KAAK,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC;AAE3C,wBAAgB,cAAc,IAAI,SAAS,CAS1C;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAS/C;AACD,wBAAgB,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,CASpD;AACD,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CA+B9D;AAED,wBAAgB,WAAW,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,SAAS,CAMtD;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE;IACR,UAAU,EAAE,OAAO,CAAC;CACrB,GACA;IACD,OAAO,EAAE,SAAS,CAAC;IACnB,OAAO,EAAE,SAAS,CAAC;CACpB,CA+BA"}
1
+ {"version":3,"file":"elo.d.ts","sourceRoot":"","sources":["../src/elo.ts"],"names":[],"mappings":"AAAA,OAAO,EAAE,iBAAiB,EAAE,MAAM,kBAAkB,CAAC;AAErD,qBAAa,SAAS;IACD,CAAC,EAAE,MAAM;gBAAT,CAAC,GAAE,MAAW;IAEjC,UAAU,CAAC,CAAC,EAAE,MAAM,GAAG,IAAI;IAG3B,UAAU,IAAI,MAAM;IAIpB,WAAW,CAAC,CAAC,EAAE,MAAM,EAAE,CAAC,EAAE,MAAM,GAAG,MAAM;IAGzC,YAAY,CAAC,QAAQ,EAAE,MAAM,EAAE,MAAM,EAAE,MAAM,EAAE,OAAO,EAAE,MAAM,GAAG,MAAM;CAGxE;AAED,MAAM,MAAM,SAAS,GAAG;IACtB,MAAM,EAAE,OAAO,CAAC;IAChB,IAAI,EAAE;QACJ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;KAC1B,CAAC;IACF,IAAI,EAAE;QACJ,CAAC,KAAK,EAAE,MAAM,GAAG,OAAO,CAAC;KAC1B,CAAC;CACH,CAAC;AAEF,KAAK,OAAO,GAAG;IACb,KAAK,EAAE,MAAM,CAAC;IACd,KAAK,EAAE,MAAM,CAAC;CACf,CAAC;AAEF,KAAK,MAAM,GAAG,MAAM,GAAG,OAAO,GAAG,SAAS,CAAC;AAE3C,wBAAgB,cAAc,IAAI,SAAS,CAS1C;AAED,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAS/C;AACD,wBAAgB,KAAK,CAAC,GAAG,EAAE,MAAM,GAAG,OAAO,GAAG,OAAO,CASpD;AACD,wBAAgB,WAAW,CAAC,GAAG,EAAE,MAAM,GAAG,SAAS,GAAG,SAAS,CA+B9D;AAED,wBAAgB,WAAW,CAAC,CAAC,EAAE,OAAO,GAAG,CAAC,IAAI,SAAS,CAMtD;AAED;;;;;;;;GAQG;AACH,wBAAgB,kBAAkB,CAChC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,SAAS,EAAE,MAAM,EACjB,OAAO,CAAC,EAAE;IACR,UAAU,EAAE,OAAO,CAAC;CACrB,GACA;IACD,OAAO,EAAE,SAAS,CAAC;IACnB,OAAO,EAAE,SAAS,CAAC;CACpB,CA+BA;AAqCD;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;;GAgCG;AACH,wBAAgB,wBAAwB,CACtC,IAAI,EAAE,MAAM,EACZ,IAAI,EAAE,MAAM,EACZ,iBAAiB,EAAE,iBAAiB,GACnC;IACD,OAAO,EAAE,SAAS,CAAC;IACnB,OAAO,EAAE,SAAS,CAAC;CACpB,CAyDA"}
package/dist/elo.js CHANGED
@@ -7,6 +7,7 @@ exports.toElo = toElo;
7
7
  exports.toCourseElo = toCourseElo;
8
8
  exports.isCourseElo = isCourseElo;
9
9
  exports.adjustCourseScores = adjustCourseScores;
10
+ exports.adjustCourseScoresPerTag = adjustCourseScoresPerTag;
10
11
  class EloRanker {
11
12
  k;
12
13
  constructor(k = 32) {
@@ -159,4 +160,86 @@ function adjustScores(userElo, cardElo, userScore) {
159
160
  },
160
161
  };
161
162
  }
163
+ /**
164
+ * Adjusts ELO scores with per-tag granularity.
165
+ *
166
+ * Unlike adjustCourseScores which applies the same score to all tags,
167
+ * this function allows different scores per tag for granular skill tracking.
168
+ *
169
+ * Tags can be scored (number 0-1) or count-only (null). Count-only tags are
170
+ * useful for exposure tracking (e.g., gpc:expose:*) where we only care about
171
+ * "how many times has the user seen this?" without measuring performance.
172
+ *
173
+ * @param aElo - User's current ELO (will be converted to CourseElo)
174
+ * @param bElo - Card's current ELO (will be converted to CourseElo)
175
+ * @param taggedPerformance - Object with _global score and per-tag scores/null
176
+ * @returns Updated user and card ELOs
177
+ *
178
+ * @example
179
+ * // Spelling "cat" as "kat" - got 'a' and 't' right, but 'c' wrong
180
+ * adjustCourseScoresPerTag(userElo, cardElo, {
181
+ * _global: 0.67,
182
+ * 'gpc:exercise:c-K': 0,
183
+ * 'gpc:exercise:a-AE': 1,
184
+ * 'gpc:exercise:t-T': 1,
185
+ * });
186
+ *
187
+ * @example
188
+ * // WhoSaidThat - exercise target, expose distractors (count-only)
189
+ * adjustCourseScoresPerTag(userElo, cardElo, {
190
+ * _global: 1.0,
191
+ * 'gpc:exercise:sh-SH': 1.0,
192
+ * 'gpc:expose:s-S': null, // count-only
193
+ * 'gpc:expose:ch-CH': null, // count-only
194
+ * });
195
+ */
196
+ function adjustCourseScoresPerTag(aElo, bElo, taggedPerformance) {
197
+ const globalScore = taggedPerformance._global;
198
+ if (globalScore < 0 || globalScore > 1) {
199
+ throw new Error(`ELO _global score must be between 0 and 1 - received ${globalScore}`);
200
+ }
201
+ const userElo = toCourseElo(aElo);
202
+ const cardElo = toCourseElo(bElo);
203
+ // Process each tag in the performance object
204
+ for (const [key, tagScore] of Object.entries(taggedPerformance)) {
205
+ if (key === '_global')
206
+ continue;
207
+ // Count-only tag (exposure tracking): increment count, use -1 sentinel score
208
+ if (tagScore === null) {
209
+ userElo.tags[key] = userElo.tags[key] ?? { count: 0, score: -1 };
210
+ userElo.tags[key] = {
211
+ ...userElo.tags[key],
212
+ count: userElo.tags[key].count + 1,
213
+ score: -1, // Sentinel: clearly not a real ELO score
214
+ };
215
+ // Skip card ELO update for count-only tags
216
+ continue;
217
+ }
218
+ if (typeof tagScore !== 'number' || tagScore < 0 || tagScore > 1) {
219
+ throw new Error(`ELO tag score for '${key}' must be between 0 and 1 - received ${tagScore}`);
220
+ }
221
+ // Initialize tag ELO on user if missing (use global score as baseline)
222
+ const userTagElo = userElo.tags[key] ?? {
223
+ count: 0,
224
+ score: userElo.global.score,
225
+ };
226
+ // Initialize tag ELO on card if missing (use global score as baseline)
227
+ const cardTagElo = cardElo.tags[key] ?? {
228
+ count: 0,
229
+ score: cardElo.global.score,
230
+ };
231
+ // Apply per-tag score
232
+ const adjusted = adjustScores(userTagElo, cardTagElo, tagScore);
233
+ userElo.tags[key] = adjusted.userElo;
234
+ cardElo.tags[key] = adjusted.cardElo;
235
+ }
236
+ // Apply global score to global ELO
237
+ const adjustedGlobal = adjustScores(userElo.global, cardElo.global, globalScore);
238
+ userElo.global = adjustedGlobal.userElo;
239
+ cardElo.global = adjustedGlobal.cardElo;
240
+ return {
241
+ userElo,
242
+ cardElo,
243
+ };
244
+ }
162
245
  //# sourceMappingURL=elo.js.map
package/dist/elo.js.map CHANGED
@@ -1 +1 @@
1
- {"version":3,"file":"elo.js","sourceRoot":"","sources":["../src/elo.ts"],"names":[],"mappings":"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"}
1
+ {"version":3,"file":"elo.js","sourceRoot":"","sources":["../src/elo.ts"],"names":[],"mappings":"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"}
package/dist/elo.mjs CHANGED
@@ -149,4 +149,86 @@ function adjustScores(userElo, cardElo, userScore) {
149
149
  },
150
150
  };
151
151
  }
152
+ /**
153
+ * Adjusts ELO scores with per-tag granularity.
154
+ *
155
+ * Unlike adjustCourseScores which applies the same score to all tags,
156
+ * this function allows different scores per tag for granular skill tracking.
157
+ *
158
+ * Tags can be scored (number 0-1) or count-only (null). Count-only tags are
159
+ * useful for exposure tracking (e.g., gpc:expose:*) where we only care about
160
+ * "how many times has the user seen this?" without measuring performance.
161
+ *
162
+ * @param aElo - User's current ELO (will be converted to CourseElo)
163
+ * @param bElo - Card's current ELO (will be converted to CourseElo)
164
+ * @param taggedPerformance - Object with _global score and per-tag scores/null
165
+ * @returns Updated user and card ELOs
166
+ *
167
+ * @example
168
+ * // Spelling "cat" as "kat" - got 'a' and 't' right, but 'c' wrong
169
+ * adjustCourseScoresPerTag(userElo, cardElo, {
170
+ * _global: 0.67,
171
+ * 'gpc:exercise:c-K': 0,
172
+ * 'gpc:exercise:a-AE': 1,
173
+ * 'gpc:exercise:t-T': 1,
174
+ * });
175
+ *
176
+ * @example
177
+ * // WhoSaidThat - exercise target, expose distractors (count-only)
178
+ * adjustCourseScoresPerTag(userElo, cardElo, {
179
+ * _global: 1.0,
180
+ * 'gpc:exercise:sh-SH': 1.0,
181
+ * 'gpc:expose:s-S': null, // count-only
182
+ * 'gpc:expose:ch-CH': null, // count-only
183
+ * });
184
+ */
185
+ export function adjustCourseScoresPerTag(aElo, bElo, taggedPerformance) {
186
+ const globalScore = taggedPerformance._global;
187
+ if (globalScore < 0 || globalScore > 1) {
188
+ throw new Error(`ELO _global score must be between 0 and 1 - received ${globalScore}`);
189
+ }
190
+ const userElo = toCourseElo(aElo);
191
+ const cardElo = toCourseElo(bElo);
192
+ // Process each tag in the performance object
193
+ for (const [key, tagScore] of Object.entries(taggedPerformance)) {
194
+ if (key === '_global')
195
+ continue;
196
+ // Count-only tag (exposure tracking): increment count, use -1 sentinel score
197
+ if (tagScore === null) {
198
+ userElo.tags[key] = userElo.tags[key] ?? { count: 0, score: -1 };
199
+ userElo.tags[key] = {
200
+ ...userElo.tags[key],
201
+ count: userElo.tags[key].count + 1,
202
+ score: -1, // Sentinel: clearly not a real ELO score
203
+ };
204
+ // Skip card ELO update for count-only tags
205
+ continue;
206
+ }
207
+ if (typeof tagScore !== 'number' || tagScore < 0 || tagScore > 1) {
208
+ throw new Error(`ELO tag score for '${key}' must be between 0 and 1 - received ${tagScore}`);
209
+ }
210
+ // Initialize tag ELO on user if missing (use global score as baseline)
211
+ const userTagElo = userElo.tags[key] ?? {
212
+ count: 0,
213
+ score: userElo.global.score,
214
+ };
215
+ // Initialize tag ELO on card if missing (use global score as baseline)
216
+ const cardTagElo = cardElo.tags[key] ?? {
217
+ count: 0,
218
+ score: cardElo.global.score,
219
+ };
220
+ // Apply per-tag score
221
+ const adjusted = adjustScores(userTagElo, cardTagElo, tagScore);
222
+ userElo.tags[key] = adjusted.userElo;
223
+ cardElo.tags[key] = adjusted.cardElo;
224
+ }
225
+ // Apply global score to global ELO
226
+ const adjustedGlobal = adjustScores(userElo.global, cardElo.global, globalScore);
227
+ userElo.global = adjustedGlobal.userElo;
228
+ cardElo.global = adjustedGlobal.cardElo;
229
+ return {
230
+ userElo,
231
+ cardElo,
232
+ };
233
+ }
152
234
  //# sourceMappingURL=elo.js.map
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.1.24",
6
+ "version": "0.1.26",
7
7
  "type": "module",
8
8
  "main": "dist/index.js",
9
9
  "module": "dist/index.mjs",
@@ -36,5 +36,5 @@
36
36
  "zod": "^3.23.8",
37
37
  "zod-to-json-schema": "^3.23.5"
38
38
  },
39
- "stableVersion": "0.1.24"
39
+ "stableVersion": "0.1.26"
40
40
  }
@@ -112,8 +112,63 @@ export interface Evaluation {
112
112
  performance: Performance;
113
113
  }
114
114
 
115
- type Performance =
116
- | number
117
- | {
118
- [dimension: string]: Performance;
119
- };
115
+ /**
116
+ * Performance can be a simple number (0-1) for overall score,
117
+ * or a structured object with per-tag granularity.
118
+ */
119
+ export type Performance = number | TaggedPerformance;
120
+
121
+ /**
122
+ * Structured performance with per-tag scoring.
123
+ *
124
+ * Questions that exercise multiple skills (e.g., spelling with multiple GPCs)
125
+ * can provide individual scores per tag for granular ELO updates.
126
+ *
127
+ * Tags can have scores (for exercise tags) or `null` (for count-only exposure tags).
128
+ * Count-only tags increment their count but maintain a sentinel score of -1,
129
+ * making them easily identifiable and preventing them from polluting real ELO data.
130
+ *
131
+ * @example
132
+ * // Spelling "cat" as "kat" - got 'a' and 't' right, but 'c' wrong
133
+ * {
134
+ * _global: 0.67, // 2/3 correct, used for SRS and global ELO
135
+ * 'gpc:exercise:c-K': 0, // incorrect
136
+ * 'gpc:exercise:a-AE': 1, // correct
137
+ * 'gpc:exercise:t-T': 1, // correct
138
+ * }
139
+ *
140
+ * @example
141
+ * // WhoSaidThat card exercising 'sh' while exposing distractors
142
+ * {
143
+ * _global: 1.0,
144
+ * 'gpc:exercise:sh-SH': 1.0, // exercised and correct
145
+ * 'gpc:expose:s-S': null, // count-only exposure (no score)
146
+ * 'gpc:expose:ch-CH': null, // count-only exposure (no score)
147
+ * }
148
+ */
149
+ export interface TaggedPerformance {
150
+ /**
151
+ * Overall score for SRS scheduling and global ELO updates.
152
+ * Required when using structured performance.
153
+ * Range: [0, 1]
154
+ */
155
+ _global: number;
156
+
157
+ /**
158
+ * Per-tag scores or count-only markers.
159
+ *
160
+ * - **Number (0-1)**: Tag is exercised; score updates via ELO formula
161
+ * - **null**: Count-only tag (e.g., exposure); increments count, score stays -1 (sentinel)
162
+ *
163
+ * Tags not present on the card will be created dynamically.
164
+ * Count-only tags (null) do not update card ELO.
165
+ */
166
+ [tag: string]: number | null;
167
+ }
168
+
169
+ /**
170
+ * Type guard to check if performance is structured (TaggedPerformance).
171
+ */
172
+ export function isTaggedPerformance(p: Performance): p is TaggedPerformance {
173
+ return typeof p === 'object' && p !== null && '_global' in p;
174
+ }
package/src/elo.ts CHANGED
@@ -1,3 +1,5 @@
1
+ import { TaggedPerformance } from './course-data.js';
2
+
1
3
  export class EloRanker {
2
4
  constructor(public k: number = 32) {}
3
5
 
@@ -191,3 +193,102 @@ function adjustScores(
191
193
  },
192
194
  };
193
195
  }
196
+
197
+ /**
198
+ * Adjusts ELO scores with per-tag granularity.
199
+ *
200
+ * Unlike adjustCourseScores which applies the same score to all tags,
201
+ * this function allows different scores per tag for granular skill tracking.
202
+ *
203
+ * Tags can be scored (number 0-1) or count-only (null). Count-only tags are
204
+ * useful for exposure tracking (e.g., gpc:expose:*) where we only care about
205
+ * "how many times has the user seen this?" without measuring performance.
206
+ *
207
+ * @param aElo - User's current ELO (will be converted to CourseElo)
208
+ * @param bElo - Card's current ELO (will be converted to CourseElo)
209
+ * @param taggedPerformance - Object with _global score and per-tag scores/null
210
+ * @returns Updated user and card ELOs
211
+ *
212
+ * @example
213
+ * // Spelling "cat" as "kat" - got 'a' and 't' right, but 'c' wrong
214
+ * adjustCourseScoresPerTag(userElo, cardElo, {
215
+ * _global: 0.67,
216
+ * 'gpc:exercise:c-K': 0,
217
+ * 'gpc:exercise:a-AE': 1,
218
+ * 'gpc:exercise:t-T': 1,
219
+ * });
220
+ *
221
+ * @example
222
+ * // WhoSaidThat - exercise target, expose distractors (count-only)
223
+ * adjustCourseScoresPerTag(userElo, cardElo, {
224
+ * _global: 1.0,
225
+ * 'gpc:exercise:sh-SH': 1.0,
226
+ * 'gpc:expose:s-S': null, // count-only
227
+ * 'gpc:expose:ch-CH': null, // count-only
228
+ * });
229
+ */
230
+ export function adjustCourseScoresPerTag(
231
+ aElo: Eloish,
232
+ bElo: Eloish,
233
+ taggedPerformance: TaggedPerformance
234
+ ): {
235
+ userElo: CourseElo;
236
+ cardElo: CourseElo;
237
+ } {
238
+ const globalScore = taggedPerformance._global;
239
+
240
+ if (globalScore < 0 || globalScore > 1) {
241
+ throw new Error(`ELO _global score must be between 0 and 1 - received ${globalScore}`);
242
+ }
243
+
244
+ const userElo: CourseElo = toCourseElo(aElo);
245
+ const cardElo: CourseElo = toCourseElo(bElo);
246
+
247
+ // Process each tag in the performance object
248
+ for (const [key, tagScore] of Object.entries(taggedPerformance)) {
249
+ if (key === '_global') continue;
250
+
251
+ // Count-only tag (exposure tracking): increment count, use -1 sentinel score
252
+ if (tagScore === null) {
253
+ userElo.tags[key] = userElo.tags[key] ?? { count: 0, score: -1 };
254
+ userElo.tags[key] = {
255
+ ...userElo.tags[key],
256
+ count: userElo.tags[key].count + 1,
257
+ score: -1, // Sentinel: clearly not a real ELO score
258
+ };
259
+ // Skip card ELO update for count-only tags
260
+ continue;
261
+ }
262
+
263
+ if (typeof tagScore !== 'number' || tagScore < 0 || tagScore > 1) {
264
+ throw new Error(`ELO tag score for '${key}' must be between 0 and 1 - received ${tagScore}`);
265
+ }
266
+
267
+ // Initialize tag ELO on user if missing (use global score as baseline)
268
+ const userTagElo: EloRank = userElo.tags[key] ?? {
269
+ count: 0,
270
+ score: userElo.global.score,
271
+ };
272
+
273
+ // Initialize tag ELO on card if missing (use global score as baseline)
274
+ const cardTagElo: EloRank = cardElo.tags[key] ?? {
275
+ count: 0,
276
+ score: cardElo.global.score,
277
+ };
278
+
279
+ // Apply per-tag score
280
+ const adjusted = adjustScores(userTagElo, cardTagElo, tagScore);
281
+ userElo.tags[key] = adjusted.userElo;
282
+ cardElo.tags[key] = adjusted.cardElo;
283
+ }
284
+
285
+ // Apply global score to global ELO
286
+ const adjustedGlobal = adjustScores(userElo.global, cardElo.global, globalScore);
287
+ userElo.global = adjustedGlobal.userElo;
288
+ cardElo.global = adjustedGlobal.cardElo;
289
+
290
+ return {
291
+ userElo,
292
+ cardElo,
293
+ };
294
+ }