@shaxpir/duiduidui-models 1.9.13 → 1.9.14

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.
@@ -0,0 +1,158 @@
1
+ import { Doc } from '@shaxpir/sharedb/lib/client';
2
+ import { CompactDateTime } from "@shaxpir/shaxpir-common";
3
+ import { ShareSync } from '../repo';
4
+ import { Conditions } from './Condition';
5
+ import { Content, ContentBody, ContentId, ContentMeta } from "./Content";
6
+ import { UncertainValue } from './Progress';
7
+ /**
8
+ * Display metadata for a Collection, used in the UI.
9
+ */
10
+ export interface CollectionDisplay {
11
+ name: string;
12
+ description: string;
13
+ icon?: string;
14
+ sort_order: number;
15
+ }
16
+ /**
17
+ * A snapshot of a term's proficiency within this collection.
18
+ * Stored in term_scores map, keyed by termId.
19
+ */
20
+ export interface CollectionTermScore {
21
+ text: string;
22
+ sense_rank: number;
23
+ theta: number;
24
+ }
25
+ /**
26
+ * The payload for a Collection document.
27
+ *
28
+ * A Collection represents a scoped subset of the corpus defined by filters,
29
+ * combined with a competency model tracking the user's mastery within that scope.
30
+ */
31
+ export interface CollectionPayload {
32
+ collection_key: string;
33
+ conditions: Conditions;
34
+ display: CollectionDisplay;
35
+ proficiency: UncertainValue;
36
+ term_scores: {
37
+ [termId: string]: CollectionTermScore;
38
+ };
39
+ total_cards: number;
40
+ last_activity_utc: CompactDateTime | null;
41
+ is_visible: boolean;
42
+ is_builtin: boolean;
43
+ }
44
+ export interface CollectionBody extends ContentBody {
45
+ meta: ContentMeta;
46
+ payload: CollectionPayload;
47
+ }
48
+ /**
49
+ * Parameters for creating a new Collection.
50
+ */
51
+ export interface CollectionCreateParams {
52
+ collectionKey: string;
53
+ conditions: Conditions;
54
+ display: CollectionDisplay;
55
+ isBuiltin?: boolean;
56
+ isVisible?: boolean;
57
+ }
58
+ export declare class Collection extends Content {
59
+ /**
60
+ * Generate a deterministic Collection ID from userId and collectionKey.
61
+ * The same user + collectionKey always produces the same ID.
62
+ */
63
+ static makeCollectionId(userId: ContentId, collectionKey: string): ContentId;
64
+ constructor(doc: Doc, shouldAcquire: boolean, shareSync: ShareSync);
65
+ /**
66
+ * Create a new Collection for a user.
67
+ */
68
+ static create(userId: ContentId, params: CollectionCreateParams): Collection;
69
+ get payload(): CollectionPayload;
70
+ get collectionKey(): string;
71
+ get conditions(): Conditions;
72
+ get display(): CollectionDisplay;
73
+ get name(): string;
74
+ get description(): string;
75
+ get icon(): string | undefined;
76
+ get sortOrder(): number;
77
+ get proficiency(): UncertainValue;
78
+ get proficiencyRating(): number;
79
+ get proficiencyRatingDeviation(): number;
80
+ get proficiencyVolatility(): number;
81
+ get termScores(): {
82
+ [termId: string]: CollectionTermScore;
83
+ };
84
+ /**
85
+ * Get the number of unique terms the user has reviewed in this collection.
86
+ */
87
+ get termsReviewedCount(): number;
88
+ /**
89
+ * Get the term score for a specific term, or undefined if not reviewed.
90
+ */
91
+ getTermScore(termId: string): CollectionTermScore | undefined;
92
+ get totalCards(): number;
93
+ get lastActivityUtc(): CompactDateTime | null;
94
+ get isVisible(): boolean;
95
+ get isBuiltin(): boolean;
96
+ /**
97
+ * Check if this collection has any proficiency data yet.
98
+ */
99
+ get hasProgress(): boolean;
100
+ /**
101
+ * Calculate the percentage of terms that are "mastered" (theta >= 0.8).
102
+ * Returns 0 if no terms have been reviewed.
103
+ */
104
+ get masteryPercentage(): number;
105
+ /**
106
+ * Calculate the percentage of total cards that have been reviewed.
107
+ * Returns 0 if total_cards is 0 or not set.
108
+ */
109
+ get reviewedPercentage(): number;
110
+ /**
111
+ * Get counts of terms by grade (based on theta ranges).
112
+ */
113
+ getGradeDistribution(): {
114
+ A: number;
115
+ B: number;
116
+ C: number;
117
+ D: number;
118
+ F: number;
119
+ };
120
+ /**
121
+ * Update the Glicko-2 proficiency rating.
122
+ */
123
+ setProficiency(value: UncertainValue): void;
124
+ /**
125
+ * Update or add a term score in the collection.
126
+ */
127
+ setTermScore(termId: string, score: CollectionTermScore): void;
128
+ /**
129
+ * Update just the theta value for an existing term score.
130
+ * Does nothing if the term doesn't exist in term_scores.
131
+ */
132
+ updateTermTheta(termId: string, theta: number): void;
133
+ /**
134
+ * Remove a term score from the collection.
135
+ */
136
+ removeTermScore(termId: string): void;
137
+ /**
138
+ * Update the cached total card count for this collection.
139
+ */
140
+ setTotalCards(count: number): void;
141
+ /**
142
+ * Update the last activity timestamp.
143
+ */
144
+ setLastActivityUtc(timestamp: CompactDateTime): void;
145
+ /**
146
+ * Update visibility of this collection.
147
+ */
148
+ setVisible(visible: boolean): void;
149
+ /**
150
+ * Update display metadata.
151
+ */
152
+ setDisplay(display: CollectionDisplay): void;
153
+ /**
154
+ * Update the condition definition.
155
+ * Note: Changing conditions on a builtin collection is not recommended.
156
+ */
157
+ setConditions(conditions: Conditions): void;
158
+ }
@@ -0,0 +1,315 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.Collection = void 0;
4
+ const shaxpir_common_1 = require("@shaxpir/shaxpir-common");
5
+ const repo_1 = require("../repo");
6
+ const Content_1 = require("./Content");
7
+ const ContentKind_1 = require("./ContentKind");
8
+ const Operation_1 = require("./Operation");
9
+ class Collection extends Content_1.Content {
10
+ /**
11
+ * Generate a deterministic Collection ID from userId and collectionKey.
12
+ * The same user + collectionKey always produces the same ID.
13
+ */
14
+ static makeCollectionId(userId, collectionKey) {
15
+ return shaxpir_common_1.CachingHasher.makeMd5Base62Hash(`${ContentKind_1.ContentKind.COLLECTION}-${userId}-${collectionKey}`);
16
+ }
17
+ constructor(doc, shouldAcquire, shareSync) {
18
+ super(doc, shouldAcquire, shareSync);
19
+ }
20
+ /**
21
+ * Create a new Collection for a user.
22
+ */
23
+ static create(userId, params) {
24
+ const now = shaxpir_common_1.ClockService.getClock().now();
25
+ const collectionId = Collection.makeCollectionId(userId, params.collectionKey);
26
+ return repo_1.ShareSyncFactory.get().createContent({
27
+ meta: {
28
+ ref: collectionId,
29
+ kind: ContentKind_1.ContentKind.COLLECTION,
30
+ id: collectionId,
31
+ owner: userId,
32
+ created_at: now,
33
+ updated_at: now
34
+ },
35
+ payload: {
36
+ collection_key: params.collectionKey,
37
+ conditions: params.conditions,
38
+ display: params.display,
39
+ // Initial Glicko-2 proficiency - high uncertainty, no data yet
40
+ proficiency: {
41
+ rating: 0, // Start at zero (no demonstrated skill)
42
+ rating_deviation: 50, // High initial uncertainty
43
+ volatility: 0.06 // Moderate initial volatility
44
+ },
45
+ // Empty term scores map - populated as user reviews cards
46
+ term_scores: {},
47
+ // Progress starts at zero
48
+ total_cards: 0,
49
+ last_activity_utc: null,
50
+ // Visibility defaults
51
+ is_visible: params.isVisible ?? true,
52
+ is_builtin: params.isBuiltin ?? false
53
+ }
54
+ });
55
+ }
56
+ // ============================================================
57
+ // Payload accessor
58
+ // ============================================================
59
+ get payload() {
60
+ this.checkDisposed("Collection.payload");
61
+ return this.doc.data.payload;
62
+ }
63
+ // ============================================================
64
+ // Identity getters
65
+ // ============================================================
66
+ get collectionKey() {
67
+ this.checkDisposed("Collection.collectionKey");
68
+ return this.payload.collection_key;
69
+ }
70
+ // ============================================================
71
+ // Condition definition getters
72
+ // ============================================================
73
+ get conditions() {
74
+ this.checkDisposed("Collection.conditions");
75
+ return this.payload.conditions;
76
+ }
77
+ // ============================================================
78
+ // Display metadata getters
79
+ // ============================================================
80
+ get display() {
81
+ this.checkDisposed("Collection.display");
82
+ return this.payload.display;
83
+ }
84
+ get name() {
85
+ this.checkDisposed("Collection.name");
86
+ return this.payload.display.name;
87
+ }
88
+ get description() {
89
+ this.checkDisposed("Collection.description");
90
+ return this.payload.display.description;
91
+ }
92
+ get icon() {
93
+ this.checkDisposed("Collection.icon");
94
+ return this.payload.display.icon;
95
+ }
96
+ get sortOrder() {
97
+ this.checkDisposed("Collection.sortOrder");
98
+ return this.payload.display.sort_order;
99
+ }
100
+ // ============================================================
101
+ // Proficiency getters
102
+ // ============================================================
103
+ get proficiency() {
104
+ this.checkDisposed("Collection.proficiency");
105
+ return shaxpir_common_1.Struct.clone(this.payload.proficiency);
106
+ }
107
+ get proficiencyRating() {
108
+ this.checkDisposed("Collection.proficiencyRating");
109
+ return this.payload.proficiency.rating;
110
+ }
111
+ get proficiencyRatingDeviation() {
112
+ this.checkDisposed("Collection.proficiencyRatingDeviation");
113
+ return this.payload.proficiency.rating_deviation;
114
+ }
115
+ get proficiencyVolatility() {
116
+ this.checkDisposed("Collection.proficiencyVolatility");
117
+ return this.payload.proficiency.volatility;
118
+ }
119
+ // ============================================================
120
+ // Term scores getters
121
+ // ============================================================
122
+ get termScores() {
123
+ this.checkDisposed("Collection.termScores");
124
+ return this.payload.term_scores;
125
+ }
126
+ /**
127
+ * Get the number of unique terms the user has reviewed in this collection.
128
+ */
129
+ get termsReviewedCount() {
130
+ this.checkDisposed("Collection.termsReviewedCount");
131
+ return Object.keys(this.payload.term_scores).length;
132
+ }
133
+ /**
134
+ * Get the term score for a specific term, or undefined if not reviewed.
135
+ */
136
+ getTermScore(termId) {
137
+ this.checkDisposed("Collection.getTermScore");
138
+ return this.payload.term_scores[termId];
139
+ }
140
+ // ============================================================
141
+ // Progress statistics getters
142
+ // ============================================================
143
+ get totalCards() {
144
+ this.checkDisposed("Collection.totalCards");
145
+ return this.payload.total_cards;
146
+ }
147
+ get lastActivityUtc() {
148
+ this.checkDisposed("Collection.lastActivityUtc");
149
+ return this.payload.last_activity_utc;
150
+ }
151
+ // ============================================================
152
+ // Visibility and type getters
153
+ // ============================================================
154
+ get isVisible() {
155
+ this.checkDisposed("Collection.isVisible");
156
+ return this.payload.is_visible;
157
+ }
158
+ get isBuiltin() {
159
+ this.checkDisposed("Collection.isBuiltin");
160
+ return this.payload.is_builtin;
161
+ }
162
+ // ============================================================
163
+ // Computed properties
164
+ // ============================================================
165
+ /**
166
+ * Check if this collection has any proficiency data yet.
167
+ */
168
+ get hasProgress() {
169
+ this.checkDisposed("Collection.hasProgress");
170
+ return Object.keys(this.payload.term_scores).length > 0;
171
+ }
172
+ /**
173
+ * Calculate the percentage of terms that are "mastered" (theta >= 0.8).
174
+ * Returns 0 if no terms have been reviewed.
175
+ */
176
+ get masteryPercentage() {
177
+ this.checkDisposed("Collection.masteryPercentage");
178
+ const scores = Object.values(this.payload.term_scores);
179
+ if (scores.length === 0)
180
+ return 0;
181
+ const mastered = scores.filter(s => s.theta >= 0.8).length;
182
+ return Math.round((mastered / scores.length) * 100);
183
+ }
184
+ /**
185
+ * Calculate the percentage of total cards that have been reviewed.
186
+ * Returns 0 if total_cards is 0 or not set.
187
+ */
188
+ get reviewedPercentage() {
189
+ this.checkDisposed("Collection.reviewedPercentage");
190
+ if (this.payload.total_cards === 0)
191
+ return 0;
192
+ const reviewed = Object.keys(this.payload.term_scores).length;
193
+ return Math.round((reviewed / this.payload.total_cards) * 100);
194
+ }
195
+ /**
196
+ * Get counts of terms by grade (based on theta ranges).
197
+ */
198
+ getGradeDistribution() {
199
+ this.checkDisposed("Collection.getGradeDistribution");
200
+ const scores = Object.values(this.payload.term_scores);
201
+ return {
202
+ A: scores.filter(s => s.theta >= 0.8).length,
203
+ B: scores.filter(s => s.theta >= 0.6 && s.theta < 0.8).length,
204
+ C: scores.filter(s => s.theta >= 0.4 && s.theta < 0.6).length,
205
+ D: scores.filter(s => s.theta >= 0.2 && s.theta < 0.4).length,
206
+ F: scores.filter(s => s.theta < 0.2).length
207
+ };
208
+ }
209
+ // ============================================================
210
+ // Mutators
211
+ // ============================================================
212
+ /**
213
+ * Update the Glicko-2 proficiency rating.
214
+ */
215
+ setProficiency(value) {
216
+ this.checkDisposed("Collection.setProficiency");
217
+ if (!shaxpir_common_1.Struct.equals(this.payload.proficiency, value)) {
218
+ const batch = new Operation_1.BatchOperation(this);
219
+ batch.setPathValue(['payload', 'proficiency', 'rating'], value.rating);
220
+ batch.setPathValue(['payload', 'proficiency', 'rating_deviation'], value.rating_deviation);
221
+ batch.setPathValue(['payload', 'proficiency', 'volatility'], value.volatility);
222
+ batch.commit();
223
+ }
224
+ }
225
+ /**
226
+ * Update or add a term score in the collection.
227
+ */
228
+ setTermScore(termId, score) {
229
+ this.checkDisposed("Collection.setTermScore");
230
+ const batch = new Operation_1.BatchOperation(this);
231
+ batch.setPathValue(['payload', 'term_scores', termId], score);
232
+ batch.commit();
233
+ }
234
+ /**
235
+ * Update just the theta value for an existing term score.
236
+ * Does nothing if the term doesn't exist in term_scores.
237
+ */
238
+ updateTermTheta(termId, theta) {
239
+ this.checkDisposed("Collection.updateTermTheta");
240
+ if (this.payload.term_scores[termId]) {
241
+ const batch = new Operation_1.BatchOperation(this);
242
+ batch.setPathValue(['payload', 'term_scores', termId, 'theta'], theta);
243
+ batch.commit();
244
+ }
245
+ }
246
+ /**
247
+ * Remove a term score from the collection.
248
+ */
249
+ removeTermScore(termId) {
250
+ this.checkDisposed("Collection.removeTermScore");
251
+ if (this.payload.term_scores[termId]) {
252
+ const batch = new Operation_1.BatchOperation(this);
253
+ batch.removeValueAtPath(['payload', 'term_scores', termId]);
254
+ batch.commit();
255
+ }
256
+ }
257
+ /**
258
+ * Update the cached total card count for this collection.
259
+ */
260
+ setTotalCards(count) {
261
+ this.checkDisposed("Collection.setTotalCards");
262
+ if (this.payload.total_cards !== count) {
263
+ const batch = new Operation_1.BatchOperation(this);
264
+ batch.setPathValue(['payload', 'total_cards'], count);
265
+ batch.commit();
266
+ }
267
+ }
268
+ /**
269
+ * Update the last activity timestamp.
270
+ */
271
+ setLastActivityUtc(timestamp) {
272
+ this.checkDisposed("Collection.setLastActivityUtc");
273
+ if (this.payload.last_activity_utc !== timestamp) {
274
+ const batch = new Operation_1.BatchOperation(this);
275
+ batch.setPathValue(['payload', 'last_activity_utc'], timestamp);
276
+ batch.commit();
277
+ }
278
+ }
279
+ /**
280
+ * Update visibility of this collection.
281
+ */
282
+ setVisible(visible) {
283
+ this.checkDisposed("Collection.setVisible");
284
+ if (this.payload.is_visible !== visible) {
285
+ const batch = new Operation_1.BatchOperation(this);
286
+ batch.setPathValue(['payload', 'is_visible'], visible);
287
+ batch.commit();
288
+ }
289
+ }
290
+ /**
291
+ * Update display metadata.
292
+ */
293
+ setDisplay(display) {
294
+ this.checkDisposed("Collection.setDisplay");
295
+ const batch = new Operation_1.BatchOperation(this);
296
+ batch.setPathValue(['payload', 'display', 'name'], display.name);
297
+ batch.setPathValue(['payload', 'display', 'description'], display.description);
298
+ batch.setPathValue(['payload', 'display', 'sort_order'], display.sort_order);
299
+ if (display.icon !== undefined) {
300
+ batch.setPathValue(['payload', 'display', 'icon'], display.icon);
301
+ }
302
+ batch.commit();
303
+ }
304
+ /**
305
+ * Update the condition definition.
306
+ * Note: Changing conditions on a builtin collection is not recommended.
307
+ */
308
+ setConditions(conditions) {
309
+ this.checkDisposed("Collection.setConditions");
310
+ const batch = new Operation_1.BatchOperation(this);
311
+ batch.setPathValue(['payload', 'conditions'], conditions);
312
+ batch.commit();
313
+ }
314
+ }
315
+ exports.Collection = Collection;
@@ -47,11 +47,12 @@ export interface ComponentCondition extends BaseCondition {
47
47
  mode: 'any' | 'all';
48
48
  }
49
49
  export type AnyCondition = TagCondition | GradeCondition | ThetaCondition | TemporalCondition | CountCondition | StarredCondition | DifficultyCondition | HasTermCondition | ComponentCondition;
50
- export interface ConditionFilters {
50
+ export interface Conditions {
51
51
  all?: AnyCondition[];
52
52
  any?: AnyCondition[];
53
53
  none?: AnyCondition[];
54
54
  }
55
+ export type ConditionFilters = Conditions;
55
56
  export declare const Condition: {
56
57
  tag: (name: string) => TagCondition;
57
58
  grade: (grade: "A" | "B" | "C" | "D" | "F") => GradeCondition;
@@ -89,44 +90,44 @@ export declare const Condition: {
89
90
  /**
90
91
  * Check if starred condition is required (in 'all' section)
91
92
  */
92
- requiresStarred: (filters?: ConditionFilters) => boolean;
93
+ requiresStarred: (conditions?: Conditions) => boolean;
93
94
  /**
94
95
  * Check if starred condition is optional (in 'any' section)
95
96
  */
96
- allowsStarred: (filters?: ConditionFilters) => boolean;
97
+ allowsStarred: (conditions?: Conditions) => boolean;
97
98
  /**
98
99
  * Check if starred condition is excluded (in 'none' section)
99
100
  */
100
- excludesStarred: (filters?: ConditionFilters) => boolean;
101
+ excludesStarred: (conditions?: Conditions) => boolean;
101
102
  /**
102
103
  * Check if starred condition exists anywhere in filters
103
104
  */
104
- hasStarred: (filters?: ConditionFilters) => boolean;
105
+ hasStarred: (conditions?: Conditions) => boolean;
105
106
  /**
106
107
  * Check if a specific tag is required (in 'all' section)
107
108
  */
108
- requiresTag: (filters: ConditionFilters | undefined, tag: string) => boolean;
109
+ requiresTag: (conditions: Conditions | undefined, tag: string) => boolean;
109
110
  /**
110
111
  * Check if a specific tag is optional (in 'any' section)
111
112
  */
112
- allowsTag: (filters: ConditionFilters | undefined, tag: string) => boolean;
113
+ allowsTag: (conditions: Conditions | undefined, tag: string) => boolean;
113
114
  /**
114
115
  * Check if a specific tag is excluded (in 'none' section)
115
116
  */
116
- excludesTag: (filters: ConditionFilters | undefined, tag: string) => boolean;
117
+ excludesTag: (conditions: Conditions | undefined, tag: string) => boolean;
117
118
  /**
118
119
  * Check if a specific grade is required (in 'all' section)
119
120
  */
120
- requiresGrade: (filters: ConditionFilters | undefined, grade: "A" | "B" | "C" | "D" | "F") => boolean;
121
+ requiresGrade: (conditions: Conditions | undefined, grade: "A" | "B" | "C" | "D" | "F") => boolean;
121
122
  /**
122
123
  * Check if any condition of a specific type exists in filters
123
124
  */
124
- hasConditionType: (filters: ConditionFilters | undefined, type: string) => boolean;
125
+ hasConditionType: (conditions: Conditions | undefined, type: string) => boolean;
125
126
  /**
126
127
  * Validate filters for logical inconsistencies
127
128
  * Returns an array of error messages, empty if valid
128
129
  */
129
- validate: (filters?: ConditionFilters) => string[];
130
+ validate: (conditions?: Conditions) => string[];
130
131
  hasStarredInAll: (filters?: ConditionFilters) => boolean;
131
132
  hasStarredInAny: (filters?: ConditionFilters) => boolean;
132
133
  hasStarredInNone: (filters?: ConditionFilters) => boolean;
@@ -49,84 +49,84 @@ exports.Condition = {
49
49
  /**
50
50
  * Check if starred condition is required (in 'all' section)
51
51
  */
52
- requiresStarred: (filters) => {
53
- return !!(filters?.all?.some(c => c.type === 'starred' && c.value === true));
52
+ requiresStarred: (conditions) => {
53
+ return !!(conditions?.all?.some(c => c.type === 'starred' && c.value === true));
54
54
  },
55
55
  /**
56
56
  * Check if starred condition is optional (in 'any' section)
57
57
  */
58
- allowsStarred: (filters) => {
59
- return !!(filters?.any?.some(c => c.type === 'starred' && c.value === true));
58
+ allowsStarred: (conditions) => {
59
+ return !!(conditions?.any?.some(c => c.type === 'starred' && c.value === true));
60
60
  },
61
61
  /**
62
62
  * Check if starred condition is excluded (in 'none' section)
63
63
  */
64
- excludesStarred: (filters) => {
65
- return !!(filters?.none?.some(c => c.type === 'starred' && c.value === true));
64
+ excludesStarred: (conditions) => {
65
+ return !!(conditions?.none?.some(c => c.type === 'starred' && c.value === true));
66
66
  },
67
67
  /**
68
68
  * Check if starred condition exists anywhere in filters
69
69
  */
70
- hasStarred: (filters) => {
71
- return exports.Condition.requiresStarred(filters) ||
72
- exports.Condition.allowsStarred(filters) ||
73
- exports.Condition.excludesStarred(filters);
70
+ hasStarred: (conditions) => {
71
+ return exports.Condition.requiresStarred(conditions) ||
72
+ exports.Condition.allowsStarred(conditions) ||
73
+ exports.Condition.excludesStarred(conditions);
74
74
  },
75
75
  /**
76
76
  * Check if a specific tag is required (in 'all' section)
77
77
  */
78
- requiresTag: (filters, tag) => {
79
- return !!(filters?.all?.some(c => c.type === 'tag' && c.tag === tag));
78
+ requiresTag: (conditions, tag) => {
79
+ return !!(conditions?.all?.some(c => c.type === 'tag' && c.tag === tag));
80
80
  },
81
81
  /**
82
82
  * Check if a specific tag is optional (in 'any' section)
83
83
  */
84
- allowsTag: (filters, tag) => {
85
- return !!(filters?.any?.some(c => c.type === 'tag' && c.tag === tag));
84
+ allowsTag: (conditions, tag) => {
85
+ return !!(conditions?.any?.some(c => c.type === 'tag' && c.tag === tag));
86
86
  },
87
87
  /**
88
88
  * Check if a specific tag is excluded (in 'none' section)
89
89
  */
90
- excludesTag: (filters, tag) => {
91
- return !!(filters?.none?.some(c => c.type === 'tag' && c.tag === tag));
90
+ excludesTag: (conditions, tag) => {
91
+ return !!(conditions?.none?.some(c => c.type === 'tag' && c.tag === tag));
92
92
  },
93
93
  /**
94
94
  * Check if a specific grade is required (in 'all' section)
95
95
  */
96
- requiresGrade: (filters, grade) => {
97
- return !!(filters?.all?.some(c => c.type === 'grade' && c.grade === grade));
96
+ requiresGrade: (conditions, grade) => {
97
+ return !!(conditions?.all?.some(c => c.type === 'grade' && c.grade === grade));
98
98
  },
99
99
  /**
100
100
  * Check if any condition of a specific type exists in filters
101
101
  */
102
- hasConditionType: (filters, type) => {
103
- return !!(filters?.all?.some(c => c.type === type) ||
104
- filters?.any?.some(c => c.type === type) ||
105
- filters?.none?.some(c => c.type === type));
102
+ hasConditionType: (conditions, type) => {
103
+ return !!(conditions?.all?.some(c => c.type === type) ||
104
+ conditions?.any?.some(c => c.type === type) ||
105
+ conditions?.none?.some(c => c.type === type));
106
106
  },
107
107
  /**
108
108
  * Validate filters for logical inconsistencies
109
109
  * Returns an array of error messages, empty if valid
110
110
  */
111
- validate: (filters) => {
112
- if (!filters)
111
+ validate: (conditions) => {
112
+ if (!conditions)
113
113
  return [];
114
114
  const errors = [];
115
115
  // Check for contradictory starred conditions
116
- if (exports.Condition.requiresStarred(filters) && exports.Condition.excludesStarred(filters)) {
116
+ if (exports.Condition.requiresStarred(conditions) && exports.Condition.excludesStarred(conditions)) {
117
117
  errors.push('Cannot require starred items and exclude starred items at the same time');
118
118
  }
119
119
  // Check for contradictory tags
120
- const allTags = filters.all?.filter(c => c.type === 'tag').map(c => c.tag) || [];
121
- const noneTags = filters.none?.filter(c => c.type === 'tag').map(c => c.tag) || [];
120
+ const allTags = conditions.all?.filter(c => c.type === 'tag').map(c => c.tag) || [];
121
+ const noneTags = conditions.none?.filter(c => c.type === 'tag').map(c => c.tag) || [];
122
122
  allTags.forEach(tag => {
123
123
  if (noneTags.includes(tag)) {
124
124
  errors.push(`Tag "${tag}" cannot be both required and excluded`);
125
125
  }
126
126
  });
127
127
  // Check for contradictory grades
128
- const allGrades = filters.all?.filter(c => c.type === 'grade').map(c => c.grade) || [];
129
- const noneGrades = filters.none?.filter(c => c.type === 'grade').map(c => c.grade) || [];
128
+ const allGrades = conditions.all?.filter(c => c.type === 'grade').map(c => c.grade) || [];
129
+ const noneGrades = conditions.none?.filter(c => c.type === 'grade').map(c => c.grade) || [];
130
130
  allGrades.forEach(grade => {
131
131
  if (noneGrades.includes(grade)) {
132
132
  errors.push(`Grade "${grade}" cannot be both required and excluded`);
@@ -137,24 +137,24 @@ exports.Condition = {
137
137
  errors.push(`Cannot require multiple grades: ${allGrades.join(', ')} - an item can only have one grade`);
138
138
  }
139
139
  // Check for contradictory has_term conditions
140
- const requiresTerm = filters.all?.some(c => c.type === 'has_term' && c.value === true);
141
- const excludesTerm = filters.all?.some(c => c.type === 'has_term' && c.value === false);
140
+ const requiresTerm = conditions.all?.some(c => c.type === 'has_term' && c.value === true);
141
+ const excludesTerm = conditions.all?.some(c => c.type === 'has_term' && c.value === false);
142
142
  if (requiresTerm && excludesTerm) {
143
143
  errors.push('Cannot require both has_term and no_term');
144
144
  }
145
145
  // Check for implicit term requirements conflicting with no_term
146
146
  const termDependentTypes = ['grade', 'theta', 'starred', 'temporal', 'count'];
147
- const hasTermDependentInAll = filters.all?.some(c => termDependentTypes.includes(c.type));
148
- const hasTermDependentInAny = filters.any?.some(c => termDependentTypes.includes(c.type));
147
+ const hasTermDependentInAll = conditions.all?.some(c => termDependentTypes.includes(c.type));
148
+ const hasTermDependentInAny = conditions.any?.some(c => termDependentTypes.includes(c.type));
149
149
  if (excludesTerm && hasTermDependentInAll) {
150
- const conflictingTypes = filters.all
150
+ const conflictingTypes = conditions.all
151
151
  ?.filter(c => termDependentTypes.includes(c.type))
152
152
  .map(c => c.type)
153
153
  .join(', ');
154
154
  errors.push(`Conditions [${conflictingTypes}] require a term record, but has_term is false`);
155
155
  }
156
156
  if (excludesTerm && hasTermDependentInAny) {
157
- const conflictingTypes = filters.any
157
+ const conflictingTypes = conditions.any
158
158
  ?.filter(c => termDependentTypes.includes(c.type))
159
159
  .map(c => c.type)
160
160
  .join(', ');
@@ -2,6 +2,7 @@ import { Doc } from '@shaxpir/sharedb/lib/client';
2
2
  import { CompactDateTime, MultiTime } from '@shaxpir/shaxpir-common';
3
3
  import { ShareSync } from '../repo';
4
4
  import { BillingPayload } from './Billing';
5
+ import { CollectionPayload } from './Collection';
5
6
  import { ContentKind } from './ContentKind';
6
7
  import { DevicePayload } from './Device';
7
8
  import { ImagePayload } from './Image';
@@ -37,7 +38,7 @@ export interface ContentMeta {
37
38
  created_at: MultiTime;
38
39
  updated_at: MultiTime;
39
40
  }
40
- export type ContentPayload = BillingPayload | DevicePayload | ImagePayload | MetricPayload | ProfilePayload | ProgressPayload | SessionPayload | SocialPayload | TermPayload | UserPayload | WorkspacePayload;
41
+ export type ContentPayload = BillingPayload | CollectionPayload | DevicePayload | ImagePayload | MetricPayload | ProfilePayload | ProgressPayload | SessionPayload | SocialPayload | TermPayload | UserPayload | WorkspacePayload;
41
42
  export declare abstract class Content extends Model {
42
43
  static ID_LENGTH: number;
43
44
  constructor(doc: Doc, shouldAcquire: boolean, shareSync: ShareSync);
@@ -1,5 +1,6 @@
1
1
  export declare enum ContentKind {
2
2
  BILLING = "billing",
3
+ COLLECTION = "collection",
3
4
  DEVICE = "device",
4
5
  METRIC = "metric",
5
6
  PROFILE = "profile",
@@ -6,6 +6,7 @@ var ContentKind;
6
6
  // No models yet for these, but they're definitely coming
7
7
  ContentKind["BILLING"] = "billing";
8
8
  // These are all the current Content subclasses
9
+ ContentKind["COLLECTION"] = "collection";
9
10
  ContentKind["DEVICE"] = "device";
10
11
  ContentKind["METRIC"] = "metric";
11
12
  ContentKind["PROFILE"] = "profile";
@@ -2,7 +2,7 @@ import { Doc } from '@shaxpir/sharedb/lib/client';
2
2
  import { CompactDateTime } from "@shaxpir/shaxpir-common";
3
3
  import { ShareSync } from '../repo';
4
4
  import { Content, ContentBody, ContentId, ContentMeta } from "./Content";
5
- import { ConditionFilters } from './Condition';
5
+ import { Conditions } from './Condition';
6
6
  import { BuiltInDbState, DatabaseVersion } from '../util/Database';
7
7
  import { SearchHistory, SearchState } from './SearchState';
8
8
  export interface LastSync {
@@ -34,7 +34,7 @@ export interface DevicePayload {
34
34
  chinese_font?: string;
35
35
  raw_search_text?: string;
36
36
  star_filter?: boolean;
37
- conditions?: ConditionFilters;
37
+ conditions?: Conditions;
38
38
  download_queue?: DownloadQueueItem[];
39
39
  upload_queue?: UploadQueueItem[];
40
40
  builtin_db?: BuiltInDbState;
@@ -1,4 +1,4 @@
1
- import { ConditionFilters } from './Condition';
1
+ import { Conditions } from './Condition';
2
2
  /**
3
3
  * Represents the data portion of a search state.
4
4
  * This is the shareable/persistable part of a search - it excludes
@@ -12,7 +12,7 @@ import { ConditionFilters } from './Condition';
12
12
  export interface SearchState {
13
13
  query?: string;
14
14
  senseRank?: number;
15
- conditions?: ConditionFilters;
15
+ conditions?: Conditions;
16
16
  }
17
17
  /**
18
18
  * Maximum number of entries in the search history stack.
@@ -1,7 +1,7 @@
1
1
  import { Doc } from '@shaxpir/sharedb/lib/client';
2
2
  import { MultiTime } from "@shaxpir/shaxpir-common";
3
3
  import { ShareSync } from '../repo';
4
- import { ConditionFilters } from './Condition';
4
+ import { Conditions } from './Condition';
5
5
  import { Content, ContentBody, ContentId, ContentMeta, ContentRef } from "./Content";
6
6
  import { Review } from './Review';
7
7
  import { UncertainValue } from './Progress';
@@ -12,7 +12,7 @@ export interface SessionConfig {
12
12
  };
13
13
  selection?: {
14
14
  difficulty_ramp?: number;
15
- filters?: ConditionFilters;
15
+ filters?: Conditions;
16
16
  strategy?: {
17
17
  name: string;
18
18
  constants?: Record<string, any>;
@@ -3,7 +3,7 @@ import { CompactDate, CompactDateTime, MultiTime } from "@shaxpir/shaxpir-common
3
3
  import { ShareSync } from '../repo';
4
4
  import { ArrayView } from './ArrayView';
5
5
  import { Content, ContentBody, ContentId, ContentMeta } from "./Content";
6
- import { ConditionFilters } from './Condition';
6
+ import { Conditions } from './Condition';
7
7
  import { Session } from './Session';
8
8
  export interface JourneyEntry {
9
9
  key: string;
@@ -16,7 +16,7 @@ export interface WorkspacePayload {
16
16
  journey: {
17
17
  [key: string]: CompactDateTime;
18
18
  };
19
- global_conditions: ConditionFilters;
19
+ global_conditions: Conditions;
20
20
  uploaded_avatars: ContentId[];
21
21
  default_voice?: VoicePreference;
22
22
  }
@@ -30,7 +30,7 @@ export declare class Workspace extends Content {
30
30
  private _uploadedAvatarsView;
31
31
  constructor(doc: Doc, shouldAcquire: boolean, shareSync: ShareSync);
32
32
  get payload(): WorkspacePayload;
33
- getGlobalConditions(): ConditionFilters;
33
+ getGlobalConditions(): Conditions;
34
34
  static makeWorkspaceId(userId: ContentId): ContentId;
35
35
  /**
36
36
  * Validate that a WorkspacePayload has all required fields.
@@ -46,7 +46,7 @@ export declare class Workspace extends Content {
46
46
  hasJourneyDate(journeyKey: string): boolean;
47
47
  getJourneyDate(journeyKey: string): CompactDateTime;
48
48
  setJourneyDate(journeyKey: string, journeyDate: CompactDateTime): void;
49
- setGlobalConditions(filters: ConditionFilters): void;
49
+ setGlobalConditions(conditions: Conditions): void;
50
50
  get defaultVoice(): VoicePreference;
51
51
  setDefaultVoice(value: VoicePreference): void;
52
52
  }
@@ -130,10 +130,10 @@ class Workspace extends Content_1.Content {
130
130
  batch.setPathValue(['payload', 'journey', journeyKey], journeyDate);
131
131
  batch.commit();
132
132
  }
133
- setGlobalConditions(filters) {
133
+ setGlobalConditions(conditions) {
134
134
  this.checkDisposed("Workspace.setGlobalConditions");
135
135
  const batch = new Operation_1.BatchOperation(this);
136
- batch.setPathValue(['payload', 'global_conditions'], filters);
136
+ batch.setPathValue(['payload', 'global_conditions'], conditions);
137
137
  batch.commit();
138
138
  }
139
139
  get defaultVoice() {
@@ -2,6 +2,7 @@ export * from './ArrayView';
2
2
  export * from './BayesianScore';
3
3
  export * from './Billing';
4
4
  export * from './ChangeModel';
5
+ export * from './Collection';
5
6
  export * from './Condition';
6
7
  export * from './Content';
7
8
  export * from './ContentKind';
@@ -19,6 +19,7 @@ __exportStar(require("./ArrayView"), exports);
19
19
  __exportStar(require("./BayesianScore"), exports);
20
20
  __exportStar(require("./Billing"), exports);
21
21
  __exportStar(require("./ChangeModel"), exports);
22
+ __exportStar(require("./Collection"), exports);
22
23
  __exportStar(require("./Condition"), exports);
23
24
  __exportStar(require("./Content"), exports);
24
25
  __exportStar(require("./ContentKind"), exports);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@shaxpir/duiduidui-models",
3
- "version": "1.9.13",
3
+ "version": "1.9.14",
4
4
  "repository": {
5
5
  "type": "git",
6
6
  "url": "https://github.com/shaxpir/duiduidui-models"