@vue-skuilder/db 0.1.31-b → 0.1.31
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/{contentSource-ygoFw9oV.d.ts → contentSource-Bdwkvqa8.d.ts} +16 -0
- package/dist/{contentSource-B7nXusjk.d.cts → contentSource-DF1nUbPQ.d.cts} +16 -0
- package/dist/core/index.d.cts +34 -3
- package/dist/core/index.d.ts +34 -3
- package/dist/core/index.js +510 -50
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +510 -50
- package/dist/core/index.mjs.map +1 -1
- package/dist/{dataLayerProvider-BW7HvkMt.d.cts → dataLayerProvider-BKmVoyJR.d.ts} +20 -1
- package/dist/{dataLayerProvider-BfXUVDuG.d.ts → dataLayerProvider-BQdfJuBN.d.cts} +20 -1
- package/dist/impl/couch/index.d.cts +156 -4
- package/dist/impl/couch/index.d.ts +156 -4
- package/dist/impl/couch/index.js +730 -41
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +729 -41
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.d.cts +3 -2
- package/dist/impl/static/index.d.ts +3 -2
- package/dist/impl/static/index.js +467 -31
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +467 -31
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/index.d.cts +64 -3
- package/dist/index.d.ts +64 -3
- package/dist/index.js +948 -72
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +948 -72
- package/dist/index.mjs.map +1 -1
- package/package.json +3 -3
- package/src/core/interfaces/contentSource.ts +6 -0
- package/src/core/interfaces/courseDB.ts +6 -0
- package/src/core/interfaces/dataLayerProvider.ts +20 -0
- package/src/core/navigators/Pipeline.ts +414 -9
- package/src/core/navigators/PipelineAssembler.ts +23 -18
- package/src/core/navigators/PipelineDebugger.ts +35 -1
- package/src/core/navigators/filters/hierarchyDefinition.ts +78 -8
- package/src/core/navigators/generators/prescribed.ts +95 -0
- package/src/core/navigators/index.ts +12 -0
- package/src/impl/common/BaseUserDB.ts +4 -1
- package/src/impl/couch/CourseSyncService.ts +356 -0
- package/src/impl/couch/PouchDataLayerProvider.ts +21 -1
- package/src/impl/couch/courseDB.ts +60 -13
- package/src/impl/couch/index.ts +1 -0
- package/src/impl/static/courseDB.ts +5 -0
- package/src/study/ItemQueue.ts +42 -0
- package/src/study/SessionController.ts +195 -22
- package/src/study/SpacedRepetition.ts +3 -1
- package/tests/core/navigators/Pipeline.test.ts +1 -1
- package/tests/core/navigators/PipelineAssembler.test.ts +15 -14
|
@@ -8,6 +8,21 @@ import {
|
|
|
8
8
|
isFilter,
|
|
9
9
|
} from './index';
|
|
10
10
|
import { logger } from '../../util/logger';
|
|
11
|
+
import type { Pipeline, CardSpaceDiagnosis } from './Pipeline';
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* Captured reference to the most recently created Pipeline instance.
|
|
15
|
+
* Used by the debug API to run diagnostics against the live pipeline.
|
|
16
|
+
*/
|
|
17
|
+
let _activePipeline: Pipeline | null = null;
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Register a pipeline instance for diagnostic access.
|
|
21
|
+
* Called by Pipeline constructor.
|
|
22
|
+
*/
|
|
23
|
+
export function registerPipelineForDebug(pipeline: Pipeline): void {
|
|
24
|
+
_activePipeline = pipeline;
|
|
25
|
+
}
|
|
11
26
|
|
|
12
27
|
// ============================================================================
|
|
13
28
|
// PIPELINE DEBUGGER
|
|
@@ -76,6 +91,7 @@ export interface PipelineRunReport {
|
|
|
76
91
|
origin: 'new' | 'review' | 'unknown';
|
|
77
92
|
finalScore: number;
|
|
78
93
|
provenance: StrategyContribution[];
|
|
94
|
+
tags?: string[];
|
|
79
95
|
selected: boolean;
|
|
80
96
|
}>;
|
|
81
97
|
}
|
|
@@ -135,6 +151,7 @@ export function buildRunReport(
|
|
|
135
151
|
origin: getOrigin(card),
|
|
136
152
|
finalScore: card.score,
|
|
137
153
|
provenance: card.provenance,
|
|
154
|
+
tags: card.tags,
|
|
138
155
|
selected: selectedIds.has(card.cardId),
|
|
139
156
|
}));
|
|
140
157
|
|
|
@@ -459,6 +476,22 @@ export const pipelineDebugAPI = {
|
|
|
459
476
|
console.groupEnd();
|
|
460
477
|
},
|
|
461
478
|
|
|
479
|
+
/**
|
|
480
|
+
* Scan the full card space through the filter chain for the current user.
|
|
481
|
+
*
|
|
482
|
+
* Reports how many cards are well-indicated and how many are new.
|
|
483
|
+
* Use this to understand how the search space grows during onboarding.
|
|
484
|
+
*
|
|
485
|
+
* @param threshold - Score threshold for "well indicated" (default 0.10)
|
|
486
|
+
*/
|
|
487
|
+
async diagnoseCardSpace(threshold?: number): Promise<CardSpaceDiagnosis | null> {
|
|
488
|
+
if (!_activePipeline) {
|
|
489
|
+
logger.info('[Pipeline Debug] No active pipeline. Run a session first.');
|
|
490
|
+
return null;
|
|
491
|
+
}
|
|
492
|
+
return _activePipeline.diagnoseCardSpace({ threshold });
|
|
493
|
+
},
|
|
494
|
+
|
|
462
495
|
/**
|
|
463
496
|
* Show help.
|
|
464
497
|
*/
|
|
@@ -471,6 +504,7 @@ Commands:
|
|
|
471
504
|
.showRun(id|index) Show summary of a specific run (by index or ID suffix)
|
|
472
505
|
.showCard(cardId) Show provenance trail for a specific card
|
|
473
506
|
.explainReviews() Analyze why reviews were/weren't selected
|
|
507
|
+
.diagnoseCardSpace() Scan full card space through filters (async)
|
|
474
508
|
.showRegistry() Show navigator registry (classes + roles)
|
|
475
509
|
.showStrategies() Show registry + strategy mapping from last run
|
|
476
510
|
.listRuns() List all captured runs in table format
|
|
@@ -482,7 +516,7 @@ Commands:
|
|
|
482
516
|
Example:
|
|
483
517
|
window.skuilder.pipeline.showLastRun()
|
|
484
518
|
window.skuilder.pipeline.showRun(1)
|
|
485
|
-
window.skuilder.pipeline.
|
|
519
|
+
await window.skuilder.pipeline.diagnoseCardSpace()
|
|
486
520
|
`);
|
|
487
521
|
},
|
|
488
522
|
};
|
|
@@ -20,6 +20,15 @@ interface TagPrerequisite {
|
|
|
20
20
|
/** Minimum interaction count (default: 3) */
|
|
21
21
|
minCount?: number;
|
|
22
22
|
};
|
|
23
|
+
/**
|
|
24
|
+
* Score multiplier applied to cards that carry this prereq tag while
|
|
25
|
+
* the gate is still closed. Steers the pipeline toward cards that help
|
|
26
|
+
* unlock the gated content. Falls away once the prereq is met.
|
|
27
|
+
*
|
|
28
|
+
* Example: `preReqBoost: 1.3` gives a 30% score increase to cards
|
|
29
|
+
* tagged `gpc:expose:t-T` while `gpc:intro:t-T` is still locked.
|
|
30
|
+
*/
|
|
31
|
+
preReqBoost?: number;
|
|
23
32
|
}
|
|
24
33
|
|
|
25
34
|
/**
|
|
@@ -38,7 +47,7 @@ const DEFAULT_MIN_COUNT = 3;
|
|
|
38
47
|
* A filter strategy that gates cards based on prerequisite mastery.
|
|
39
48
|
*
|
|
40
49
|
* Cards are locked until the user masters all prerequisite tags.
|
|
41
|
-
* Locked cards receive score * 0.
|
|
50
|
+
* Locked cards receive score * 0.05 (strong penalty, not hard filter).
|
|
42
51
|
*
|
|
43
52
|
* Mastery is determined by:
|
|
44
53
|
* - User's ELO for the tag exceeds threshold (or avgElo if not specified)
|
|
@@ -94,8 +103,13 @@ export default class HierarchyDefinitionNavigator extends ContentNavigator imple
|
|
|
94
103
|
|
|
95
104
|
if (prereq.masteryThreshold?.minElo !== undefined) {
|
|
96
105
|
return userTagElo.score >= prereq.masteryThreshold.minElo;
|
|
106
|
+
} else if (prereq.masteryThreshold?.minCount !== undefined) {
|
|
107
|
+
// Explicit minCount without minElo: count alone is sufficient.
|
|
108
|
+
// The config author specified a concrete interaction threshold —
|
|
109
|
+
// don't additionally require above-average ELO.
|
|
110
|
+
return true;
|
|
97
111
|
} else {
|
|
98
|
-
//
|
|
112
|
+
// No thresholds specified at all: fall back to above-average ELO
|
|
99
113
|
return userTagElo.score >= userGlobalElo;
|
|
100
114
|
}
|
|
101
115
|
}
|
|
@@ -195,17 +209,51 @@ export default class HierarchyDefinitionNavigator extends ContentNavigator imple
|
|
|
195
209
|
}
|
|
196
210
|
}
|
|
197
211
|
|
|
212
|
+
/**
|
|
213
|
+
* Build a map of prereq tag → max configured boost for all *closed* gates.
|
|
214
|
+
*
|
|
215
|
+
* When a gate is closed (prereqs unmet), cards carrying that gate's prereq
|
|
216
|
+
* tags get boosted — steering the pipeline toward content that helps unlock
|
|
217
|
+
* the gated material. Once the gate opens, the boost disappears.
|
|
218
|
+
*/
|
|
219
|
+
private getPreReqBoosts(
|
|
220
|
+
unlockedTags: Set<string>,
|
|
221
|
+
masteredTags: Set<string>
|
|
222
|
+
): Map<string, number> {
|
|
223
|
+
const boosts = new Map<string, number>();
|
|
224
|
+
|
|
225
|
+
for (const [tagId, prereqs] of Object.entries(this.config.prerequisites)) {
|
|
226
|
+
// Only boost prereqs of closed gates
|
|
227
|
+
if (unlockedTags.has(tagId)) continue;
|
|
228
|
+
|
|
229
|
+
for (const prereq of prereqs) {
|
|
230
|
+
if (!prereq.preReqBoost || prereq.preReqBoost <= 1.0) continue;
|
|
231
|
+
// Only boost prereqs that aren't already met
|
|
232
|
+
if (masteredTags.has(prereq.tag)) continue;
|
|
233
|
+
|
|
234
|
+
const existing = boosts.get(prereq.tag) ?? 1.0;
|
|
235
|
+
boosts.set(prereq.tag, Math.max(existing, prereq.preReqBoost));
|
|
236
|
+
}
|
|
237
|
+
}
|
|
238
|
+
|
|
239
|
+
return boosts;
|
|
240
|
+
}
|
|
241
|
+
|
|
198
242
|
/**
|
|
199
243
|
* CardFilter.transform implementation.
|
|
200
244
|
*
|
|
201
|
-
*
|
|
245
|
+
* Two effects:
|
|
246
|
+
* 1. Cards with locked tags receive score * 0.05 (gating penalty)
|
|
247
|
+
* 2. Cards carrying prereq tags of closed gates receive a configured
|
|
248
|
+
* boost (preReqBoost), steering toward content that unlocks gates
|
|
202
249
|
*/
|
|
203
250
|
async transform(cards: WeightedCard[], context: FilterContext): Promise<WeightedCard[]> {
|
|
204
251
|
// Get mastery state
|
|
205
252
|
const masteredTags = await this.getMasteredTags(context);
|
|
206
253
|
const unlockedTags = this.getUnlockedTags(masteredTags);
|
|
254
|
+
const preReqBoosts = this.getPreReqBoosts(unlockedTags, masteredTags);
|
|
207
255
|
|
|
208
|
-
// Apply prerequisite gating
|
|
256
|
+
// Apply prerequisite gating + prereq boosting
|
|
209
257
|
const gated: WeightedCard[] = [];
|
|
210
258
|
|
|
211
259
|
for (const card of cards) {
|
|
@@ -215,9 +263,31 @@ export default class HierarchyDefinitionNavigator extends ContentNavigator imple
|
|
|
215
263
|
unlockedTags,
|
|
216
264
|
masteredTags
|
|
217
265
|
);
|
|
218
|
-
const LOCKED_PENALTY = 0.
|
|
219
|
-
|
|
220
|
-
|
|
266
|
+
const LOCKED_PENALTY = 0.02;
|
|
267
|
+
let finalScore = isUnlocked ? card.score : card.score * LOCKED_PENALTY;
|
|
268
|
+
let action: 'passed' | 'penalized' | 'boosted' = isUnlocked ? 'passed' : 'penalized';
|
|
269
|
+
let finalReason = reason;
|
|
270
|
+
|
|
271
|
+
// Apply prereq boost to cards that passed gating (don't boost locked cards)
|
|
272
|
+
if (isUnlocked && preReqBoosts.size > 0) {
|
|
273
|
+
const cardTags = card.tags ?? [];
|
|
274
|
+
let maxBoost = 1.0;
|
|
275
|
+
const boostedPrereqs: string[] = [];
|
|
276
|
+
|
|
277
|
+
for (const tag of cardTags) {
|
|
278
|
+
const boost = preReqBoosts.get(tag);
|
|
279
|
+
if (boost && boost > maxBoost) {
|
|
280
|
+
maxBoost = boost;
|
|
281
|
+
boostedPrereqs.push(tag);
|
|
282
|
+
}
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (maxBoost > 1.0) {
|
|
286
|
+
finalScore *= maxBoost;
|
|
287
|
+
action = 'boosted';
|
|
288
|
+
finalReason = `${reason} | preReqBoost ×${maxBoost.toFixed(2)} for ${boostedPrereqs.join(', ')}`;
|
|
289
|
+
}
|
|
290
|
+
}
|
|
221
291
|
|
|
222
292
|
gated.push({
|
|
223
293
|
...card,
|
|
@@ -230,7 +300,7 @@ export default class HierarchyDefinitionNavigator extends ContentNavigator imple
|
|
|
230
300
|
strategyId: this.strategyId || 'NAVIGATION_STRATEGY-hierarchy',
|
|
231
301
|
action,
|
|
232
302
|
score: finalScore,
|
|
233
|
-
reason,
|
|
303
|
+
reason: finalReason,
|
|
234
304
|
},
|
|
235
305
|
],
|
|
236
306
|
});
|
|
@@ -0,0 +1,95 @@
|
|
|
1
|
+
import type { CourseDBInterface } from '../../interfaces/courseDB';
|
|
2
|
+
import type { UserDBInterface } from '../../interfaces/userDB';
|
|
3
|
+
import { ContentNavigator } from '../index';
|
|
4
|
+
import type { WeightedCard } from '../index';
|
|
5
|
+
import type { ContentNavigationStrategyData } from '../../types/contentNavigationStrategy';
|
|
6
|
+
import type { CardGenerator, GeneratorContext } from './types';
|
|
7
|
+
import { logger } from '@db/util/logger';
|
|
8
|
+
|
|
9
|
+
// ============================================================================
|
|
10
|
+
// PRESCRIBED CARDS GENERATOR
|
|
11
|
+
// ============================================================================
|
|
12
|
+
//
|
|
13
|
+
// A generator that always emits a configured list of card IDs at score 1.0.
|
|
14
|
+
//
|
|
15
|
+
// Use case: Cold-start curriculum bootstrapping. Ensures critical cards
|
|
16
|
+
// (e.g., intro-s, early WS exercises) are always in the candidate set
|
|
17
|
+
// regardless of ELO proximity sampling. Filters (hierarchy, priority)
|
|
18
|
+
// still run — cards whose utility has expired get penalized normally
|
|
19
|
+
// and drop out of the top-N selection.
|
|
20
|
+
//
|
|
21
|
+
// Config format:
|
|
22
|
+
// { "cardIds": ["c-intro-s-S", "c-ws-sit-abc123", ...] }
|
|
23
|
+
//
|
|
24
|
+
// ============================================================================
|
|
25
|
+
|
|
26
|
+
interface PrescribedConfig {
|
|
27
|
+
cardIds: string[];
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
export default class PrescribedCardsGenerator extends ContentNavigator implements CardGenerator {
|
|
31
|
+
name: string;
|
|
32
|
+
private config: PrescribedConfig;
|
|
33
|
+
|
|
34
|
+
constructor(
|
|
35
|
+
user: UserDBInterface,
|
|
36
|
+
course: CourseDBInterface,
|
|
37
|
+
strategyData: ContentNavigationStrategyData
|
|
38
|
+
) {
|
|
39
|
+
super(user, course, strategyData);
|
|
40
|
+
this.name = strategyData.name || 'Prescribed Cards';
|
|
41
|
+
|
|
42
|
+
try {
|
|
43
|
+
const parsed = JSON.parse(strategyData.serializedData);
|
|
44
|
+
this.config = { cardIds: parsed.cardIds || [] };
|
|
45
|
+
} catch {
|
|
46
|
+
this.config = { cardIds: [] };
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
logger.debug(
|
|
50
|
+
`[Prescribed] Initialized with ${this.config.cardIds.length} prescribed cards`
|
|
51
|
+
);
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
async getWeightedCards(limit: number, _context?: GeneratorContext): Promise<WeightedCard[]> {
|
|
55
|
+
if (this.config.cardIds.length === 0) {
|
|
56
|
+
return [];
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const courseId = this.course.getCourseID();
|
|
60
|
+
|
|
61
|
+
// Filter out cards the user has already interacted with
|
|
62
|
+
const activeCards = await this.user.getActiveCards();
|
|
63
|
+
const activeIds = new Set(activeCards.map((ac) => ac.cardID));
|
|
64
|
+
const eligibleIds = this.config.cardIds.filter((id) => !activeIds.has(id));
|
|
65
|
+
|
|
66
|
+
if (eligibleIds.length === 0) {
|
|
67
|
+
logger.debug('[Prescribed] All prescribed cards already active, returning empty');
|
|
68
|
+
return [];
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
// Emit at score 1.0 — CompositeGenerator deduplicates, and if ELO
|
|
72
|
+
// also surfaces the same card, the composite picks the higher score.
|
|
73
|
+
const cards: WeightedCard[] = eligibleIds.slice(0, limit).map((cardId) => ({
|
|
74
|
+
cardId,
|
|
75
|
+
courseId,
|
|
76
|
+
score: 1.0,
|
|
77
|
+
provenance: [
|
|
78
|
+
{
|
|
79
|
+
strategy: 'prescribed',
|
|
80
|
+
strategyName: this.strategyName || this.name,
|
|
81
|
+
strategyId: this.strategyId || 'NAVIGATION_STRATEGY-prescribed',
|
|
82
|
+
action: 'generated' as const,
|
|
83
|
+
score: 1.0,
|
|
84
|
+
reason: `Prescribed card (${eligibleIds.length} eligible of ${this.config.cardIds.length} configured)`,
|
|
85
|
+
},
|
|
86
|
+
],
|
|
87
|
+
}));
|
|
88
|
+
|
|
89
|
+
logger.info(
|
|
90
|
+
`[Prescribed] Emitting ${cards.length} cards (${eligibleIds.length} eligible, ${activeIds.size} already active)`
|
|
91
|
+
);
|
|
92
|
+
|
|
93
|
+
return cards;
|
|
94
|
+
}
|
|
95
|
+
}
|
|
@@ -140,8 +140,10 @@ export async function initializeNavigatorRegistry(): Promise<void> {
|
|
|
140
140
|
import('./generators/elo'),
|
|
141
141
|
import('./generators/srs'),
|
|
142
142
|
]);
|
|
143
|
+
const prescribedModule = await import('./generators/prescribed');
|
|
143
144
|
registerNavigator('elo', eloModule.default);
|
|
144
145
|
registerNavigator('srs', srsModule.default);
|
|
146
|
+
registerNavigator('prescribed', prescribedModule.default);
|
|
145
147
|
|
|
146
148
|
// Import and register filters
|
|
147
149
|
const [
|
|
@@ -346,6 +348,7 @@ export function getCardOrigin(card: WeightedCard): 'new' | 'review' | 'failed' {
|
|
|
346
348
|
export enum Navigators {
|
|
347
349
|
ELO = 'elo',
|
|
348
350
|
SRS = 'srs',
|
|
351
|
+
PRESCRIBED = 'prescribed',
|
|
349
352
|
HIERARCHY = 'hierarchyDefinition',
|
|
350
353
|
INTERFERENCE = 'interferenceMitigator',
|
|
351
354
|
RELATIVE_PRIORITY = 'relativePriority',
|
|
@@ -384,6 +387,7 @@ export enum NavigatorRole {
|
|
|
384
387
|
export const NavigatorRoles: Record<Navigators, NavigatorRole> = {
|
|
385
388
|
[Navigators.ELO]: NavigatorRole.GENERATOR,
|
|
386
389
|
[Navigators.SRS]: NavigatorRole.GENERATOR,
|
|
390
|
+
[Navigators.PRESCRIBED]: NavigatorRole.GENERATOR,
|
|
387
391
|
[Navigators.HIERARCHY]: NavigatorRole.FILTER,
|
|
388
392
|
[Navigators.INTERFERENCE]: NavigatorRole.FILTER,
|
|
389
393
|
[Navigators.RELATIVE_PRIORITY]: NavigatorRole.FILTER,
|
|
@@ -624,4 +628,12 @@ export abstract class ContentNavigator implements StudyContentSource {
|
|
|
624
628
|
async getWeightedCards(_limit: number): Promise<WeightedCard[]> {
|
|
625
629
|
throw new Error(`${this.constructor.name} must implement getWeightedCards(). `);
|
|
626
630
|
}
|
|
631
|
+
|
|
632
|
+
/**
|
|
633
|
+
* Set ephemeral hints for the next pipeline run.
|
|
634
|
+
* No-op for non-Pipeline navigators. Pipeline overrides this.
|
|
635
|
+
*/
|
|
636
|
+
setEphemeralHints(_hints: Record<string, unknown>): void {
|
|
637
|
+
// no-op — only Pipeline implements this
|
|
638
|
+
}
|
|
627
639
|
}
|
|
@@ -174,10 +174,13 @@ Currently logged-in as ${this._username}.`
|
|
|
174
174
|
const docsToDelete = allDocs.rows
|
|
175
175
|
.filter((row) => {
|
|
176
176
|
const id = row.id;
|
|
177
|
-
// Delete user progress data but preserve
|
|
177
|
+
// Delete user progress data but preserve authentication and user identity
|
|
178
178
|
return (
|
|
179
179
|
id.startsWith(DocTypePrefixes[DocType.CARDRECORD]) || // Card interaction history
|
|
180
180
|
id.startsWith(DocTypePrefixes[DocType.SCHEDULED_CARD]) || // Scheduled reviews
|
|
181
|
+
id.startsWith(DocTypePrefixes[DocType.STRATEGY_STATE]) || // Strategy state (user prefs, progression)
|
|
182
|
+
id.startsWith(DocTypePrefixes[DocType.USER_OUTCOME]) || // Evolutionary orchestration outcomes
|
|
183
|
+
id.startsWith(DocTypePrefixes[DocType.STRATEGY_LEARNING_STATE]) || // Strategy learning state
|
|
181
184
|
id === BaseUser.DOC_IDS.COURSE_REGISTRATIONS || // Course registrations
|
|
182
185
|
id === BaseUser.DOC_IDS.CLASSROOM_REGISTRATIONS || // Classroom registrations
|
|
183
186
|
id === BaseUser.DOC_IDS.CONFIG // User config
|