@shaxpir/duiduidui-models 1.36.2 → 1.37.0
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/BayesianScore.d.ts +5 -0
- package/dist/models/BayesianScore.js +25 -0
- package/dist/models/Collection.d.ts +7 -0
- package/dist/models/Collection.js +17 -1
- package/dist/models/Session.d.ts +5 -0
- package/dist/models/Session.js +14 -0
- package/dist/models/SkillLevel.d.ts +10 -0
- package/dist/models/SkillLevel.js +29 -0
- package/dist/models/Term.d.ts +10 -0
- package/dist/models/Term.js +36 -0
- package/package.json +1 -1
|
@@ -11,6 +11,11 @@ export interface BayesianScore {
|
|
|
11
11
|
}
|
|
12
12
|
export declare class BayesianScoreModel {
|
|
13
13
|
static apply(prevScore: BayesianScore, result: ReviewResult, weight: number): BayesianScore;
|
|
14
|
+
/**
|
|
15
|
+
* Reverse a previously applied Bayesian update.
|
|
16
|
+
* Subtracts the same deltas that apply() added, then recalculates theta and uncertainty.
|
|
17
|
+
*/
|
|
18
|
+
static reverse(currentScore: BayesianScore, result: ReviewResult, weight: number): BayesianScore;
|
|
14
19
|
/**
|
|
15
20
|
* Calculate uncertainty for a given Bayesian score.
|
|
16
21
|
* Higher values indicate less confidence in the theta estimate.
|
|
@@ -24,6 +24,31 @@ class BayesianScoreModel {
|
|
|
24
24
|
nextScore.uncertainty = BayesianScoreModel.calculateUncertainty(nextScore.alpha, nextScore.beta);
|
|
25
25
|
return nextScore;
|
|
26
26
|
}
|
|
27
|
+
/**
|
|
28
|
+
* Reverse a previously applied Bayesian update.
|
|
29
|
+
* Subtracts the same deltas that apply() added, then recalculates theta and uncertainty.
|
|
30
|
+
*/
|
|
31
|
+
static reverse(currentScore, result, weight) {
|
|
32
|
+
if (isNaN(weight) || !isFinite(weight) || weight < 0 || weight > 1) {
|
|
33
|
+
throw new Error(`illegal weight value: ${weight}`);
|
|
34
|
+
}
|
|
35
|
+
const prevScore = shaxpir_common_1.Struct.clone(currentScore);
|
|
36
|
+
if (result == 'EASY') {
|
|
37
|
+
prevScore.alpha -= (weight * 1.0);
|
|
38
|
+
}
|
|
39
|
+
else if (result == 'GOOD') {
|
|
40
|
+
prevScore.alpha -= (weight * 0.7);
|
|
41
|
+
}
|
|
42
|
+
else if (result == 'HARD') {
|
|
43
|
+
prevScore.beta -= (weight * 0.3);
|
|
44
|
+
}
|
|
45
|
+
else if (result == 'FAIL') {
|
|
46
|
+
prevScore.beta -= (weight * 1.0);
|
|
47
|
+
}
|
|
48
|
+
prevScore.theta = prevScore.alpha / (prevScore.alpha + prevScore.beta);
|
|
49
|
+
prevScore.uncertainty = BayesianScoreModel.calculateUncertainty(prevScore.alpha, prevScore.beta);
|
|
50
|
+
return prevScore;
|
|
51
|
+
}
|
|
27
52
|
/**
|
|
28
53
|
* Calculate uncertainty for a given Bayesian score.
|
|
29
54
|
* Higher values indicate less confidence in the theta estimate.
|
|
@@ -37,6 +37,7 @@ export interface CollectionPayload {
|
|
|
37
37
|
term_scores: {
|
|
38
38
|
[termKey: string]: number;
|
|
39
39
|
};
|
|
40
|
+
engagement_score: number;
|
|
40
41
|
total_cards: number;
|
|
41
42
|
last_activity_utc: CompactDateTime | null;
|
|
42
43
|
is_visible: boolean;
|
|
@@ -96,6 +97,12 @@ export declare class Collection extends Content {
|
|
|
96
97
|
get termScores(): {
|
|
97
98
|
[termKey: string]: number;
|
|
98
99
|
};
|
|
100
|
+
/**
|
|
101
|
+
* Cached sum of all term_scores values. Maintained incrementally by
|
|
102
|
+
* setTermScore() and removeTermScore() so that engagement-based sorting
|
|
103
|
+
* doesn't need to iterate the full map.
|
|
104
|
+
*/
|
|
105
|
+
get engagementScore(): number;
|
|
99
106
|
/**
|
|
100
107
|
* Get the number of unique terms the user has reviewed in this collection.
|
|
101
108
|
*/
|
|
@@ -59,6 +59,7 @@ class Collection extends Content_1.Content {
|
|
|
59
59
|
proficiency: SkillLevel_1.SkillLevelModel.createDefault(),
|
|
60
60
|
// Empty term scores map - populated as user reviews cards
|
|
61
61
|
term_scores: {},
|
|
62
|
+
engagement_score: 0,
|
|
62
63
|
// Progress starts at zero
|
|
63
64
|
total_cards: 0,
|
|
64
65
|
last_activity_utc: null,
|
|
@@ -141,6 +142,15 @@ class Collection extends Content_1.Content {
|
|
|
141
142
|
this.checkDisposed("Collection.termScores");
|
|
142
143
|
return this.payload.term_scores;
|
|
143
144
|
}
|
|
145
|
+
/**
|
|
146
|
+
* Cached sum of all term_scores values. Maintained incrementally by
|
|
147
|
+
* setTermScore() and removeTermScore() so that engagement-based sorting
|
|
148
|
+
* doesn't need to iterate the full map.
|
|
149
|
+
*/
|
|
150
|
+
get engagementScore() {
|
|
151
|
+
this.checkDisposed("Collection.engagementScore");
|
|
152
|
+
return this.payload.engagement_score ?? 0;
|
|
153
|
+
}
|
|
144
154
|
/**
|
|
145
155
|
* Get the number of unique terms the user has reviewed in this collection.
|
|
146
156
|
*/
|
|
@@ -274,8 +284,11 @@ class Collection extends Content_1.Content {
|
|
|
274
284
|
setTermScore(text, senseRank, theta) {
|
|
275
285
|
this.checkDisposed("Collection.setTermScore");
|
|
276
286
|
const termKey = Collection.encodeTermKey(text, senseRank);
|
|
287
|
+
const prevTheta = this.payload.term_scores[termKey] ?? 0;
|
|
288
|
+
const prevEngagement = this.payload.engagement_score ?? 0;
|
|
277
289
|
const batch = new Operation_1.BatchOperation(this);
|
|
278
290
|
batch.setPathValue(['payload', 'term_scores', termKey], theta);
|
|
291
|
+
batch.setPathValue(['payload', 'engagement_score'], prevEngagement - prevTheta + theta);
|
|
279
292
|
batch.commit();
|
|
280
293
|
}
|
|
281
294
|
/**
|
|
@@ -284,9 +297,12 @@ class Collection extends Content_1.Content {
|
|
|
284
297
|
removeTermScore(text, senseRank) {
|
|
285
298
|
this.checkDisposed("Collection.removeTermScore");
|
|
286
299
|
const termKey = Collection.encodeTermKey(text, senseRank);
|
|
287
|
-
|
|
300
|
+
const prevTheta = this.payload.term_scores[termKey];
|
|
301
|
+
if (prevTheta !== undefined) {
|
|
302
|
+
const prevEngagement = this.payload.engagement_score ?? 0;
|
|
288
303
|
const batch = new Operation_1.BatchOperation(this);
|
|
289
304
|
batch.removeValueAtPath(['payload', 'term_scores', termKey]);
|
|
305
|
+
batch.setPathValue(['payload', 'engagement_score'], prevEngagement - prevTheta);
|
|
290
306
|
batch.commit();
|
|
291
307
|
}
|
|
292
308
|
}
|
package/dist/models/Session.d.ts
CHANGED
|
@@ -88,6 +88,11 @@ export declare class Session extends Content {
|
|
|
88
88
|
get deviceId(): ContentId;
|
|
89
89
|
get reviews(): Review[];
|
|
90
90
|
get reviewCount(): number;
|
|
91
|
+
/**
|
|
92
|
+
* Remove the last review from this session.
|
|
93
|
+
* Decrements review_count.
|
|
94
|
+
*/
|
|
95
|
+
removeLastReview(): void;
|
|
91
96
|
addReview(review: Review): void;
|
|
92
97
|
get config(): ReviewConfig | undefined;
|
|
93
98
|
setConfig(config: ReviewConfig): void;
|
package/dist/models/Session.js
CHANGED
|
@@ -95,6 +95,20 @@ class Session extends Content_1.Content {
|
|
|
95
95
|
this.checkDisposed("Session.getReviewCount");
|
|
96
96
|
return this._reviewsView.length;
|
|
97
97
|
}
|
|
98
|
+
/**
|
|
99
|
+
* Remove the last review from this session.
|
|
100
|
+
* Decrements review_count.
|
|
101
|
+
*/
|
|
102
|
+
removeLastReview() {
|
|
103
|
+
this.checkDisposed("Session.removeLastReview");
|
|
104
|
+
if (this._reviewsView.length === 0)
|
|
105
|
+
return;
|
|
106
|
+
const lastIndex = this._reviewsView.length - 1;
|
|
107
|
+
this._reviewsView.removeAt(lastIndex);
|
|
108
|
+
const batch = new Operation_1.BatchOperation(this);
|
|
109
|
+
batch.setPathValue(['payload', 'review_count'], this._reviewsView.length);
|
|
110
|
+
batch.commit();
|
|
111
|
+
}
|
|
98
112
|
addReview(review) {
|
|
99
113
|
this.checkDisposed("Session.addReview");
|
|
100
114
|
if (review.hasOwnProperty('context') && review.hasOwnProperty('weight')) {
|
|
@@ -113,6 +113,16 @@ export declare class SkillLevelModel {
|
|
|
113
113
|
* @returns Updated skill level with new mu and sigma
|
|
114
114
|
*/
|
|
115
115
|
static update(mu: number, sigma: number, cardDifficulty: number, outcome: ReviewResult, params?: SkillUpdateParams): SkillLevel;
|
|
116
|
+
/**
|
|
117
|
+
* Reverse a previously applied skill level update.
|
|
118
|
+
* Algebraically inverts the forward update: oldMu = (newMu - coeff * difficulty) / (1 - coeff).
|
|
119
|
+
* If the forward update was uninformative (no change), this is also a no-op.
|
|
120
|
+
*
|
|
121
|
+
* Note: If the forward update hit the Math.max(0, ...) clamp on mu, the original value
|
|
122
|
+
* is not perfectly recoverable. In practice this doesn't happen because mu represents
|
|
123
|
+
* a character count and starts at 0 moving upward.
|
|
124
|
+
*/
|
|
125
|
+
static reverse(mu: number, sigma: number, cardDifficulty: number, outcome: ReviewResult, params?: SkillUpdateParams): SkillLevel;
|
|
116
126
|
/**
|
|
117
127
|
* Map a review outcome to a success score for IRT.
|
|
118
128
|
*
|
|
@@ -73,6 +73,35 @@ class SkillLevelModel {
|
|
|
73
73
|
sigma: Math.max(minSigma, Math.min(maxSigma, newSigma))
|
|
74
74
|
};
|
|
75
75
|
}
|
|
76
|
+
/**
|
|
77
|
+
* Reverse a previously applied skill level update.
|
|
78
|
+
* Algebraically inverts the forward update: oldMu = (newMu - coeff * difficulty) / (1 - coeff).
|
|
79
|
+
* If the forward update was uninformative (no change), this is also a no-op.
|
|
80
|
+
*
|
|
81
|
+
* Note: If the forward update hit the Math.max(0, ...) clamp on mu, the original value
|
|
82
|
+
* is not perfectly recoverable. In practice this doesn't happen because mu represents
|
|
83
|
+
* a character count and starts at 0 moving upward.
|
|
84
|
+
*/
|
|
85
|
+
static reverse(mu, sigma, cardDifficulty, outcome, params = {}) {
|
|
86
|
+
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;
|
|
87
|
+
// Reverse sigma first: oldSigma = newSigma / sigmaDecay
|
|
88
|
+
const oldSigma = sigma / sigmaDecay;
|
|
89
|
+
// Reverse mu: oldMu = (newMu - coeff * difficulty) / (1 - coeff)
|
|
90
|
+
const oldMu = (mu - updateCoefficient * cardDifficulty) / (1 - updateCoefficient);
|
|
91
|
+
// Check if the forward update would have been uninformative from the original state.
|
|
92
|
+
// If so, mu and sigma didn't change — return the current values unchanged.
|
|
93
|
+
const isSuccess = outcome === 'EASY' || outcome === 'GOOD';
|
|
94
|
+
const isFailure = outcome === 'FAIL' || outcome === 'HARD';
|
|
95
|
+
const wasInformative = (isSuccess && cardDifficulty > oldMu) ||
|
|
96
|
+
(isFailure && cardDifficulty < oldMu);
|
|
97
|
+
if (!wasInformative) {
|
|
98
|
+
return { mu, sigma };
|
|
99
|
+
}
|
|
100
|
+
return {
|
|
101
|
+
mu: Math.max(0, oldMu),
|
|
102
|
+
sigma: Math.max(minSigma, Math.min(maxSigma, oldSigma))
|
|
103
|
+
};
|
|
104
|
+
}
|
|
76
105
|
/**
|
|
77
106
|
* Map a review outcome to a success score for IRT.
|
|
78
107
|
*
|
package/dist/models/Term.d.ts
CHANGED
|
@@ -91,5 +91,15 @@ export declare class Term extends Content {
|
|
|
91
91
|
get impliedReviews(): ImpliedReview[];
|
|
92
92
|
get reviewCount(): number;
|
|
93
93
|
get impliedReviewCount(): number;
|
|
94
|
+
/**
|
|
95
|
+
* Remove the last direct review from this term.
|
|
96
|
+
* Decrements review_count and restores last_review_utc from the previous review.
|
|
97
|
+
*/
|
|
98
|
+
removeLastReview(): void;
|
|
99
|
+
/**
|
|
100
|
+
* Remove the last implied review from this term.
|
|
101
|
+
* Decrements implied_review_count.
|
|
102
|
+
*/
|
|
103
|
+
removeLastImpliedReview(): void;
|
|
94
104
|
addReview(review: ReviewLike): void;
|
|
95
105
|
}
|
package/dist/models/Term.js
CHANGED
|
@@ -256,6 +256,42 @@ class Term extends Content_1.Content {
|
|
|
256
256
|
this.checkDisposed("Term.impliedReviewCount");
|
|
257
257
|
return this.payload.implied_review_count;
|
|
258
258
|
}
|
|
259
|
+
/**
|
|
260
|
+
* Remove the last direct review from this term.
|
|
261
|
+
* Decrements review_count and restores last_review_utc from the previous review.
|
|
262
|
+
*/
|
|
263
|
+
removeLastReview() {
|
|
264
|
+
this.checkDisposed("Term.removeLastReview");
|
|
265
|
+
if (this._reviewsView.length === 0)
|
|
266
|
+
return;
|
|
267
|
+
const lastIndex = this._reviewsView.length - 1;
|
|
268
|
+
this._reviewsView.removeAt(lastIndex);
|
|
269
|
+
const batch = new Operation_1.BatchOperation(this);
|
|
270
|
+
batch.setPathValue(['payload', 'review_count'], this._reviewsView.length);
|
|
271
|
+
// Restore last_review_utc from the new last review, or null if no reviews remain
|
|
272
|
+
if (this._reviewsView.length > 0) {
|
|
273
|
+
const prevLast = this._reviewsView.get(this._reviewsView.length - 1);
|
|
274
|
+
batch.setPathValue(['payload', 'last_review_utc'], prevLast.at_utc_time);
|
|
275
|
+
}
|
|
276
|
+
else {
|
|
277
|
+
batch.setPathValue(['payload', 'last_review_utc'], null);
|
|
278
|
+
}
|
|
279
|
+
batch.commit();
|
|
280
|
+
}
|
|
281
|
+
/**
|
|
282
|
+
* Remove the last implied review from this term.
|
|
283
|
+
* Decrements implied_review_count.
|
|
284
|
+
*/
|
|
285
|
+
removeLastImpliedReview() {
|
|
286
|
+
this.checkDisposed("Term.removeLastImpliedReview");
|
|
287
|
+
if (this._impliedReviewsView.length === 0)
|
|
288
|
+
return;
|
|
289
|
+
const lastIndex = this._impliedReviewsView.length - 1;
|
|
290
|
+
this._impliedReviewsView.removeAt(lastIndex);
|
|
291
|
+
const batch = new Operation_1.BatchOperation(this);
|
|
292
|
+
batch.setPathValue(['payload', 'implied_review_count'], this._impliedReviewsView.length);
|
|
293
|
+
batch.commit();
|
|
294
|
+
}
|
|
259
295
|
// Add either an implied or explicit review
|
|
260
296
|
addReview(review) {
|
|
261
297
|
this.checkDisposed("Term.addReview");
|