@shaxpir/duiduidui-models 1.9.20 → 1.9.22
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/models/Collection.d.ts +7 -8
- package/dist/models/Collection.js +12 -20
- package/dist/models/Progress.d.ts +8 -14
- package/dist/models/Progress.js +26 -44
- package/dist/models/Session.d.ts +2 -2
- package/dist/models/SkillLevel.d.ts +108 -34
- package/dist/models/SkillLevel.js +139 -53
- package/package.json +1 -2
|
@@ -3,7 +3,7 @@ import { CompactDateTime } from "@shaxpir/shaxpir-common";
|
|
|
3
3
|
import { ShareSync } from '../repo';
|
|
4
4
|
import { Conditions } from './Condition';
|
|
5
5
|
import { Content, ContentBody, ContentId, ContentMeta } from "./Content";
|
|
6
|
-
import {
|
|
6
|
+
import { SkillLevel } from './SkillLevel';
|
|
7
7
|
/**
|
|
8
8
|
* Display metadata for a Collection, used in the UI.
|
|
9
9
|
*/
|
|
@@ -23,7 +23,7 @@ export interface CollectionPayload {
|
|
|
23
23
|
collection_key: string;
|
|
24
24
|
conditions: Conditions;
|
|
25
25
|
display: CollectionDisplay;
|
|
26
|
-
proficiency:
|
|
26
|
+
proficiency: SkillLevel;
|
|
27
27
|
term_scores: {
|
|
28
28
|
[termKey: string]: number;
|
|
29
29
|
};
|
|
@@ -77,10 +77,9 @@ export declare class Collection extends Content {
|
|
|
77
77
|
get description(): string;
|
|
78
78
|
get icon(): string | undefined;
|
|
79
79
|
get sortOrder(): number;
|
|
80
|
-
get proficiency():
|
|
81
|
-
get
|
|
82
|
-
get
|
|
83
|
-
get proficiencyVolatility(): number;
|
|
80
|
+
get proficiency(): SkillLevel;
|
|
81
|
+
get proficiencyMu(): number;
|
|
82
|
+
get proficiencySigma(): number;
|
|
84
83
|
get termScores(): {
|
|
85
84
|
[termKey: string]: number;
|
|
86
85
|
};
|
|
@@ -125,9 +124,9 @@ export declare class Collection extends Content {
|
|
|
125
124
|
F: number;
|
|
126
125
|
};
|
|
127
126
|
/**
|
|
128
|
-
* Update the
|
|
127
|
+
* Update the IRT proficiency rating.
|
|
129
128
|
*/
|
|
130
|
-
setProficiency(value:
|
|
129
|
+
setProficiency(value: SkillLevel): void;
|
|
131
130
|
/**
|
|
132
131
|
* Update or add a term score in the collection.
|
|
133
132
|
*/
|
|
@@ -7,6 +7,7 @@ const SenseRankEncoder_1 = require("../util/SenseRankEncoder");
|
|
|
7
7
|
const Content_1 = require("./Content");
|
|
8
8
|
const ContentKind_1 = require("./ContentKind");
|
|
9
9
|
const Operation_1 = require("./Operation");
|
|
10
|
+
const SkillLevel_1 = require("./SkillLevel");
|
|
10
11
|
class Collection extends Content_1.Content {
|
|
11
12
|
/**
|
|
12
13
|
* Generate a deterministic Collection ID from userId and collectionKey.
|
|
@@ -50,12 +51,8 @@ class Collection extends Content_1.Content {
|
|
|
50
51
|
collection_key: params.collectionKey,
|
|
51
52
|
conditions: params.conditions,
|
|
52
53
|
display: params.display,
|
|
53
|
-
// Initial
|
|
54
|
-
proficiency:
|
|
55
|
-
rating: 0, // Start at zero (no demonstrated skill)
|
|
56
|
-
rating_deviation: 50, // High initial uncertainty
|
|
57
|
-
volatility: 0.06 // Moderate initial volatility
|
|
58
|
-
},
|
|
54
|
+
// Initial IRT proficiency - high uncertainty, no data yet
|
|
55
|
+
proficiency: SkillLevel_1.SkillLevelModel.createDefault(),
|
|
59
56
|
// Empty term scores map - populated as user reviews cards
|
|
60
57
|
term_scores: {},
|
|
61
58
|
// Progress starts at zero
|
|
@@ -118,17 +115,13 @@ class Collection extends Content_1.Content {
|
|
|
118
115
|
this.checkDisposed("Collection.proficiency");
|
|
119
116
|
return shaxpir_common_1.Struct.clone(this.payload.proficiency);
|
|
120
117
|
}
|
|
121
|
-
get
|
|
122
|
-
this.checkDisposed("Collection.
|
|
123
|
-
return this.payload.proficiency.
|
|
118
|
+
get proficiencyMu() {
|
|
119
|
+
this.checkDisposed("Collection.proficiencyMu");
|
|
120
|
+
return this.payload.proficiency.mu;
|
|
124
121
|
}
|
|
125
|
-
get
|
|
126
|
-
this.checkDisposed("Collection.
|
|
127
|
-
return this.payload.proficiency.
|
|
128
|
-
}
|
|
129
|
-
get proficiencyVolatility() {
|
|
130
|
-
this.checkDisposed("Collection.proficiencyVolatility");
|
|
131
|
-
return this.payload.proficiency.volatility;
|
|
122
|
+
get proficiencySigma() {
|
|
123
|
+
this.checkDisposed("Collection.proficiencySigma");
|
|
124
|
+
return this.payload.proficiency.sigma;
|
|
132
125
|
}
|
|
133
126
|
// ============================================================
|
|
134
127
|
// Term scores getters
|
|
@@ -232,15 +225,14 @@ class Collection extends Content_1.Content {
|
|
|
232
225
|
// Mutators
|
|
233
226
|
// ============================================================
|
|
234
227
|
/**
|
|
235
|
-
* Update the
|
|
228
|
+
* Update the IRT proficiency rating.
|
|
236
229
|
*/
|
|
237
230
|
setProficiency(value) {
|
|
238
231
|
this.checkDisposed("Collection.setProficiency");
|
|
239
232
|
if (!shaxpir_common_1.Struct.equals(this.payload.proficiency, value)) {
|
|
240
233
|
const batch = new Operation_1.BatchOperation(this);
|
|
241
|
-
batch.setPathValue(['payload', 'proficiency', '
|
|
242
|
-
batch.setPathValue(['payload', 'proficiency', '
|
|
243
|
-
batch.setPathValue(['payload', 'proficiency', 'volatility'], value.volatility);
|
|
234
|
+
batch.setPathValue(['payload', 'proficiency', 'mu'], value.mu);
|
|
235
|
+
batch.setPathValue(['payload', 'proficiency', 'sigma'], value.sigma);
|
|
244
236
|
batch.commit();
|
|
245
237
|
}
|
|
246
238
|
}
|
|
@@ -2,11 +2,7 @@ import { Doc } from '@shaxpir/sharedb/lib/client';
|
|
|
2
2
|
import { CompactDate } from "@shaxpir/shaxpir-common";
|
|
3
3
|
import { ShareSync } from '../repo';
|
|
4
4
|
import { Content, ContentBody, ContentId, ContentMeta } from "./Content";
|
|
5
|
-
|
|
6
|
-
rating: number;
|
|
7
|
-
rating_deviation: number;
|
|
8
|
-
volatility: number;
|
|
9
|
-
}
|
|
5
|
+
import { SkillLevel } from './SkillLevel';
|
|
10
6
|
export interface StreakData {
|
|
11
7
|
daily: {
|
|
12
8
|
current: number;
|
|
@@ -26,7 +22,7 @@ export interface StreakData {
|
|
|
26
22
|
total_days: number;
|
|
27
23
|
}
|
|
28
24
|
export interface ProgressPayload {
|
|
29
|
-
skill_level:
|
|
25
|
+
skill_level: SkillLevel;
|
|
30
26
|
cognitive_load: number;
|
|
31
27
|
streaks?: StreakData;
|
|
32
28
|
total_review_count: number;
|
|
@@ -40,18 +36,16 @@ export declare class Progress extends Content {
|
|
|
40
36
|
static create(userId: ContentId): Progress;
|
|
41
37
|
constructor(doc: Doc, shouldAcquire: boolean, shareSync: ShareSync);
|
|
42
38
|
get payload(): ProgressPayload;
|
|
43
|
-
getSkillLevel():
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
getSkillLevelVolatility(): number;
|
|
39
|
+
getSkillLevel(): SkillLevel;
|
|
40
|
+
getSkillLevelMu(): number;
|
|
41
|
+
getSkillLevelSigma(): number;
|
|
47
42
|
getSkillLevelLowerBound(): number;
|
|
48
43
|
getSkillLevelUpperBound(): number;
|
|
49
44
|
needsCalibration(): boolean;
|
|
50
45
|
getCognitiveLoad(): number;
|
|
51
|
-
setSkillLevel(
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
setSkillLevelVolatility(volatility: number): void;
|
|
46
|
+
setSkillLevel(skillLevel: SkillLevel): void;
|
|
47
|
+
setSkillLevelMu(mu: number): void;
|
|
48
|
+
setSkillLevelSigma(sigma: number): void;
|
|
55
49
|
setCognitiveLoad(value: number): void;
|
|
56
50
|
getStreaks(): StreakData | undefined;
|
|
57
51
|
setStreaks(streaks: StreakData): void;
|
package/dist/models/Progress.js
CHANGED
|
@@ -6,6 +6,7 @@ const repo_1 = require("../repo");
|
|
|
6
6
|
const Content_1 = require("./Content");
|
|
7
7
|
const ContentKind_1 = require("./ContentKind");
|
|
8
8
|
const Operation_1 = require("./Operation");
|
|
9
|
+
const SkillLevel_1 = require("./SkillLevel");
|
|
9
10
|
class Progress extends Content_1.Content {
|
|
10
11
|
static makeProgressId(userId) {
|
|
11
12
|
return shaxpir_common_1.CachingHasher.makeMd5Base62Hash(userId + "-" + ContentKind_1.ContentKind.PROGRESS);
|
|
@@ -23,13 +24,9 @@ class Progress extends Content_1.Content {
|
|
|
23
24
|
updated_at: now
|
|
24
25
|
},
|
|
25
26
|
payload: {
|
|
26
|
-
skill_level:
|
|
27
|
-
rating: 0, // Start at absolute beginner (knows ~0 characters)
|
|
28
|
-
rating_deviation: 50, // Initial uncertainty (±100 chars at 95% CI)
|
|
29
|
-
volatility: 0.06 // Moderate initial volatility
|
|
30
|
-
},
|
|
27
|
+
skill_level: SkillLevel_1.SkillLevelModel.createDefault(),
|
|
31
28
|
cognitive_load: 0,
|
|
32
|
-
total_review_count: 0
|
|
29
|
+
total_review_count: 0
|
|
33
30
|
}
|
|
34
31
|
});
|
|
35
32
|
}
|
|
@@ -44,71 +41,56 @@ class Progress extends Content_1.Content {
|
|
|
44
41
|
this.checkDisposed("Progress.getSkillLevel");
|
|
45
42
|
return shaxpir_common_1.Struct.clone(this.payload.skill_level);
|
|
46
43
|
}
|
|
47
|
-
|
|
48
|
-
this.checkDisposed("Progress.
|
|
49
|
-
return this.payload.skill_level.
|
|
44
|
+
getSkillLevelMu() {
|
|
45
|
+
this.checkDisposed("Progress.getSkillLevelMu");
|
|
46
|
+
return this.payload.skill_level.mu;
|
|
50
47
|
}
|
|
51
|
-
|
|
52
|
-
this.checkDisposed("Progress.
|
|
53
|
-
return this.payload.skill_level.
|
|
54
|
-
}
|
|
55
|
-
getSkillLevelVolatility() {
|
|
56
|
-
this.checkDisposed("Progress.getSkillLevelVolatility");
|
|
57
|
-
return this.payload.skill_level.volatility;
|
|
48
|
+
getSkillLevelSigma() {
|
|
49
|
+
this.checkDisposed("Progress.getSkillLevelSigma");
|
|
50
|
+
return this.payload.skill_level.sigma;
|
|
58
51
|
}
|
|
59
52
|
getSkillLevelLowerBound() {
|
|
60
53
|
this.checkDisposed("Progress.getSkillLevelLowerBound");
|
|
61
|
-
const
|
|
62
|
-
const bounds = SkillLevelModel.calculateBounds(this.payload.skill_level.rating, this.payload.skill_level.rating_deviation);
|
|
54
|
+
const bounds = SkillLevel_1.SkillLevelModel.calculateBounds(this.payload.skill_level.mu, this.payload.skill_level.sigma);
|
|
63
55
|
return bounds.lower;
|
|
64
56
|
}
|
|
65
57
|
getSkillLevelUpperBound() {
|
|
66
58
|
this.checkDisposed("Progress.getSkillLevelUpperBound");
|
|
67
|
-
const
|
|
68
|
-
const bounds = SkillLevelModel.calculateBounds(this.payload.skill_level.rating, this.payload.skill_level.rating_deviation);
|
|
59
|
+
const bounds = SkillLevel_1.SkillLevelModel.calculateBounds(this.payload.skill_level.mu, this.payload.skill_level.sigma);
|
|
69
60
|
return bounds.upper;
|
|
70
61
|
}
|
|
71
62
|
needsCalibration() {
|
|
72
63
|
this.checkDisposed("Progress.needsCalibration");
|
|
73
|
-
// High
|
|
74
|
-
//
|
|
75
|
-
return this.payload.skill_level.
|
|
64
|
+
// High sigma means we need more data to calibrate user level
|
|
65
|
+
// sigma > 30 means confidence bounds are still quite wide (±60 chars at 95% CI)
|
|
66
|
+
return this.payload.skill_level.sigma > 30;
|
|
76
67
|
}
|
|
77
68
|
getCognitiveLoad() {
|
|
78
69
|
this.checkDisposed("Progress.getCognitiveLoad");
|
|
79
70
|
return this.payload.cognitive_load;
|
|
80
71
|
}
|
|
81
|
-
setSkillLevel(
|
|
72
|
+
setSkillLevel(skillLevel) {
|
|
82
73
|
this.checkDisposed("Progress.setSkillLevel");
|
|
83
|
-
if (!shaxpir_common_1.Struct.equals(this.payload.skill_level,
|
|
84
|
-
const batch = new Operation_1.BatchOperation(this);
|
|
85
|
-
batch.setPathValue(['payload', 'skill_level', 'rating'], uncertainValue.rating);
|
|
86
|
-
batch.setPathValue(['payload', 'skill_level', 'rating_deviation'], uncertainValue.rating_deviation);
|
|
87
|
-
batch.setPathValue(['payload', 'skill_level', 'volatility'], uncertainValue.volatility);
|
|
88
|
-
batch.commit();
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
setSkillLevelValue(value) {
|
|
92
|
-
this.checkDisposed("Progress.setSkillLevelValue");
|
|
93
|
-
if (this.payload.skill_level.rating !== value) {
|
|
74
|
+
if (!shaxpir_common_1.Struct.equals(this.payload.skill_level, skillLevel)) {
|
|
94
75
|
const batch = new Operation_1.BatchOperation(this);
|
|
95
|
-
batch.setPathValue(['payload', 'skill_level', '
|
|
76
|
+
batch.setPathValue(['payload', 'skill_level', 'mu'], skillLevel.mu);
|
|
77
|
+
batch.setPathValue(['payload', 'skill_level', 'sigma'], skillLevel.sigma);
|
|
96
78
|
batch.commit();
|
|
97
79
|
}
|
|
98
80
|
}
|
|
99
|
-
|
|
100
|
-
this.checkDisposed("Progress.
|
|
101
|
-
if (this.payload.skill_level.
|
|
81
|
+
setSkillLevelMu(mu) {
|
|
82
|
+
this.checkDisposed("Progress.setSkillLevelMu");
|
|
83
|
+
if (this.payload.skill_level.mu !== mu) {
|
|
102
84
|
const batch = new Operation_1.BatchOperation(this);
|
|
103
|
-
batch.setPathValue(['payload', 'skill_level', '
|
|
85
|
+
batch.setPathValue(['payload', 'skill_level', 'mu'], mu);
|
|
104
86
|
batch.commit();
|
|
105
87
|
}
|
|
106
88
|
}
|
|
107
|
-
|
|
108
|
-
this.checkDisposed("Progress.
|
|
109
|
-
if (this.payload.skill_level.
|
|
89
|
+
setSkillLevelSigma(sigma) {
|
|
90
|
+
this.checkDisposed("Progress.setSkillLevelSigma");
|
|
91
|
+
if (this.payload.skill_level.sigma !== sigma) {
|
|
110
92
|
const batch = new Operation_1.BatchOperation(this);
|
|
111
|
-
batch.setPathValue(['payload', 'skill_level', '
|
|
93
|
+
batch.setPathValue(['payload', 'skill_level', 'sigma'], sigma);
|
|
112
94
|
batch.commit();
|
|
113
95
|
}
|
|
114
96
|
}
|
package/dist/models/Session.d.ts
CHANGED
|
@@ -4,7 +4,7 @@ import { ShareSync } from '../repo';
|
|
|
4
4
|
import { Conditions } from './Condition';
|
|
5
5
|
import { Content, ContentBody, ContentId, ContentMeta, ContentRef } from "./Content";
|
|
6
6
|
import { Review } from './Review';
|
|
7
|
-
import {
|
|
7
|
+
import { SkillLevel } from './SkillLevel';
|
|
8
8
|
/**
|
|
9
9
|
* How aggressively the card selection algorithm stretches the user
|
|
10
10
|
* beyond their current skill level.
|
|
@@ -29,7 +29,7 @@ export interface SessionConfig {
|
|
|
29
29
|
};
|
|
30
30
|
}
|
|
31
31
|
export interface SessionStats {
|
|
32
|
-
skill_level:
|
|
32
|
+
skill_level: SkillLevel;
|
|
33
33
|
cognitive_load: number;
|
|
34
34
|
theta_distribution: {
|
|
35
35
|
A: number;
|
|
@@ -1,72 +1,146 @@
|
|
|
1
1
|
import { ReviewResult } from './Review';
|
|
2
2
|
import { Bounds } from './BayesianScore';
|
|
3
3
|
/**
|
|
4
|
-
* Represents a skill level
|
|
5
|
-
* Uses
|
|
4
|
+
* Represents a skill level estimate with uncertainty.
|
|
5
|
+
* Uses Item Response Theory (IRT) for skill assessment.
|
|
6
|
+
*
|
|
7
|
+
* The skill level represents the estimated number of characters
|
|
8
|
+
* a user has mastered. A user with skill level N is expected to
|
|
9
|
+
* succeed on recognition tasks for characters with difficulty <= N.
|
|
6
10
|
*/
|
|
7
11
|
export interface SkillLevel {
|
|
12
|
+
/** Estimated skill level (maps to ~number of characters mastered) */
|
|
13
|
+
mu: number;
|
|
14
|
+
/** Uncertainty in the estimate (standard deviation) */
|
|
15
|
+
sigma: number;
|
|
16
|
+
}
|
|
17
|
+
/**
|
|
18
|
+
* Legacy interface for backwards compatibility during migration.
|
|
19
|
+
* Maps to the new SkillLevel interface.
|
|
20
|
+
* @deprecated Use SkillLevel with mu/sigma instead
|
|
21
|
+
*/
|
|
22
|
+
export interface LegacySkillLevel {
|
|
8
23
|
rating: number;
|
|
9
24
|
rating_deviation: number;
|
|
10
25
|
volatility: number;
|
|
11
26
|
}
|
|
12
27
|
/**
|
|
13
|
-
* Model for updating and managing skill levels using
|
|
28
|
+
* Model for updating and managing skill levels using Item Response Theory (IRT).
|
|
14
29
|
*
|
|
15
|
-
*
|
|
16
|
-
*
|
|
17
|
-
* - Rating 0 = absolute beginner (knows ~0 characters)
|
|
18
|
-
* - Rating 100 = knows ~100 characters
|
|
19
|
-
* - Rating 3000 = knows ~3000 characters
|
|
30
|
+
* This model estimates user skill based on a logistic psychometric function:
|
|
31
|
+
* P(success | difficulty d, skill μ) = 1 / (1 + exp(k * (d - μ)))
|
|
20
32
|
*
|
|
21
|
-
*
|
|
22
|
-
*
|
|
33
|
+
* Where:
|
|
34
|
+
* - μ (mu) is the skill level we're estimating
|
|
35
|
+
* - k controls the steepness of the transition from "easy" to "hard"
|
|
36
|
+
* - Cards with difficulty < μ are likely to be answered correctly
|
|
37
|
+
* - Cards with difficulty > μ are likely to be answered incorrectly
|
|
38
|
+
*
|
|
39
|
+
* The model uses Bayesian updating with a Gaussian approximation:
|
|
40
|
+
* - Each observation updates μ based on prediction error
|
|
41
|
+
* - Uncertainty (σ) decreases as we accumulate evidence
|
|
42
|
+
* - No full history is needed - just μ and σ are sufficient statistics
|
|
43
|
+
*
|
|
44
|
+
* Scale semantics:
|
|
45
|
+
* - μ = 0: absolute beginner (knows ~0 characters)
|
|
46
|
+
* - μ = 100: knows ~100 characters
|
|
47
|
+
* - μ = 3000: knows ~3000 characters
|
|
23
48
|
*/
|
|
24
49
|
export declare class SkillLevelModel {
|
|
25
50
|
/**
|
|
26
|
-
*
|
|
51
|
+
* Steepness parameter for the logistic function.
|
|
52
|
+
*
|
|
53
|
+
* Controls how sharply success probability drops as difficulty exceeds skill:
|
|
54
|
+
* - k = 0.05 means the transition zone spans ~40 difficulty points
|
|
55
|
+
* - At difficulty = μ, P(success) = 50%
|
|
56
|
+
* - At difficulty = μ + 20, P(success) ≈ 27%
|
|
57
|
+
* - At difficulty = μ + 40, P(success) ≈ 12%
|
|
58
|
+
*
|
|
59
|
+
* Lower k = more gradual transition, higher k = sharper cutoff.
|
|
60
|
+
*/
|
|
61
|
+
private static readonly K;
|
|
62
|
+
/**
|
|
63
|
+
* Minimum sigma value to prevent over-confidence.
|
|
64
|
+
* Even with many observations, we maintain some uncertainty.
|
|
65
|
+
*/
|
|
66
|
+
private static readonly MIN_SIGMA;
|
|
67
|
+
/**
|
|
68
|
+
* Maximum sigma value for initial state.
|
|
69
|
+
*/
|
|
70
|
+
private static readonly MAX_SIGMA;
|
|
71
|
+
/**
|
|
72
|
+
* Update skill level using IRT after a review.
|
|
27
73
|
*
|
|
28
|
-
*
|
|
29
|
-
*
|
|
30
|
-
*
|
|
31
|
-
* @param
|
|
32
|
-
* @param
|
|
74
|
+
* Uses Bayesian updating with a Gaussian approximation to the posterior.
|
|
75
|
+
* The update is incremental - only requires current μ and σ, not full history.
|
|
76
|
+
*
|
|
77
|
+
* @param mu Current skill level estimate
|
|
78
|
+
* @param sigma Current uncertainty (standard deviation)
|
|
79
|
+
* @param cardDifficulty Difficulty of the card reviewed
|
|
33
80
|
* @param outcome Review result (FAIL/HARD/GOOD/EASY)
|
|
34
|
-
* @
|
|
35
|
-
* @returns Updated skill level with new rating, rating_deviation, and volatility
|
|
81
|
+
* @returns Updated skill level with new mu and sigma
|
|
36
82
|
*/
|
|
37
|
-
static
|
|
83
|
+
static update(mu: number, sigma: number, cardDifficulty: number, outcome: ReviewResult): SkillLevel;
|
|
38
84
|
/**
|
|
39
|
-
*
|
|
85
|
+
* Update skill level using IRT (Glicko-2 compatible signature).
|
|
86
|
+
*
|
|
87
|
+
* This method provides backwards compatibility with the old Glicko-2 API.
|
|
88
|
+
* The volatility and cardRatingDeviation parameters are ignored.
|
|
89
|
+
*
|
|
90
|
+
* @deprecated Use update() instead for cleaner IRT semantics
|
|
91
|
+
*/
|
|
92
|
+
static updateWithGlicko2(rating: number, ratingDeviation: number, _volatility: number, cardDifficulty: number, _cardRatingDeviation: number, outcome: ReviewResult, _tau?: number): LegacySkillLevel;
|
|
93
|
+
/**
|
|
94
|
+
* Map a review outcome to a success score for IRT.
|
|
40
95
|
*
|
|
41
96
|
* @param outcome Review result
|
|
42
|
-
* @returns
|
|
97
|
+
* @returns Success score from 0.0 (complete failure) to 1.0 (perfect success)
|
|
43
98
|
*/
|
|
44
99
|
static outcomeToScore(outcome: ReviewResult): number;
|
|
45
100
|
/**
|
|
46
|
-
* Calculate
|
|
47
|
-
*
|
|
101
|
+
* Calculate the probability of success on a card given skill level.
|
|
102
|
+
*
|
|
103
|
+
* @param mu Skill level estimate
|
|
104
|
+
* @param cardDifficulty Difficulty of the card
|
|
105
|
+
* @returns Probability of success (0 to 1)
|
|
106
|
+
*/
|
|
107
|
+
static predictSuccess(mu: number, cardDifficulty: number): number;
|
|
108
|
+
/**
|
|
109
|
+
* Calculate confidence bounds for the skill estimate.
|
|
48
110
|
*
|
|
49
|
-
* @param
|
|
50
|
-
* @param
|
|
111
|
+
* @param mu Skill level estimate
|
|
112
|
+
* @param sigma Uncertainty (standard deviation)
|
|
51
113
|
* @param confidence Confidence level (0.95 for 95%, 0.99 for 99%)
|
|
52
114
|
* @returns Bounds object with lower and upper confidence limits
|
|
53
115
|
*/
|
|
54
|
-
static calculateBounds(
|
|
116
|
+
static calculateBounds(mu: number, sigma: number, confidence?: number): Bounds;
|
|
55
117
|
/**
|
|
56
118
|
* Create a default skill level for a new user.
|
|
57
|
-
* Starts at
|
|
119
|
+
* Starts at skill 0 (knows no characters) with high uncertainty.
|
|
58
120
|
*
|
|
59
|
-
* @param
|
|
60
|
-
* @param initialVolatility Initial volatility (default 0.06, moderate)
|
|
121
|
+
* @param initialSigma Initial uncertainty (default 50)
|
|
61
122
|
* @returns New SkillLevel object
|
|
62
123
|
*/
|
|
63
|
-
static createDefault(
|
|
124
|
+
static createDefault(initialSigma?: number): SkillLevel;
|
|
125
|
+
/**
|
|
126
|
+
* Create a default skill level in legacy format.
|
|
127
|
+
* @deprecated Use createDefault() instead
|
|
128
|
+
*/
|
|
129
|
+
static createDefaultLegacy(initialRatingDeviation?: number, initialVolatility?: number): LegacySkillLevel;
|
|
130
|
+
/**
|
|
131
|
+
* Convert from legacy Glicko-2 format to IRT format.
|
|
132
|
+
*/
|
|
133
|
+
static fromLegacy(legacy: LegacySkillLevel): SkillLevel;
|
|
134
|
+
/**
|
|
135
|
+
* Convert from IRT format to legacy Glicko-2 format.
|
|
136
|
+
*/
|
|
137
|
+
static toLegacy(skill: SkillLevel): LegacySkillLevel;
|
|
64
138
|
/**
|
|
65
139
|
* Check if the skill level has high confidence (low uncertainty).
|
|
66
140
|
*
|
|
67
|
-
* @param
|
|
68
|
-
* @param threshold Maximum
|
|
69
|
-
* @returns True if uncertainty is low
|
|
141
|
+
* @param sigma Uncertainty (standard deviation)
|
|
142
|
+
* @param threshold Maximum sigma to be considered "high confidence" (default 15)
|
|
143
|
+
* @returns True if uncertainty is low
|
|
70
144
|
*/
|
|
71
|
-
static isHighConfidence(
|
|
145
|
+
static isHighConfidence(sigma: number, threshold?: number): boolean;
|
|
72
146
|
}
|
|
@@ -1,106 +1,192 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
-
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
-
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
-
};
|
|
5
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
3
|
exports.SkillLevelModel = void 0;
|
|
7
|
-
const glicko2_lite_1 = __importDefault(require("glicko2-lite"));
|
|
8
4
|
/**
|
|
9
|
-
* Model for updating and managing skill levels using
|
|
5
|
+
* Model for updating and managing skill levels using Item Response Theory (IRT).
|
|
10
6
|
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
* - Rating 0 = absolute beginner (knows ~0 characters)
|
|
14
|
-
* - Rating 100 = knows ~100 characters
|
|
15
|
-
* - Rating 3000 = knows ~3000 characters
|
|
7
|
+
* This model estimates user skill based on a logistic psychometric function:
|
|
8
|
+
* P(success | difficulty d, skill μ) = 1 / (1 + exp(k * (d - μ)))
|
|
16
9
|
*
|
|
17
|
-
*
|
|
18
|
-
*
|
|
10
|
+
* Where:
|
|
11
|
+
* - μ (mu) is the skill level we're estimating
|
|
12
|
+
* - k controls the steepness of the transition from "easy" to "hard"
|
|
13
|
+
* - Cards with difficulty < μ are likely to be answered correctly
|
|
14
|
+
* - Cards with difficulty > μ are likely to be answered incorrectly
|
|
15
|
+
*
|
|
16
|
+
* The model uses Bayesian updating with a Gaussian approximation:
|
|
17
|
+
* - Each observation updates μ based on prediction error
|
|
18
|
+
* - Uncertainty (σ) decreases as we accumulate evidence
|
|
19
|
+
* - No full history is needed - just μ and σ are sufficient statistics
|
|
20
|
+
*
|
|
21
|
+
* Scale semantics:
|
|
22
|
+
* - μ = 0: absolute beginner (knows ~0 characters)
|
|
23
|
+
* - μ = 100: knows ~100 characters
|
|
24
|
+
* - μ = 3000: knows ~3000 characters
|
|
19
25
|
*/
|
|
20
26
|
class SkillLevelModel {
|
|
21
27
|
/**
|
|
22
|
-
* Update skill level using
|
|
28
|
+
* Update skill level using IRT after a review.
|
|
29
|
+
*
|
|
30
|
+
* Uses Bayesian updating with a Gaussian approximation to the posterior.
|
|
31
|
+
* The update is incremental - only requires current μ and σ, not full history.
|
|
23
32
|
*
|
|
24
|
-
* @param
|
|
25
|
-
* @param
|
|
26
|
-
* @param
|
|
27
|
-
* @param cardDifficulty Difficulty of the card reviewed (on same scale as rating)
|
|
28
|
-
* @param cardRatingDeviation Rating deviation for the card (typically fixed, e.g., 100)
|
|
33
|
+
* @param mu Current skill level estimate
|
|
34
|
+
* @param sigma Current uncertainty (standard deviation)
|
|
35
|
+
* @param cardDifficulty Difficulty of the card reviewed
|
|
29
36
|
* @param outcome Review result (FAIL/HARD/GOOD/EASY)
|
|
30
|
-
* @
|
|
31
|
-
|
|
37
|
+
* @returns Updated skill level with new mu and sigma
|
|
38
|
+
*/
|
|
39
|
+
static update(mu, sigma, cardDifficulty, outcome) {
|
|
40
|
+
const k = SkillLevelModel.K;
|
|
41
|
+
// Map outcome to success value (0 to 1)
|
|
42
|
+
const y = SkillLevelModel.outcomeToScore(outcome);
|
|
43
|
+
// Predicted probability of success given current skill estimate
|
|
44
|
+
const p = 1 / (1 + Math.exp(k * (cardDifficulty - mu)));
|
|
45
|
+
// Prediction error: positive if did better than expected, negative if worse
|
|
46
|
+
const error = y - p;
|
|
47
|
+
// Fisher information for logistic model (highest at p = 0.5)
|
|
48
|
+
const info = p * (1 - p);
|
|
49
|
+
// Update variance (precision increases with information)
|
|
50
|
+
const sigmaSquared = sigma * sigma;
|
|
51
|
+
const newSigmaSquared = 1 / (1 / sigmaSquared + k * k * info);
|
|
52
|
+
const newSigma = Math.sqrt(newSigmaSquared);
|
|
53
|
+
// Update mean (shift toward evidence)
|
|
54
|
+
const newMu = mu + newSigmaSquared * k * error;
|
|
55
|
+
return {
|
|
56
|
+
mu: Math.max(0, newMu), // Skill can't go negative
|
|
57
|
+
sigma: Math.max(SkillLevelModel.MIN_SIGMA, Math.min(SkillLevelModel.MAX_SIGMA, newSigma))
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
/**
|
|
61
|
+
* Update skill level using IRT (Glicko-2 compatible signature).
|
|
62
|
+
*
|
|
63
|
+
* This method provides backwards compatibility with the old Glicko-2 API.
|
|
64
|
+
* The volatility and cardRatingDeviation parameters are ignored.
|
|
65
|
+
*
|
|
66
|
+
* @deprecated Use update() instead for cleaner IRT semantics
|
|
32
67
|
*/
|
|
33
|
-
static updateWithGlicko2(rating, ratingDeviation,
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
// Call glicko2-lite with a single match
|
|
37
|
-
// Match format: [opponentRating, opponentRD, outcome]
|
|
38
|
-
const result = (0, glicko2_lite_1.default)(rating, ratingDeviation, volatility, [
|
|
39
|
-
[cardDifficulty, cardRatingDeviation, score]
|
|
40
|
-
], { tau });
|
|
68
|
+
static updateWithGlicko2(rating, ratingDeviation, _volatility, cardDifficulty, _cardRatingDeviation, outcome, _tau = 0.5) {
|
|
69
|
+
const result = SkillLevelModel.update(rating, ratingDeviation, cardDifficulty, outcome);
|
|
70
|
+
// Return in legacy format
|
|
41
71
|
return {
|
|
42
|
-
rating: result.
|
|
43
|
-
rating_deviation: result.
|
|
44
|
-
volatility:
|
|
72
|
+
rating: result.mu,
|
|
73
|
+
rating_deviation: result.sigma,
|
|
74
|
+
volatility: 0.06 // Fixed value, not used in IRT
|
|
45
75
|
};
|
|
46
76
|
}
|
|
47
77
|
/**
|
|
48
|
-
* Map a review outcome to a
|
|
78
|
+
* Map a review outcome to a success score for IRT.
|
|
49
79
|
*
|
|
50
80
|
* @param outcome Review result
|
|
51
|
-
* @returns
|
|
81
|
+
* @returns Success score from 0.0 (complete failure) to 1.0 (perfect success)
|
|
52
82
|
*/
|
|
53
83
|
static outcomeToScore(outcome) {
|
|
54
84
|
switch (outcome) {
|
|
55
85
|
case 'EASY': return 1.0;
|
|
56
|
-
case 'GOOD': return 0.
|
|
57
|
-
case 'HARD': return 0.
|
|
86
|
+
case 'GOOD': return 0.85;
|
|
87
|
+
case 'HARD': return 0.3;
|
|
58
88
|
case 'FAIL': return 0.0;
|
|
59
89
|
}
|
|
60
90
|
}
|
|
61
91
|
/**
|
|
62
|
-
* Calculate
|
|
63
|
-
* Returns a proper statistical confidence interval.
|
|
92
|
+
* Calculate the probability of success on a card given skill level.
|
|
64
93
|
*
|
|
65
|
-
* @param
|
|
66
|
-
* @param
|
|
94
|
+
* @param mu Skill level estimate
|
|
95
|
+
* @param cardDifficulty Difficulty of the card
|
|
96
|
+
* @returns Probability of success (0 to 1)
|
|
97
|
+
*/
|
|
98
|
+
static predictSuccess(mu, cardDifficulty) {
|
|
99
|
+
return 1 / (1 + Math.exp(SkillLevelModel.K * (cardDifficulty - mu)));
|
|
100
|
+
}
|
|
101
|
+
/**
|
|
102
|
+
* Calculate confidence bounds for the skill estimate.
|
|
103
|
+
*
|
|
104
|
+
* @param mu Skill level estimate
|
|
105
|
+
* @param sigma Uncertainty (standard deviation)
|
|
67
106
|
* @param confidence Confidence level (0.95 for 95%, 0.99 for 99%)
|
|
68
107
|
* @returns Bounds object with lower and upper confidence limits
|
|
69
108
|
*/
|
|
70
|
-
static calculateBounds(
|
|
109
|
+
static calculateBounds(mu, sigma, confidence = 0.95) {
|
|
71
110
|
// Z-score for desired confidence level
|
|
72
|
-
// 95% CI: ±1.96 standard deviations
|
|
73
|
-
// 99% CI: ±2.58 standard deviations
|
|
74
111
|
const z = confidence === 0.95 ? 1.96 : confidence === 0.99 ? 2.58 : 1.96;
|
|
75
112
|
return {
|
|
76
|
-
lower: Math.max(0,
|
|
77
|
-
upper:
|
|
113
|
+
lower: Math.max(0, mu - z * sigma),
|
|
114
|
+
upper: mu + z * sigma
|
|
78
115
|
};
|
|
79
116
|
}
|
|
80
117
|
/**
|
|
81
118
|
* Create a default skill level for a new user.
|
|
82
|
-
* Starts at
|
|
119
|
+
* Starts at skill 0 (knows no characters) with high uncertainty.
|
|
83
120
|
*
|
|
84
|
-
* @param
|
|
85
|
-
* @param initialVolatility Initial volatility (default 0.06, moderate)
|
|
121
|
+
* @param initialSigma Initial uncertainty (default 50)
|
|
86
122
|
* @returns New SkillLevel object
|
|
87
123
|
*/
|
|
88
|
-
static createDefault(
|
|
124
|
+
static createDefault(initialSigma = 50) {
|
|
125
|
+
return {
|
|
126
|
+
mu: 0,
|
|
127
|
+
sigma: initialSigma
|
|
128
|
+
};
|
|
129
|
+
}
|
|
130
|
+
/**
|
|
131
|
+
* Create a default skill level in legacy format.
|
|
132
|
+
* @deprecated Use createDefault() instead
|
|
133
|
+
*/
|
|
134
|
+
static createDefaultLegacy(initialRatingDeviation = 50, initialVolatility = 0.06) {
|
|
89
135
|
return {
|
|
90
136
|
rating: 0,
|
|
91
137
|
rating_deviation: initialRatingDeviation,
|
|
92
138
|
volatility: initialVolatility
|
|
93
139
|
};
|
|
94
140
|
}
|
|
141
|
+
/**
|
|
142
|
+
* Convert from legacy Glicko-2 format to IRT format.
|
|
143
|
+
*/
|
|
144
|
+
static fromLegacy(legacy) {
|
|
145
|
+
return {
|
|
146
|
+
mu: legacy.rating,
|
|
147
|
+
sigma: legacy.rating_deviation
|
|
148
|
+
};
|
|
149
|
+
}
|
|
150
|
+
/**
|
|
151
|
+
* Convert from IRT format to legacy Glicko-2 format.
|
|
152
|
+
*/
|
|
153
|
+
static toLegacy(skill) {
|
|
154
|
+
return {
|
|
155
|
+
rating: skill.mu,
|
|
156
|
+
rating_deviation: skill.sigma,
|
|
157
|
+
volatility: 0.06
|
|
158
|
+
};
|
|
159
|
+
}
|
|
95
160
|
/**
|
|
96
161
|
* Check if the skill level has high confidence (low uncertainty).
|
|
97
162
|
*
|
|
98
|
-
* @param
|
|
99
|
-
* @param threshold Maximum
|
|
100
|
-
* @returns True if uncertainty is low
|
|
163
|
+
* @param sigma Uncertainty (standard deviation)
|
|
164
|
+
* @param threshold Maximum sigma to be considered "high confidence" (default 15)
|
|
165
|
+
* @returns True if uncertainty is low
|
|
101
166
|
*/
|
|
102
|
-
static isHighConfidence(
|
|
103
|
-
return
|
|
167
|
+
static isHighConfidence(sigma, threshold = 15) {
|
|
168
|
+
return sigma < threshold;
|
|
104
169
|
}
|
|
105
170
|
}
|
|
106
171
|
exports.SkillLevelModel = SkillLevelModel;
|
|
172
|
+
/**
|
|
173
|
+
* Steepness parameter for the logistic function.
|
|
174
|
+
*
|
|
175
|
+
* Controls how sharply success probability drops as difficulty exceeds skill:
|
|
176
|
+
* - k = 0.05 means the transition zone spans ~40 difficulty points
|
|
177
|
+
* - At difficulty = μ, P(success) = 50%
|
|
178
|
+
* - At difficulty = μ + 20, P(success) ≈ 27%
|
|
179
|
+
* - At difficulty = μ + 40, P(success) ≈ 12%
|
|
180
|
+
*
|
|
181
|
+
* Lower k = more gradual transition, higher k = sharper cutoff.
|
|
182
|
+
*/
|
|
183
|
+
SkillLevelModel.K = 0.05;
|
|
184
|
+
/**
|
|
185
|
+
* Minimum sigma value to prevent over-confidence.
|
|
186
|
+
* Even with many observations, we maintain some uncertainty.
|
|
187
|
+
*/
|
|
188
|
+
SkillLevelModel.MIN_SIGMA = 5;
|
|
189
|
+
/**
|
|
190
|
+
* Maximum sigma value for initial state.
|
|
191
|
+
*/
|
|
192
|
+
SkillLevelModel.MAX_SIGMA = 100;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@shaxpir/duiduidui-models",
|
|
3
|
-
"version": "1.9.
|
|
3
|
+
"version": "1.9.22",
|
|
4
4
|
"repository": {
|
|
5
5
|
"type": "git",
|
|
6
6
|
"url": "https://github.com/shaxpir/duiduidui-models"
|
|
@@ -19,7 +19,6 @@
|
|
|
19
19
|
"@shaxpir/duiduidui-models": "^1.4.14",
|
|
20
20
|
"@shaxpir/sharedb": "^6.0.6",
|
|
21
21
|
"@shaxpir/shaxpir-common": "^1.4.1",
|
|
22
|
-
"glicko2-lite": "^4.0.0",
|
|
23
22
|
"ot-json1": "1.0.1",
|
|
24
23
|
"ot-text-unicode": "4.0.0",
|
|
25
24
|
"reconnecting-websocket": "4.4.0"
|