@vue-skuilder/common 0.1.25 → 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.
@@ -21,13 +21,26 @@ export type Performance = number | TaggedPerformance;
21
21
  * Questions that exercise multiple skills (e.g., spelling with multiple GPCs)
22
22
  * can provide individual scores per tag for granular ELO updates.
23
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
+ *
24
28
  * @example
25
29
  * // Spelling "cat" as "kat" - got 'a' and 't' right, but 'c' wrong
26
30
  * {
27
- * _global: 0.67, // 2/3 correct, used for SRS and global ELO
28
- * 'GPC-c-K': 0, // incorrect
29
- * 'GPC-a-AE': 1, // correct
30
- * 'GPC-t-T': 1, // correct
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)
31
44
  * }
32
45
  */
33
46
  export interface TaggedPerformance {
@@ -38,11 +51,15 @@ export interface TaggedPerformance {
38
51
  */
39
52
  _global: number;
40
53
  /**
41
- * Per-tag scores. Keys are tag IDs (e.g., 'GPC-c-K').
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
+ *
42
59
  * Tags not present on the card will be created dynamically.
43
- * Range: [0, 1] for each value.
60
+ * Count-only tags (null) do not update card ELO.
44
61
  */
45
- [tag: string]: number;
62
+ [tag: string]: number | null;
46
63
  }
47
64
  /**
48
65
  * Type guard to check if performance is structured (TaggedPerformance).
@@ -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;;;GAGG;AACH,MAAM,MAAM,WAAW,GAAG,MAAM,GAAG,iBAAiB,CAAC;AAErD;;;;;;;;;;;;;;GAcG;AACH,MAAM,WAAW,iBAAiB;IAChC;;;;OAIG;IACH,OAAO,EAAE,MAAM,CAAC;IAEhB;;;;OAIG;IACH,CAAC,GAAG,EAAE,MAAM,GAAG,MAAM,CAAC;CACvB;AAED;;GAEG;AACH,wBAAgB,mBAAmB,CAAC,CAAC,EAAE,WAAW,GAAG,CAAC,IAAI,iBAAiB,CAE1E"}
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 +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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"}
package/dist/elo.d.ts CHANGED
@@ -47,18 +47,31 @@ export declare function adjustCourseScores(aElo: Eloish, bElo: Eloish, userScore
47
47
  * Unlike adjustCourseScores which applies the same score to all tags,
48
48
  * this function allows different scores per tag for granular skill tracking.
49
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
+ *
50
54
  * @param aElo - User's current ELO (will be converted to CourseElo)
51
55
  * @param bElo - Card's current ELO (will be converted to CourseElo)
52
- * @param taggedPerformance - Object with _global score and per-tag scores
56
+ * @param taggedPerformance - Object with _global score and per-tag scores/null
53
57
  * @returns Updated user and card ELOs
54
58
  *
55
59
  * @example
56
60
  * // Spelling "cat" as "kat" - got 'a' and 't' right, but 'c' wrong
57
61
  * adjustCourseScoresPerTag(userElo, cardElo, {
58
62
  * _global: 0.67,
59
- * 'GPC-c-K': 0,
60
- * 'GPC-a-AE': 1,
61
- * 'GPC-t-T': 1,
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
62
75
  * });
63
76
  */
64
77
  export declare function adjustCourseScoresPerTag(aElo: Eloish, bElo: Eloish, taggedPerformance: TaggedPerformance): {
package/dist/elo.d.ts.map CHANGED
@@ -1 +1 @@
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;;;;;;;;;;;;;;;;;;;GAmBG;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,CA6CA"}
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
@@ -166,18 +166,31 @@ function adjustScores(userElo, cardElo, userScore) {
166
166
  * Unlike adjustCourseScores which applies the same score to all tags,
167
167
  * this function allows different scores per tag for granular skill tracking.
168
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
+ *
169
173
  * @param aElo - User's current ELO (will be converted to CourseElo)
170
174
  * @param bElo - Card's current ELO (will be converted to CourseElo)
171
- * @param taggedPerformance - Object with _global score and per-tag scores
175
+ * @param taggedPerformance - Object with _global score and per-tag scores/null
172
176
  * @returns Updated user and card ELOs
173
177
  *
174
178
  * @example
175
179
  * // Spelling "cat" as "kat" - got 'a' and 't' right, but 'c' wrong
176
180
  * adjustCourseScoresPerTag(userElo, cardElo, {
177
181
  * _global: 0.67,
178
- * 'GPC-c-K': 0,
179
- * 'GPC-a-AE': 1,
180
- * 'GPC-t-T': 1,
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
181
194
  * });
182
195
  */
183
196
  function adjustCourseScoresPerTag(aElo, bElo, taggedPerformance) {
@@ -191,6 +204,17 @@ function adjustCourseScoresPerTag(aElo, bElo, taggedPerformance) {
191
204
  for (const [key, tagScore] of Object.entries(taggedPerformance)) {
192
205
  if (key === '_global')
193
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
+ }
194
218
  if (typeof tagScore !== 'number' || tagScore < 0 || tagScore > 1) {
195
219
  throw new Error(`ELO tag score for '${key}' must be between 0 and 1 - received ${tagScore}`);
196
220
  }
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
@@ -155,18 +155,31 @@ function adjustScores(userElo, cardElo, userScore) {
155
155
  * Unlike adjustCourseScores which applies the same score to all tags,
156
156
  * this function allows different scores per tag for granular skill tracking.
157
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
+ *
158
162
  * @param aElo - User's current ELO (will be converted to CourseElo)
159
163
  * @param bElo - Card's current ELO (will be converted to CourseElo)
160
- * @param taggedPerformance - Object with _global score and per-tag scores
164
+ * @param taggedPerformance - Object with _global score and per-tag scores/null
161
165
  * @returns Updated user and card ELOs
162
166
  *
163
167
  * @example
164
168
  * // Spelling "cat" as "kat" - got 'a' and 't' right, but 'c' wrong
165
169
  * adjustCourseScoresPerTag(userElo, cardElo, {
166
170
  * _global: 0.67,
167
- * 'GPC-c-K': 0,
168
- * 'GPC-a-AE': 1,
169
- * 'GPC-t-T': 1,
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
170
183
  * });
171
184
  */
172
185
  export function adjustCourseScoresPerTag(aElo, bElo, taggedPerformance) {
@@ -180,6 +193,17 @@ export function adjustCourseScoresPerTag(aElo, bElo, taggedPerformance) {
180
193
  for (const [key, tagScore] of Object.entries(taggedPerformance)) {
181
194
  if (key === '_global')
182
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
+ }
183
207
  if (typeof tagScore !== 'number' || tagScore < 0 || tagScore > 1) {
184
208
  throw new Error(`ELO tag score for '${key}' must be between 0 and 1 - received ${tagScore}`);
185
209
  }
package/package.json CHANGED
@@ -3,7 +3,7 @@
3
3
  "publishConfig": {
4
4
  "access": "public"
5
5
  },
6
- "version": "0.1.25",
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.25"
39
+ "stableVersion": "0.1.26"
40
40
  }
@@ -124,13 +124,26 @@ export type Performance = number | TaggedPerformance;
124
124
  * Questions that exercise multiple skills (e.g., spelling with multiple GPCs)
125
125
  * can provide individual scores per tag for granular ELO updates.
126
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
+ *
127
131
  * @example
128
132
  * // Spelling "cat" as "kat" - got 'a' and 't' right, but 'c' wrong
129
133
  * {
130
- * _global: 0.67, // 2/3 correct, used for SRS and global ELO
131
- * 'GPC-c-K': 0, // incorrect
132
- * 'GPC-a-AE': 1, // correct
133
- * 'GPC-t-T': 1, // correct
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)
134
147
  * }
135
148
  */
136
149
  export interface TaggedPerformance {
@@ -142,11 +155,15 @@ export interface TaggedPerformance {
142
155
  _global: number;
143
156
 
144
157
  /**
145
- * Per-tag scores. Keys are tag IDs (e.g., 'GPC-c-K').
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
+ *
146
163
  * Tags not present on the card will be created dynamically.
147
- * Range: [0, 1] for each value.
164
+ * Count-only tags (null) do not update card ELO.
148
165
  */
149
- [tag: string]: number;
166
+ [tag: string]: number | null;
150
167
  }
151
168
 
152
169
  /**
package/src/elo.ts CHANGED
@@ -200,18 +200,31 @@ function adjustScores(
200
200
  * Unlike adjustCourseScores which applies the same score to all tags,
201
201
  * this function allows different scores per tag for granular skill tracking.
202
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
+ *
203
207
  * @param aElo - User's current ELO (will be converted to CourseElo)
204
208
  * @param bElo - Card's current ELO (will be converted to CourseElo)
205
- * @param taggedPerformance - Object with _global score and per-tag scores
209
+ * @param taggedPerformance - Object with _global score and per-tag scores/null
206
210
  * @returns Updated user and card ELOs
207
211
  *
208
212
  * @example
209
213
  * // Spelling "cat" as "kat" - got 'a' and 't' right, but 'c' wrong
210
214
  * adjustCourseScoresPerTag(userElo, cardElo, {
211
215
  * _global: 0.67,
212
- * 'GPC-c-K': 0,
213
- * 'GPC-a-AE': 1,
214
- * 'GPC-t-T': 1,
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
215
228
  * });
216
229
  */
217
230
  export function adjustCourseScoresPerTag(
@@ -235,6 +248,18 @@ export function adjustCourseScoresPerTag(
235
248
  for (const [key, tagScore] of Object.entries(taggedPerformance)) {
236
249
  if (key === '_global') continue;
237
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
+
238
263
  if (typeof tagScore !== 'number' || tagScore < 0 || tagScore > 1) {
239
264
  throw new Error(`ELO tag score for '${key}' must be between 0 and 1 - received ${tagScore}`);
240
265
  }