@shaxpir/duiduidui-models 1.9.21 → 1.9.25
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 +73 -63
- package/dist/models/SkillLevel.js +53 -99
- package/package.json +1 -1
|
@@ -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;
|
|
@@ -15,31 +15,71 @@ export interface SkillLevel {
|
|
|
15
15
|
sigma: number;
|
|
16
16
|
}
|
|
17
17
|
/**
|
|
18
|
-
*
|
|
19
|
-
*
|
|
20
|
-
* @deprecated Use SkillLevel with mu/sigma instead
|
|
18
|
+
* Parameters for skill level updates.
|
|
19
|
+
* All parameters are optional and have sensible defaults.
|
|
21
20
|
*/
|
|
22
|
-
export interface
|
|
23
|
-
|
|
24
|
-
|
|
25
|
-
|
|
21
|
+
export interface SkillUpdateParams {
|
|
22
|
+
/**
|
|
23
|
+
* Coefficient controlling how quickly skill moves toward evidence.
|
|
24
|
+
*
|
|
25
|
+
* When informative evidence is received, skill moves this fraction
|
|
26
|
+
* of the distance from current mu toward the card's difficulty.
|
|
27
|
+
*
|
|
28
|
+
* - 0.5 means move halfway toward the card difficulty each time
|
|
29
|
+
* - Lower values = more conservative, requires more evidence
|
|
30
|
+
* - Higher values = more aggressive, trusts individual observations more
|
|
31
|
+
*
|
|
32
|
+
* With coefficient 0.5, approaching a difficulty level:
|
|
33
|
+
* - After 1 review: 50% of the way
|
|
34
|
+
* - After 2 reviews: 75% of the way
|
|
35
|
+
* - After 3 reviews: 87.5% of the way
|
|
36
|
+
* - After 5 reviews: 96.9% of the way
|
|
37
|
+
*
|
|
38
|
+
* @default 0.5
|
|
39
|
+
*/
|
|
40
|
+
updateCoefficient?: number;
|
|
41
|
+
/**
|
|
42
|
+
* Coefficient for sigma updates when evidence is informative.
|
|
43
|
+
* Sigma is multiplied by this value (so 0.9 = 10% decay).
|
|
44
|
+
*
|
|
45
|
+
* @default 0.9
|
|
46
|
+
*/
|
|
47
|
+
sigmaDecay?: number;
|
|
48
|
+
/**
|
|
49
|
+
* Minimum sigma value to prevent over-confidence.
|
|
50
|
+
* Even with many observations, we maintain some uncertainty.
|
|
51
|
+
*
|
|
52
|
+
* @default 5
|
|
53
|
+
*/
|
|
54
|
+
minSigma?: number;
|
|
55
|
+
/**
|
|
56
|
+
* Maximum sigma value.
|
|
57
|
+
*
|
|
58
|
+
* @default 100
|
|
59
|
+
*/
|
|
60
|
+
maxSigma?: number;
|
|
26
61
|
}
|
|
27
62
|
/**
|
|
28
|
-
*
|
|
63
|
+
* Default parameters for skill updates.
|
|
64
|
+
*/
|
|
65
|
+
export declare const DEFAULT_SKILL_PARAMS: Required<SkillUpdateParams>;
|
|
66
|
+
/**
|
|
67
|
+
* Model for updating and managing skill levels using Item Response Theory (IRT)
|
|
68
|
+
* with asymmetric evidence clamping.
|
|
29
69
|
*
|
|
30
|
-
* This model
|
|
31
|
-
*
|
|
70
|
+
* This model is based on standard IRT psychometric principles but applies a
|
|
71
|
+
* clamping constraint that reflects the asymmetric nature of flashcard evidence:
|
|
32
72
|
*
|
|
33
|
-
*
|
|
34
|
-
*
|
|
35
|
-
* -
|
|
36
|
-
*
|
|
37
|
-
* -
|
|
73
|
+
* - Succeeding on a card of difficulty D provides evidence that skill >= D,
|
|
74
|
+
* but a single success should NOT push skill estimate above D
|
|
75
|
+
* - Failing on a card of difficulty D provides evidence that skill <= D,
|
|
76
|
+
* but a single failure should NOT push skill estimate below D
|
|
77
|
+
* - Succeeding on easy cards (difficulty < skill) is uninformative
|
|
78
|
+
* - Failing on hard cards (difficulty > skill) is uninformative
|
|
38
79
|
*
|
|
39
|
-
* The
|
|
40
|
-
*
|
|
41
|
-
*
|
|
42
|
-
* - No full history is needed - just μ and σ are sufficient statistics
|
|
80
|
+
* The update rule moves skill toward the card difficulty by a fraction (coefficient),
|
|
81
|
+
* ensuring that skill can only asymptotically approach demonstrated competence
|
|
82
|
+
* through multiple consistent observations.
|
|
43
83
|
*
|
|
44
84
|
* Scale semantics:
|
|
45
85
|
* - μ = 0: absolute beginner (knows ~0 characters)
|
|
@@ -48,48 +88,31 @@ export interface LegacySkillLevel {
|
|
|
48
88
|
*/
|
|
49
89
|
export declare class SkillLevelModel {
|
|
50
90
|
/**
|
|
51
|
-
*
|
|
52
|
-
*
|
|
53
|
-
*
|
|
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.
|
|
91
|
+
* Discrimination parameter for predictSuccess logistic function.
|
|
92
|
+
* Controls how sharply probability transitions around difficulty = mu.
|
|
93
|
+
* Higher values = steeper curve. 0.05 gives gradual transition over ~60 difficulty units.
|
|
60
94
|
*/
|
|
61
95
|
private static readonly K;
|
|
62
96
|
/**
|
|
63
|
-
*
|
|
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.
|
|
97
|
+
* Update skill level using clamped IRT after a review.
|
|
73
98
|
*
|
|
74
|
-
*
|
|
75
|
-
*
|
|
99
|
+
* Applies asymmetric evidence rules:
|
|
100
|
+
* - Success on harder card (difficulty > mu): informative, move mu up toward difficulty
|
|
101
|
+
* - Failure on easier card (difficulty < mu): informative, move mu down toward difficulty
|
|
102
|
+
* - Success on easier card: uninformative, no change
|
|
103
|
+
* - Failure on harder card: uninformative, no change
|
|
104
|
+
*
|
|
105
|
+
* When evidence is informative, sigma also decreases to reflect increased confidence.
|
|
106
|
+
* When evidence is uninformative, both mu and sigma remain unchanged.
|
|
76
107
|
*
|
|
77
108
|
* @param mu Current skill level estimate
|
|
78
109
|
* @param sigma Current uncertainty (standard deviation)
|
|
79
110
|
* @param cardDifficulty Difficulty of the card reviewed
|
|
80
111
|
* @param outcome Review result (FAIL/HARD/GOOD/EASY)
|
|
112
|
+
* @param params Optional parameters to customize update behavior
|
|
81
113
|
* @returns Updated skill level with new mu and sigma
|
|
82
114
|
*/
|
|
83
|
-
static update(mu: number, sigma: number, cardDifficulty: number, outcome: ReviewResult): SkillLevel;
|
|
84
|
-
/**
|
|
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;
|
|
115
|
+
static update(mu: number, sigma: number, cardDifficulty: number, outcome: ReviewResult, params?: SkillUpdateParams): SkillLevel;
|
|
93
116
|
/**
|
|
94
117
|
* Map a review outcome to a success score for IRT.
|
|
95
118
|
*
|
|
@@ -122,19 +145,6 @@ export declare class SkillLevelModel {
|
|
|
122
145
|
* @returns New SkillLevel object
|
|
123
146
|
*/
|
|
124
147
|
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;
|
|
138
148
|
/**
|
|
139
149
|
* Check if the skill level has high confidence (low uncertainty).
|
|
140
150
|
*
|
|
@@ -1,22 +1,32 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
-
exports.SkillLevelModel = void 0;
|
|
3
|
+
exports.SkillLevelModel = exports.DEFAULT_SKILL_PARAMS = void 0;
|
|
4
4
|
/**
|
|
5
|
-
*
|
|
5
|
+
* Default parameters for skill updates.
|
|
6
|
+
*/
|
|
7
|
+
exports.DEFAULT_SKILL_PARAMS = {
|
|
8
|
+
updateCoefficient: 0.5,
|
|
9
|
+
sigmaDecay: 0.9,
|
|
10
|
+
minSigma: 5,
|
|
11
|
+
maxSigma: 100
|
|
12
|
+
};
|
|
13
|
+
/**
|
|
14
|
+
* Model for updating and managing skill levels using Item Response Theory (IRT)
|
|
15
|
+
* with asymmetric evidence clamping.
|
|
6
16
|
*
|
|
7
|
-
* This model
|
|
8
|
-
*
|
|
17
|
+
* This model is based on standard IRT psychometric principles but applies a
|
|
18
|
+
* clamping constraint that reflects the asymmetric nature of flashcard evidence:
|
|
9
19
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
* -
|
|
13
|
-
*
|
|
14
|
-
* -
|
|
20
|
+
* - Succeeding on a card of difficulty D provides evidence that skill >= D,
|
|
21
|
+
* but a single success should NOT push skill estimate above D
|
|
22
|
+
* - Failing on a card of difficulty D provides evidence that skill <= D,
|
|
23
|
+
* but a single failure should NOT push skill estimate below D
|
|
24
|
+
* - Succeeding on easy cards (difficulty < skill) is uninformative
|
|
25
|
+
* - Failing on hard cards (difficulty > skill) is uninformative
|
|
15
26
|
*
|
|
16
|
-
* The
|
|
17
|
-
*
|
|
18
|
-
*
|
|
19
|
-
* - No full history is needed - just μ and σ are sufficient statistics
|
|
27
|
+
* The update rule moves skill toward the card difficulty by a fraction (coefficient),
|
|
28
|
+
* ensuring that skill can only asymptotically approach demonstrated competence
|
|
29
|
+
* through multiple consistent observations.
|
|
20
30
|
*
|
|
21
31
|
* Scale semantics:
|
|
22
32
|
* - μ = 0: absolute beginner (knows ~0 characters)
|
|
@@ -25,53 +35,42 @@ exports.SkillLevelModel = void 0;
|
|
|
25
35
|
*/
|
|
26
36
|
class SkillLevelModel {
|
|
27
37
|
/**
|
|
28
|
-
* Update skill level using IRT after a review.
|
|
38
|
+
* Update skill level using clamped IRT after a review.
|
|
29
39
|
*
|
|
30
|
-
*
|
|
31
|
-
*
|
|
40
|
+
* Applies asymmetric evidence rules:
|
|
41
|
+
* - Success on harder card (difficulty > mu): informative, move mu up toward difficulty
|
|
42
|
+
* - Failure on easier card (difficulty < mu): informative, move mu down toward difficulty
|
|
43
|
+
* - Success on easier card: uninformative, no change
|
|
44
|
+
* - Failure on harder card: uninformative, no change
|
|
45
|
+
*
|
|
46
|
+
* When evidence is informative, sigma also decreases to reflect increased confidence.
|
|
47
|
+
* When evidence is uninformative, both mu and sigma remain unchanged.
|
|
32
48
|
*
|
|
33
49
|
* @param mu Current skill level estimate
|
|
34
50
|
* @param sigma Current uncertainty (standard deviation)
|
|
35
51
|
* @param cardDifficulty Difficulty of the card reviewed
|
|
36
52
|
* @param outcome Review result (FAIL/HARD/GOOD/EASY)
|
|
53
|
+
* @param params Optional parameters to customize update behavior
|
|
37
54
|
* @returns Updated skill level with new mu and sigma
|
|
38
55
|
*/
|
|
39
|
-
static update(mu, sigma, cardDifficulty, outcome) {
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
const
|
|
43
|
-
//
|
|
44
|
-
const
|
|
45
|
-
|
|
46
|
-
|
|
47
|
-
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
const
|
|
52
|
-
|
|
53
|
-
|
|
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
|
|
67
|
-
*/
|
|
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
|
|
56
|
+
static update(mu, sigma, cardDifficulty, outcome, params = {}) {
|
|
57
|
+
const { updateCoefficient = exports.DEFAULT_SKILL_PARAMS.updateCoefficient, sigmaDecay = exports.DEFAULT_SKILL_PARAMS.sigmaDecay, minSigma = exports.DEFAULT_SKILL_PARAMS.minSigma, maxSigma = exports.DEFAULT_SKILL_PARAMS.maxSigma } = params;
|
|
58
|
+
const isSuccess = outcome === 'EASY' || outcome === 'GOOD';
|
|
59
|
+
const isFailure = outcome === 'FAIL' || outcome === 'HARD';
|
|
60
|
+
// Determine if this observation is informative
|
|
61
|
+
const isInformative = (isSuccess && cardDifficulty > mu) || // Success on harder card
|
|
62
|
+
(isFailure && cardDifficulty < mu); // Failure on easier card
|
|
63
|
+
if (!isInformative) {
|
|
64
|
+
// Uninformative evidence: no change to skill estimate
|
|
65
|
+
return { mu, sigma };
|
|
66
|
+
}
|
|
67
|
+
// Informative evidence: move mu toward card difficulty
|
|
68
|
+
const newMu = mu + updateCoefficient * (cardDifficulty - mu);
|
|
69
|
+
// Decrease sigma since we learned something
|
|
70
|
+
const newSigma = sigma * sigmaDecay;
|
|
71
71
|
return {
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
volatility: 0.06 // Fixed value, not used in IRT
|
|
72
|
+
mu: Math.max(0, newMu),
|
|
73
|
+
sigma: Math.max(minSigma, Math.min(maxSigma, newSigma))
|
|
75
74
|
};
|
|
76
75
|
}
|
|
77
76
|
/**
|
|
@@ -127,36 +126,6 @@ class SkillLevelModel {
|
|
|
127
126
|
sigma: initialSigma
|
|
128
127
|
};
|
|
129
128
|
}
|
|
130
|
-
/**
|
|
131
|
-
* Create a default skill level in legacy format.
|
|
132
|
-
* @deprecated Use createDefault() instead
|
|
133
|
-
*/
|
|
134
|
-
static createDefaultLegacy(initialRatingDeviation = 50, initialVolatility = 0.06) {
|
|
135
|
-
return {
|
|
136
|
-
rating: 0,
|
|
137
|
-
rating_deviation: initialRatingDeviation,
|
|
138
|
-
volatility: initialVolatility
|
|
139
|
-
};
|
|
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
|
-
}
|
|
160
129
|
/**
|
|
161
130
|
* Check if the skill level has high confidence (low uncertainty).
|
|
162
131
|
*
|
|
@@ -170,23 +139,8 @@ class SkillLevelModel {
|
|
|
170
139
|
}
|
|
171
140
|
exports.SkillLevelModel = SkillLevelModel;
|
|
172
141
|
/**
|
|
173
|
-
*
|
|
174
|
-
*
|
|
175
|
-
*
|
|
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.
|
|
142
|
+
* Discrimination parameter for predictSuccess logistic function.
|
|
143
|
+
* Controls how sharply probability transitions around difficulty = mu.
|
|
144
|
+
* Higher values = steeper curve. 0.05 gives gradual transition over ~60 difficulty units.
|
|
182
145
|
*/
|
|
183
146
|
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;
|