@vue-skuilder/db 0.1.18 → 0.1.21
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/CLAUDE.md +2 -2
- package/dist/{classroomDB-BgfrVb8d.d.ts → contentSource-BP9hznNV.d.ts} +220 -197
- package/dist/{classroomDB-CTOenngH.d.cts → contentSource-DsJadoBU.d.cts} +220 -197
- package/dist/core/index.d.cts +80 -6
- package/dist/core/index.d.ts +80 -6
- package/dist/core/index.js +735 -1560
- package/dist/core/index.js.map +1 -1
- package/dist/core/index.mjs +708 -1539
- package/dist/core/index.mjs.map +1 -1
- package/dist/{dataLayerProvider-D6PoCwS6.d.cts → dataLayerProvider-CHYrQ5pB.d.cts} +1 -1
- package/dist/{dataLayerProvider-CZxC9GtB.d.ts → dataLayerProvider-MDTxXq2l.d.ts} +1 -1
- package/dist/impl/couch/index.d.cts +8 -23
- package/dist/impl/couch/index.d.ts +8 -23
- package/dist/impl/couch/index.js +723 -1578
- package/dist/impl/couch/index.js.map +1 -1
- package/dist/impl/couch/index.mjs +692 -1552
- package/dist/impl/couch/index.mjs.map +1 -1
- package/dist/impl/static/index.d.cts +25 -8
- package/dist/impl/static/index.d.ts +25 -8
- package/dist/impl/static/index.js +700 -1400
- package/dist/impl/static/index.js.map +1 -1
- package/dist/impl/static/index.mjs +688 -1393
- package/dist/impl/static/index.mjs.map +1 -1
- package/dist/{index-D-Fa4Smt.d.cts → index-B_j6u5E4.d.cts} +1 -1
- package/dist/{index-CD8BZz2k.d.ts → index-Dj0SEgk3.d.ts} +1 -1
- package/dist/index.d.cts +71 -63
- package/dist/index.d.ts +71 -63
- package/dist/index.js +1162 -1996
- package/dist/index.js.map +1 -1
- package/dist/index.mjs +1124 -1955
- package/dist/index.mjs.map +1 -1
- package/dist/pouch/index.js +3 -0
- package/dist/pouch/index.js.map +1 -1
- package/dist/pouch/index.mjs +3 -0
- package/dist/pouch/index.mjs.map +1 -1
- package/dist/{types-CzPDLAK6.d.cts → types-Bn0itutr.d.cts} +1 -1
- package/dist/{types-CewsN87z.d.ts → types-DQaXnuoc.d.ts} +1 -1
- package/dist/{types-legacy-6ettoclI.d.cts → types-legacy-DDY4N-Uq.d.cts} +3 -1
- package/dist/{types-legacy-6ettoclI.d.ts → types-legacy-DDY4N-Uq.d.ts} +3 -1
- package/dist/util/packer/index.d.cts +3 -3
- package/dist/util/packer/index.d.ts +3 -3
- package/docs/navigators-architecture.md +115 -17
- package/package.json +4 -4
- package/src/core/index.ts +1 -0
- package/src/core/interfaces/classroomDB.ts +5 -13
- package/src/core/interfaces/contentSource.ts +6 -66
- package/src/core/interfaces/courseDB.ts +15 -7
- package/src/core/interfaces/userDB.ts +32 -0
- package/src/core/navigators/Pipeline.ts +136 -52
- package/src/core/navigators/PipelineAssembler.ts +1 -1
- package/src/core/navigators/defaults.ts +84 -0
- package/src/core/navigators/{hierarchyDefinition.ts → filters/hierarchyDefinition.ts} +15 -29
- package/src/core/navigators/filters/index.ts +3 -0
- package/src/core/navigators/filters/inferredPreferenceStub.ts +107 -0
- package/src/core/navigators/{interferenceMitigator.ts → filters/interferenceMitigator.ts} +11 -37
- package/src/core/navigators/{relativePriority.ts → filters/relativePriority.ts} +12 -38
- package/src/core/navigators/filters/userGoalStub.ts +136 -0
- package/src/core/navigators/filters/userTagPreference.ts +217 -0
- package/src/core/navigators/{CompositeGenerator.ts → generators/CompositeGenerator.ts} +15 -64
- package/src/core/navigators/{elo.ts → generators/elo.ts} +13 -63
- package/src/core/navigators/{srs.ts → generators/srs.ts} +11 -40
- package/src/core/navigators/generators/types.ts +1 -1
- package/src/core/navigators/index.ts +95 -91
- package/src/core/types/strategyState.ts +84 -0
- package/src/core/types/types-legacy.ts +2 -0
- package/src/impl/common/BaseUserDB.ts +74 -7
- package/src/impl/couch/adminDB.ts +1 -2
- package/src/impl/couch/classroomDB.ts +100 -103
- package/src/impl/couch/courseDB.ts +35 -91
- package/src/impl/couch/pouchdb-setup.ts +7 -0
- package/src/impl/static/StaticDataUnpacker.ts +50 -1
- package/src/impl/static/courseDB.ts +87 -37
- package/src/study/SessionController.ts +122 -202
- package/src/study/SourceMixer.ts +65 -0
- package/src/study/TagFilteredContentSource.ts +49 -92
- package/src/study/index.ts +1 -0
- package/src/study/services/CardHydrationService.ts +165 -81
- package/src/util/dataDirectory.ts +1 -1
- package/src/util/index.ts +0 -1
- package/tests/core/navigators/CompositeGenerator.test.ts +44 -168
- package/tests/core/navigators/Pipeline.test.ts +6 -72
- package/tests/core/navigators/PipelineAssembler.test.ts +8 -58
- package/tests/core/navigators/navigators.test.ts +118 -151
- package/docs/todo-pipeline-optimization.md +0 -117
- package/docs/todo-strategy-state-storage.md +0 -278
- package/src/core/navigators/hardcodedOrder.ts +0 -163
- package/src/util/tuiLogger.ts +0 -139
package/dist/pouch/index.js
CHANGED
|
@@ -40,6 +40,9 @@ var import_pouchdb_find = __toESM(require("pouchdb-find"), 1);
|
|
|
40
40
|
var import_pouchdb_authentication = __toESM(require("@nilock2/pouchdb-authentication"), 1);
|
|
41
41
|
import_pouchdb.default.plugin(import_pouchdb_find.default);
|
|
42
42
|
import_pouchdb.default.plugin(import_pouchdb_authentication.default);
|
|
43
|
+
if (typeof import_pouchdb.default.debug !== "undefined") {
|
|
44
|
+
import_pouchdb.default.debug.disable();
|
|
45
|
+
}
|
|
43
46
|
import_pouchdb.default.defaults({
|
|
44
47
|
// ajax: {
|
|
45
48
|
// timeout: 60000,
|
package/dist/pouch/index.js.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/pouch/index.ts","../../src/impl/couch/pouchdb-setup.ts"],"sourcesContent":["// Export configured PouchDB instance\nexport { default } from '../impl/couch/pouchdb-setup.js';","import PouchDB from 'pouchdb';\nimport PouchDBFind from 'pouchdb-find';\nimport PouchDBAuth from '@nilock2/pouchdb-authentication';\n\n// Register plugins\nPouchDB.plugin(PouchDBFind);\nPouchDB.plugin(PouchDBAuth);\n\n// Configure PouchDB globally\nPouchDB.defaults({\n // ajax: {\n // timeout: 60000,\n // },\n});\n\nexport default PouchDB;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,qBAAoB;AACpB,0BAAwB;AACxB,oCAAwB;AAGxB,eAAAA,QAAQ,OAAO,oBAAAC,OAAW;AAC1B,eAAAD,QAAQ,OAAO,8BAAAE,OAAW;
|
|
1
|
+
{"version":3,"sources":["../../src/pouch/index.ts","../../src/impl/couch/pouchdb-setup.ts"],"sourcesContent":["// Export configured PouchDB instance\nexport { default } from '../impl/couch/pouchdb-setup.js';","import PouchDB from 'pouchdb';\nimport PouchDBFind from 'pouchdb-find';\nimport PouchDBAuth from '@nilock2/pouchdb-authentication';\n\n// Register plugins\nPouchDB.plugin(PouchDBFind);\nPouchDB.plugin(PouchDBAuth);\n\n// Disable PouchDB debug logging to prevent interference with CLI prompts\n// Debug logging (like DerivedLogger.emit) will still go to the TUI log file\n// if initializeTuiLogging() has been called, but won't clutter terminal output\nif (typeof PouchDB.debug !== 'undefined') {\n PouchDB.debug.disable();\n}\n\n// Configure PouchDB globally\nPouchDB.defaults({\n // ajax: {\n // timeout: 60000,\n // },\n});\n\nexport default PouchDB;\n"],"mappings":";;;;;;;;;;;;;;;;;;;;;;;;;;;;;;AAAA;AAAA;AAAA;AAAA;AAAA;;;ACAA,qBAAoB;AACpB,0BAAwB;AACxB,oCAAwB;AAGxB,eAAAA,QAAQ,OAAO,oBAAAC,OAAW;AAC1B,eAAAD,QAAQ,OAAO,8BAAAE,OAAW;AAK1B,IAAI,OAAO,eAAAF,QAAQ,UAAU,aAAa;AACxC,iBAAAA,QAAQ,MAAM,QAAQ;AACxB;AAGA,eAAAA,QAAQ,SAAS;AAAA;AAAA;AAAA;AAIjB,CAAC;AAED,IAAO,wBAAQ,eAAAA;","names":["PouchDB","PouchDBFind","PouchDBAuth"]}
|
package/dist/pouch/index.mjs
CHANGED
|
@@ -4,6 +4,9 @@ import PouchDBFind from "pouchdb-find";
|
|
|
4
4
|
import PouchDBAuth from "@nilock2/pouchdb-authentication";
|
|
5
5
|
PouchDB.plugin(PouchDBFind);
|
|
6
6
|
PouchDB.plugin(PouchDBAuth);
|
|
7
|
+
if (typeof PouchDB.debug !== "undefined") {
|
|
8
|
+
PouchDB.debug.disable();
|
|
9
|
+
}
|
|
7
10
|
PouchDB.defaults({
|
|
8
11
|
// ajax: {
|
|
9
12
|
// timeout: 60000,
|
package/dist/pouch/index.mjs.map
CHANGED
|
@@ -1 +1 @@
|
|
|
1
|
-
{"version":3,"sources":["../../src/impl/couch/pouchdb-setup.ts"],"sourcesContent":["import PouchDB from 'pouchdb';\nimport PouchDBFind from 'pouchdb-find';\nimport PouchDBAuth from '@nilock2/pouchdb-authentication';\n\n// Register plugins\nPouchDB.plugin(PouchDBFind);\nPouchDB.plugin(PouchDBAuth);\n\n// Configure PouchDB globally\nPouchDB.defaults({\n // ajax: {\n // timeout: 60000,\n // },\n});\n\nexport default PouchDB;\n"],"mappings":";AAAA,OAAO,aAAa;AACpB,OAAO,iBAAiB;AACxB,OAAO,iBAAiB;AAGxB,QAAQ,OAAO,WAAW;AAC1B,QAAQ,OAAO,WAAW;
|
|
1
|
+
{"version":3,"sources":["../../src/impl/couch/pouchdb-setup.ts"],"sourcesContent":["import PouchDB from 'pouchdb';\nimport PouchDBFind from 'pouchdb-find';\nimport PouchDBAuth from '@nilock2/pouchdb-authentication';\n\n// Register plugins\nPouchDB.plugin(PouchDBFind);\nPouchDB.plugin(PouchDBAuth);\n\n// Disable PouchDB debug logging to prevent interference with CLI prompts\n// Debug logging (like DerivedLogger.emit) will still go to the TUI log file\n// if initializeTuiLogging() has been called, but won't clutter terminal output\nif (typeof PouchDB.debug !== 'undefined') {\n PouchDB.debug.disable();\n}\n\n// Configure PouchDB globally\nPouchDB.defaults({\n // ajax: {\n // timeout: 60000,\n // },\n});\n\nexport default PouchDB;\n"],"mappings":";AAAA,OAAO,aAAa;AACpB,OAAO,iBAAiB;AACxB,OAAO,iBAAiB;AAGxB,QAAQ,OAAO,WAAW;AAC1B,QAAQ,OAAO,WAAW;AAK1B,IAAI,OAAO,QAAQ,UAAU,aAAa;AACxC,UAAQ,MAAM,QAAQ;AACxB;AAGA,QAAQ,SAAS;AAAA;AAAA;AAAA;AAIjB,CAAC;AAED,IAAO,wBAAQ;","names":[]}
|
|
@@ -13,7 +13,8 @@ declare enum DocType {
|
|
|
13
13
|
CARDRECORD = "CARDRECORD",
|
|
14
14
|
SCHEDULED_CARD = "SCHEDULED_CARD",
|
|
15
15
|
TAG = "TAG",
|
|
16
|
-
NAVIGATION_STRATEGY = "NAVIGATION_STRATEGY"
|
|
16
|
+
NAVIGATION_STRATEGY = "NAVIGATION_STRATEGY",
|
|
17
|
+
STRATEGY_STATE = "STRATEGY_STATE"
|
|
17
18
|
}
|
|
18
19
|
interface QualifiedCardID {
|
|
19
20
|
courseID: string;
|
|
@@ -89,6 +90,7 @@ declare const DocTypePrefixes: {
|
|
|
89
90
|
readonly VIEW: "VIEW";
|
|
90
91
|
readonly PEDAGOGY: "PEDAGOGY";
|
|
91
92
|
readonly NAVIGATION_STRATEGY: "NAVIGATION_STRATEGY";
|
|
93
|
+
readonly STRATEGY_STATE: "STRATEGY_STATE";
|
|
92
94
|
};
|
|
93
95
|
interface CardHistory<T extends CardRecord> {
|
|
94
96
|
_id: PouchDB.Core.DocumentId;
|
|
@@ -13,7 +13,8 @@ declare enum DocType {
|
|
|
13
13
|
CARDRECORD = "CARDRECORD",
|
|
14
14
|
SCHEDULED_CARD = "SCHEDULED_CARD",
|
|
15
15
|
TAG = "TAG",
|
|
16
|
-
NAVIGATION_STRATEGY = "NAVIGATION_STRATEGY"
|
|
16
|
+
NAVIGATION_STRATEGY = "NAVIGATION_STRATEGY",
|
|
17
|
+
STRATEGY_STATE = "STRATEGY_STATE"
|
|
17
18
|
}
|
|
18
19
|
interface QualifiedCardID {
|
|
19
20
|
courseID: string;
|
|
@@ -89,6 +90,7 @@ declare const DocTypePrefixes: {
|
|
|
89
90
|
readonly VIEW: "VIEW";
|
|
90
91
|
readonly PEDAGOGY: "PEDAGOGY";
|
|
91
92
|
readonly NAVIGATION_STRATEGY: "NAVIGATION_STRATEGY";
|
|
93
|
+
readonly STRATEGY_STATE: "STRATEGY_STATE";
|
|
92
94
|
};
|
|
93
95
|
interface CardHistory<T extends CardRecord> {
|
|
94
96
|
_id: PouchDB.Core.DocumentId;
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { A as AttachmentData, C as ChunkMetadata, D as DesignDocument, I as IndexMetadata, a as PackedCourseData, P as PackerConfig, S as StaticCourseManifest } from '../../types-
|
|
2
|
-
export { C as CouchDBToStaticPacker } from '../../index-
|
|
1
|
+
export { A as AttachmentData, C as ChunkMetadata, D as DesignDocument, I as IndexMetadata, a as PackedCourseData, P as PackerConfig, S as StaticCourseManifest } from '../../types-Bn0itutr.cjs';
|
|
2
|
+
export { C as CouchDBToStaticPacker } from '../../index-B_j6u5E4.cjs';
|
|
3
3
|
import '@vue-skuilder/common';
|
|
4
|
-
import '../../types-legacy-
|
|
4
|
+
import '../../types-legacy-DDY4N-Uq.cjs';
|
|
5
5
|
import 'moment';
|
|
@@ -1,5 +1,5 @@
|
|
|
1
|
-
export { A as AttachmentData, C as ChunkMetadata, D as DesignDocument, I as IndexMetadata, a as PackedCourseData, P as PackerConfig, S as StaticCourseManifest } from '../../types-
|
|
2
|
-
export { C as CouchDBToStaticPacker } from '../../index-
|
|
1
|
+
export { A as AttachmentData, C as ChunkMetadata, D as DesignDocument, I as IndexMetadata, a as PackedCourseData, P as PackerConfig, S as StaticCourseManifest } from '../../types-DQaXnuoc.js';
|
|
2
|
+
export { C as CouchDBToStaticPacker } from '../../index-Dj0SEgk3.js';
|
|
3
3
|
import '@vue-skuilder/common';
|
|
4
|
-
import '../../types-legacy-
|
|
4
|
+
import '../../types-legacy-DDY4N-Uq.js';
|
|
5
5
|
import 'moment';
|
|
@@ -9,7 +9,7 @@ The navigation strategy system selects and scores cards for study sessions. It u
|
|
|
9
9
|
|
|
10
10
|
### WeightedCard
|
|
11
11
|
|
|
12
|
-
A card with a suitability score and
|
|
12
|
+
A card with a suitability score, audit trail, and pre-fetched metadata:
|
|
13
13
|
|
|
14
14
|
```typescript
|
|
15
15
|
interface WeightedCard {
|
|
@@ -17,6 +17,7 @@ interface WeightedCard {
|
|
|
17
17
|
courseId: string;
|
|
18
18
|
score: number; // 0-1 suitability score
|
|
19
19
|
provenance: StrategyContribution[]; // Audit trail
|
|
20
|
+
tags?: string[]; // Pre-fetched tags (hydrated by Pipeline)
|
|
20
21
|
}
|
|
21
22
|
|
|
22
23
|
interface StrategyContribution {
|
|
@@ -57,15 +58,19 @@ interface CardFilter {
|
|
|
57
58
|
}
|
|
58
59
|
```
|
|
59
60
|
|
|
61
|
+
Filters receive cards with pre-hydrated data (e.g., `card.tags`) from Pipeline, eliminating
|
|
62
|
+
redundant database queries.
|
|
63
|
+
|
|
60
64
|
**Implementations:**
|
|
61
65
|
- `HierarchyDefinitionNavigator` — Gates cards by prerequisite mastery (score=0 if locked)
|
|
62
66
|
- `InterferenceMitigatorNavigator` — Reduces scores for confusable content
|
|
63
67
|
- `RelativePriorityNavigator` — Boosts scores for high-utility content
|
|
68
|
+
- `UserTagPreferenceFilter` — Applies user-configured tag preferences (path constraints)
|
|
64
69
|
- `createEloDistanceFilter()` — Penalizes cards far from user's current ELO
|
|
65
70
|
|
|
66
71
|
### Pipeline
|
|
67
72
|
|
|
68
|
-
Orchestrates generator and filters:
|
|
73
|
+
Orchestrates generator, data hydration, and filters:
|
|
69
74
|
|
|
70
75
|
```typescript
|
|
71
76
|
class Pipeline {
|
|
@@ -77,13 +82,20 @@ class Pipeline {
|
|
|
77
82
|
)
|
|
78
83
|
|
|
79
84
|
async getWeightedCards(limit: number): Promise<WeightedCard[]> {
|
|
85
|
+
// Build shared context (user ELO, etc.)
|
|
80
86
|
const context = await this.buildContext();
|
|
87
|
+
|
|
88
|
+
// Generate candidates
|
|
81
89
|
let cards = await this.generator.getWeightedCards(fetchLimit, context);
|
|
82
|
-
|
|
90
|
+
|
|
91
|
+
// Hydrate shared data (tags, etc.) in single batch query
|
|
92
|
+
cards = await this.hydrateTags(cards);
|
|
93
|
+
|
|
94
|
+
// Apply filters sequentially
|
|
83
95
|
for (const filter of this.filters) {
|
|
84
96
|
cards = await filter.transform(cards, context);
|
|
85
97
|
}
|
|
86
|
-
|
|
98
|
+
|
|
87
99
|
return cards.filter(c => c.score > 0)
|
|
88
100
|
.sort((a, b) => b.score - a.score)
|
|
89
101
|
.slice(0, limit);
|
|
@@ -91,6 +103,12 @@ class Pipeline {
|
|
|
91
103
|
}
|
|
92
104
|
```
|
|
93
105
|
|
|
106
|
+
**Responsibilities:**
|
|
107
|
+
- **Context building** — Fetches shared data (user ELO) once for all strategies
|
|
108
|
+
- **Data hydration** — Pre-fetches commonly needed data (tags) in batch queries
|
|
109
|
+
- **Filter orchestration** — Applies filters in sequence, accumulating provenance
|
|
110
|
+
- **Result selection** — Removes zero-scores, sorts, and returns top N
|
|
111
|
+
|
|
94
112
|
## Pipeline Assembly
|
|
95
113
|
|
|
96
114
|
`PipelineAssembler` builds pipelines from strategy documents:
|
|
@@ -192,10 +210,6 @@ class MyGenerator extends ContentNavigator implements CardGenerator {
|
|
|
192
210
|
}]
|
|
193
211
|
}));
|
|
194
212
|
}
|
|
195
|
-
|
|
196
|
-
// Legacy methods - stub or implement for backward compat
|
|
197
|
-
async getNewCards() { return []; }
|
|
198
|
-
async getPendingReviews() { return []; }
|
|
199
213
|
}
|
|
200
214
|
```
|
|
201
215
|
|
|
@@ -228,15 +242,96 @@ class MyFilter extends ContentNavigator implements CardFilter {
|
|
|
228
242
|
});
|
|
229
243
|
}
|
|
230
244
|
|
|
231
|
-
// Legacy
|
|
245
|
+
// Legacy method - filters don't generate cards
|
|
232
246
|
async getWeightedCards() { throw new Error('Use transform() via Pipeline'); }
|
|
233
|
-
async getNewCards() { return []; }
|
|
234
|
-
async getPendingReviews() { return []; }
|
|
235
247
|
}
|
|
236
248
|
```
|
|
237
249
|
|
|
238
250
|
Register in `NavigatorRoles` as `NavigatorRole.FILTER`.
|
|
239
251
|
|
|
252
|
+
## Strategy State Storage
|
|
253
|
+
|
|
254
|
+
Strategies can persist user-scoped state (preferences, learned patterns, temporal tracking)
|
|
255
|
+
using the `STRATEGY_STATE` document type in the user database.
|
|
256
|
+
|
|
257
|
+
### Goals vs Preferences vs Inferred
|
|
258
|
+
|
|
259
|
+
The system distinguishes three types of user-scoped navigation data:
|
|
260
|
+
|
|
261
|
+
| Type | Defines | Example | Affects ELO | Implementation |
|
|
262
|
+
|------|---------|---------|-------------|----------------|
|
|
263
|
+
| **Goal** | Destination (what to learn) | "Master ear-training" | Yes | `userGoal.ts` (stub) |
|
|
264
|
+
| **Preference** | Path (how to learn) | "Skip text-heavy cards" | No | `filters/userTagPreference.ts` |
|
|
265
|
+
| **Inferred** | Learned patterns | "User prefers visual" | No | `inferredPreference.ts` (stub) |
|
|
266
|
+
|
|
267
|
+
- **Goals** redefine the optimization target — they scope which content matters for progress
|
|
268
|
+
- **Preferences** constrain the path — they affect card selection without changing progress tracking
|
|
269
|
+
- **Inferred** preferences are learned from behavior — they act as soft suggestions
|
|
270
|
+
|
|
271
|
+
See stub files for detailed architectural intent on goals and inferred preferences.
|
|
272
|
+
|
|
273
|
+
### Storage API
|
|
274
|
+
|
|
275
|
+
`ContentNavigator` provides protected helper methods:
|
|
276
|
+
|
|
277
|
+
```typescript
|
|
278
|
+
// Get this strategy's persisted state for the current course
|
|
279
|
+
protected async getStrategyState<T>(): Promise<T | null>
|
|
280
|
+
|
|
281
|
+
// Persist this strategy's state for the current course
|
|
282
|
+
protected async putStrategyState<T>(data: T): Promise<void>
|
|
283
|
+
|
|
284
|
+
// Override to customize the storage key (default: constructor name)
|
|
285
|
+
protected get strategyKey(): string
|
|
286
|
+
```
|
|
287
|
+
|
|
288
|
+
### Document Format
|
|
289
|
+
|
|
290
|
+
```typescript
|
|
291
|
+
interface StrategyStateDoc<T> {
|
|
292
|
+
_id: StrategyStateId; // "STRATEGY_STATE::{courseId}::{strategyKey}"
|
|
293
|
+
docType: DocType.STRATEGY_STATE;
|
|
294
|
+
courseId: string;
|
|
295
|
+
strategyKey: string;
|
|
296
|
+
data: T; // Strategy-specific payload
|
|
297
|
+
updatedAt: string; // ISO timestamp
|
|
298
|
+
}
|
|
299
|
+
```
|
|
300
|
+
|
|
301
|
+
### Example: User Tag Preferences
|
|
302
|
+
|
|
303
|
+
`UserTagPreferenceFilter` reads user preferences from strategy state:
|
|
304
|
+
|
|
305
|
+
```typescript
|
|
306
|
+
interface UserTagPreferenceState {
|
|
307
|
+
/**
|
|
308
|
+
* Tag-specific multipliers.
|
|
309
|
+
* - 0 = exclude (card score = 0)
|
|
310
|
+
* - 0.5 = penalize by 50%
|
|
311
|
+
* - 1.0 = neutral/no effect
|
|
312
|
+
* - 2.0 = 2x preference boost
|
|
313
|
+
* - Higher = stronger preference
|
|
314
|
+
*/
|
|
315
|
+
boost: Record<string, number>;
|
|
316
|
+
updatedAt: string;
|
|
317
|
+
}
|
|
318
|
+
|
|
319
|
+
// In filter's transform():
|
|
320
|
+
const prefs = await this.getStrategyState<UserTagPreferenceState>();
|
|
321
|
+
if (!prefs || Object.keys(prefs.boost).length === 0) {
|
|
322
|
+
return cards; // No preferences configured
|
|
323
|
+
}
|
|
324
|
+
|
|
325
|
+
// Apply multipliers (max wins when multiple tags match)
|
|
326
|
+
const multiplier = computeMultiplier(cardTags, prefs.boost);
|
|
327
|
+
return { ...card, score: card.score * multiplier };
|
|
328
|
+
```
|
|
329
|
+
|
|
330
|
+
**UI Component**: `packages/common-ui/src/components/UserTagPreferences.vue`
|
|
331
|
+
- Slider-based interface (0-2 default range, expandable to 10)
|
|
332
|
+
- All sliders share global max for consistent visual comparison
|
|
333
|
+
- Writes to strategy state via `userDB.putStrategyState()`
|
|
334
|
+
|
|
240
335
|
## File Reference
|
|
241
336
|
|
|
242
337
|
| File | Purpose |
|
|
@@ -254,12 +349,15 @@ Register in `NavigatorRoles` as `NavigatorRole.FILTER`.
|
|
|
254
349
|
| `core/navigators/interferenceMitigator.ts` | Interference filter |
|
|
255
350
|
| `core/navigators/relativePriority.ts` | Priority filter |
|
|
256
351
|
| `core/navigators/filters/eloDistance.ts` | ELO distance filter |
|
|
352
|
+
| `core/navigators/filters/userTagPreference.ts` | User tag preference filter |
|
|
353
|
+
| `common-ui/.../UserTagPreferences.vue` | UI for tag preference sliders |
|
|
354
|
+
| `core/navigators/userGoal.ts` | User goal navigator (stub) |
|
|
355
|
+
| `core/navigators/inferredPreference.ts` | Inferred preference navigator (stub) |
|
|
356
|
+
| `core/types/strategyState.ts` | `StrategyStateDoc`, `StrategyStateId` |
|
|
257
357
|
| `impl/couch/courseDB.ts` | `createNavigator()` entry point |
|
|
258
358
|
|
|
259
|
-
## Related
|
|
359
|
+
## Related Documentation
|
|
260
360
|
|
|
261
|
-
- `todo-
|
|
262
|
-
- `todo-
|
|
263
|
-
-
|
|
264
|
-
- todo-strategy-state-storage
|
|
265
|
-
- todo-evolutionary-orchestration
|
|
361
|
+
- `todo-strategy-authoring.md` — UX and DX for authoring strategies
|
|
362
|
+
- `todo-evolutionary-orchestration.md` — Long-term adaptive strategy vision
|
|
363
|
+
- `devlog/1004` — Implementation details for tag hydration optimization
|
package/package.json
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
"publishConfig": {
|
|
5
5
|
"access": "public"
|
|
6
6
|
},
|
|
7
|
-
"version": "0.1.
|
|
7
|
+
"version": "0.1.21",
|
|
8
8
|
"description": "Database layer for vue-skuilder",
|
|
9
9
|
"main": "dist/index.js",
|
|
10
10
|
"module": "dist/index.mjs",
|
|
@@ -48,7 +48,7 @@
|
|
|
48
48
|
},
|
|
49
49
|
"dependencies": {
|
|
50
50
|
"@nilock2/pouchdb-authentication": "^1.0.2",
|
|
51
|
-
"@vue-skuilder/common": "0.1.
|
|
51
|
+
"@vue-skuilder/common": "0.1.21",
|
|
52
52
|
"cross-fetch": "^4.1.0",
|
|
53
53
|
"moment": "^2.29.4",
|
|
54
54
|
"pouchdb": "^9.0.0",
|
|
@@ -60,7 +60,7 @@
|
|
|
60
60
|
"tsup": "^8.0.2",
|
|
61
61
|
"typescript": "~5.9.3",
|
|
62
62
|
"vite": "^7.0.0",
|
|
63
|
-
"vitest": "^4.0.
|
|
63
|
+
"vitest": "^4.0.15"
|
|
64
64
|
},
|
|
65
|
-
"stableVersion": "0.1.
|
|
65
|
+
"stableVersion": "0.1.21"
|
|
66
66
|
}
|
package/src/core/index.ts
CHANGED
|
@@ -1,6 +1,4 @@
|
|
|
1
1
|
import { ClassroomConfig } from '@vue-skuilder/common';
|
|
2
|
-
import { ScheduledCard } from '../types/user';
|
|
3
|
-
import { StudySessionNewItem, StudySessionReviewItem } from './contentSource';
|
|
4
2
|
|
|
5
3
|
/**
|
|
6
4
|
* Classroom management
|
|
@@ -29,17 +27,11 @@ export interface TeacherClassroomDBInterface extends ClassroomDBInterface {
|
|
|
29
27
|
removeContent?(content: AssignedContent): Promise<void>;
|
|
30
28
|
}
|
|
31
29
|
|
|
32
|
-
|
|
33
|
-
|
|
34
|
-
|
|
35
|
-
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* For student interfaces: get new cards
|
|
40
|
-
*/
|
|
41
|
-
getNewCards?(limit?: number): Promise<StudySessionNewItem[]>;
|
|
42
|
-
}
|
|
30
|
+
/**
|
|
31
|
+
* Student-facing classroom interface.
|
|
32
|
+
* Content is accessed via StudyContentSource.getWeightedCards().
|
|
33
|
+
*/
|
|
34
|
+
export type StudentClassroomDBInterface = ClassroomDBInterface;
|
|
43
35
|
|
|
44
36
|
export type AssignedContent = AssignedCourse | AssignedTag | AssignedCard;
|
|
45
37
|
|
|
@@ -1,43 +1,10 @@
|
|
|
1
1
|
import { getDataLayer } from '@db/factory';
|
|
2
2
|
import { UserDBInterface } from '..';
|
|
3
3
|
import { StudentClassroomDB } from '../../impl/couch/classroomDB';
|
|
4
|
-
import { ScheduledCard } from '@db/core/types/user';
|
|
5
4
|
import { WeightedCard } from '../navigators';
|
|
6
5
|
import { TagFilter, hasActiveFilter } from '@vue-skuilder/common';
|
|
7
6
|
import { TagFilteredContentSource } from '../../study/TagFilteredContentSource';
|
|
8
7
|
|
|
9
|
-
// ============================================================================
|
|
10
|
-
// API MIGRATION NOTICE
|
|
11
|
-
// ============================================================================
|
|
12
|
-
//
|
|
13
|
-
// The StudyContentSource interface is being superseded by the ContentNavigator
|
|
14
|
-
// class and its getWeightedCards() API. See:
|
|
15
|
-
// packages/db/src/core/navigators/ARCHITECTURE.md
|
|
16
|
-
//
|
|
17
|
-
// HISTORICAL CONTEXT:
|
|
18
|
-
// - This interface was designed to abstract 'classrooms' and 'courses' as
|
|
19
|
-
// content sources for study sessions.
|
|
20
|
-
// - getNewCards() and getPendingReviews() were artifacts of two hard-coded
|
|
21
|
-
// navigation strategies: ELO proximity (new) and SRS scheduling (reviews).
|
|
22
|
-
// - The new/review split reflected implementation details, not fundamentals.
|
|
23
|
-
//
|
|
24
|
-
// THE PROBLEM:
|
|
25
|
-
// - "What does 'get reviews' mean for an interference mitigator?" - it doesn't.
|
|
26
|
-
// - SRS is just one strategy that could express review urgency as scores.
|
|
27
|
-
// - Some strategies generate candidates, others filter/score them.
|
|
28
|
-
//
|
|
29
|
-
// THE SOLUTION:
|
|
30
|
-
// - ContentNavigator.getWeightedCards() returns unified scored candidates.
|
|
31
|
-
// - WeightedCard.source field distinguishes new/review/failed (metadata, not API).
|
|
32
|
-
// - Strategies compose via delegate pattern (filter wraps generator).
|
|
33
|
-
//
|
|
34
|
-
// MIGRATION PATH:
|
|
35
|
-
// 1. ContentNavigator implements StudyContentSource for backward compat
|
|
36
|
-
// 2. SessionController will migrate to call getWeightedCards()
|
|
37
|
-
// 3. Legacy methods will be deprecated, then removed
|
|
38
|
-
//
|
|
39
|
-
// ============================================================================
|
|
40
|
-
|
|
41
8
|
export type StudySessionFailedItem = StudySessionFailedNewItem | StudySessionFailedReviewItem;
|
|
42
9
|
|
|
43
10
|
export interface StudySessionFailedNewItem extends StudySessionItem {
|
|
@@ -89,49 +56,22 @@ export interface ContentSourceID {
|
|
|
89
56
|
/**
|
|
90
57
|
* Interface for sources that provide study content to SessionController.
|
|
91
58
|
*
|
|
92
|
-
*
|
|
93
|
-
*
|
|
94
|
-
* strategies. The new API returns unified WeightedCard[] with scores.
|
|
95
|
-
*
|
|
96
|
-
* MIGRATION:
|
|
97
|
-
* - Implement ContentNavigator instead of StudyContentSource directly
|
|
98
|
-
* - Override getWeightedCards() as the primary method
|
|
99
|
-
* - Legacy methods can delegate to getWeightedCards() or be left as-is
|
|
59
|
+
* Content sources return scored candidates via getWeightedCards(), which
|
|
60
|
+
* SessionController uses to populate study queues.
|
|
100
61
|
*
|
|
101
|
-
* See: packages/db/
|
|
62
|
+
* See: packages/db/docs/navigators-architecture.md
|
|
102
63
|
*/
|
|
103
64
|
export interface StudyContentSource {
|
|
104
|
-
/**
|
|
105
|
-
* Get cards scheduled for review based on SRS algorithm.
|
|
106
|
-
*
|
|
107
|
-
* @deprecated Will be replaced by getWeightedCards() which returns scored candidates.
|
|
108
|
-
* Review urgency will be expressed as a score rather than a separate method.
|
|
109
|
-
*/
|
|
110
|
-
getPendingReviews(): Promise<(StudySessionReviewItem & ScheduledCard)[]>;
|
|
111
|
-
|
|
112
|
-
/**
|
|
113
|
-
* Get new cards for introduction, typically ordered by ELO proximity.
|
|
114
|
-
*
|
|
115
|
-
* @deprecated Will be replaced by getWeightedCards() which returns scored candidates.
|
|
116
|
-
* New card selection and scoring will be unified with review scoring.
|
|
117
|
-
*
|
|
118
|
-
* @param n - Maximum number of new cards to return
|
|
119
|
-
*/
|
|
120
|
-
getNewCards(n?: number): Promise<StudySessionNewItem[]>;
|
|
121
|
-
|
|
122
65
|
/**
|
|
123
66
|
* Get cards with suitability scores for presentation.
|
|
124
67
|
*
|
|
125
|
-
*
|
|
126
|
-
*
|
|
127
|
-
*
|
|
128
|
-
* The `source` field on WeightedCard indicates origin ('new' | 'review' | 'failed')
|
|
129
|
-
* for queue routing purposes during the migration period.
|
|
68
|
+
* Returns unified scored candidates that can be sorted and selected by SessionController.
|
|
69
|
+
* The card origin ('new' | 'review' | 'failed') is determined by provenance metadata.
|
|
130
70
|
*
|
|
131
71
|
* @param limit - Maximum number of cards to return
|
|
132
72
|
* @returns Cards sorted by score descending
|
|
133
73
|
*/
|
|
134
|
-
getWeightedCards
|
|
74
|
+
getWeightedCards(limit: number): Promise<WeightedCard[]>;
|
|
135
75
|
}
|
|
136
76
|
// #endregion docs_StudyContentSource
|
|
137
77
|
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import { CourseConfig, CourseElo, DataShape, SkuilderCourseData } from '@vue-skuilder/common';
|
|
2
|
-
import {
|
|
2
|
+
import { StudyContentSource, StudySessionItem } from './contentSource';
|
|
3
3
|
import { TagStub, Tag, QualifiedCardID } from '../types/types-legacy';
|
|
4
4
|
import { DataLayerResult } from '../types/db';
|
|
5
5
|
import { NavigationStrategyManager } from './navigationStrategyManager';
|
|
@@ -26,7 +26,7 @@ export interface CourseInfo {
|
|
|
26
26
|
registeredUsers: number;
|
|
27
27
|
}
|
|
28
28
|
|
|
29
|
-
export interface CourseDBInterface extends NavigationStrategyManager {
|
|
29
|
+
export interface CourseDBInterface extends NavigationStrategyManager, StudyContentSource {
|
|
30
30
|
/**
|
|
31
31
|
* Get course config
|
|
32
32
|
*/
|
|
@@ -74,11 +74,6 @@ export interface CourseDBInterface extends NavigationStrategyManager {
|
|
|
74
74
|
*/
|
|
75
75
|
updateCardElo(cardId: string, elo: CourseElo): Promise<PouchDB.Core.Response>;
|
|
76
76
|
|
|
77
|
-
/**
|
|
78
|
-
* Get new cards for study
|
|
79
|
-
*/
|
|
80
|
-
getNewCards(limit?: number): Promise<StudySessionNewItem[]>;
|
|
81
|
-
|
|
82
77
|
/**
|
|
83
78
|
* Get cards centered at a particular ELO rating
|
|
84
79
|
*/
|
|
@@ -92,6 +87,19 @@ export interface CourseDBInterface extends NavigationStrategyManager {
|
|
|
92
87
|
*/
|
|
93
88
|
getAppliedTags(cardId: string): Promise<PouchDB.Query.Response<TagStub>>;
|
|
94
89
|
|
|
90
|
+
/**
|
|
91
|
+
* Get tags for multiple cards in a single batch query.
|
|
92
|
+
* More efficient than calling getAppliedTags() for each card.
|
|
93
|
+
*
|
|
94
|
+
* This method reduces redundant database operations when multiple filters
|
|
95
|
+
* need tag data for the same cards. The Pipeline uses this to pre-hydrate
|
|
96
|
+
* tags on WeightedCard objects before filters run.
|
|
97
|
+
*
|
|
98
|
+
* @param cardIds - Array of card IDs to fetch tags for
|
|
99
|
+
* @returns Map from cardId to array of tag names
|
|
100
|
+
*/
|
|
101
|
+
getAppliedTagsBatch(cardIds: string[]): Promise<Map<string, string[]>>;
|
|
102
|
+
|
|
95
103
|
/**
|
|
96
104
|
* Add a tag to a card
|
|
97
105
|
*/
|
|
@@ -56,6 +56,18 @@ export interface UserDBReader {
|
|
|
56
56
|
|
|
57
57
|
getActivityRecords(): Promise<ActivityRecord[]>;
|
|
58
58
|
|
|
59
|
+
/**
|
|
60
|
+
* Get strategy-specific state for a course.
|
|
61
|
+
*
|
|
62
|
+
* Strategies use this to persist preferences, learned patterns, or temporal
|
|
63
|
+
* tracking data across sessions. Each strategy owns its own namespace.
|
|
64
|
+
*
|
|
65
|
+
* @param courseId - The course this state applies to
|
|
66
|
+
* @param strategyKey - Unique key identifying the strategy (typically class name)
|
|
67
|
+
* @returns The strategy's data payload, or null if no state exists
|
|
68
|
+
*/
|
|
69
|
+
getStrategyState<T>(courseId: string, strategyKey: string): Promise<T | null>;
|
|
70
|
+
|
|
59
71
|
/**
|
|
60
72
|
* Get user's classroom registrations
|
|
61
73
|
*/
|
|
@@ -132,6 +144,26 @@ export interface UserDBWriter extends DocumentUpdater {
|
|
|
132
144
|
* Reset all user data (progress, registrations, etc.) while preserving authentication
|
|
133
145
|
*/
|
|
134
146
|
resetUserData(): Promise<{ status: Status; error?: string }>;
|
|
147
|
+
|
|
148
|
+
/**
|
|
149
|
+
* Store strategy-specific state for a course.
|
|
150
|
+
*
|
|
151
|
+
* Strategies use this to persist preferences, learned patterns, or temporal
|
|
152
|
+
* tracking data across sessions. Each strategy owns its own namespace.
|
|
153
|
+
*
|
|
154
|
+
* @param courseId - The course this state applies to
|
|
155
|
+
* @param strategyKey - Unique key identifying the strategy (typically class name)
|
|
156
|
+
* @param data - The strategy's data payload to store
|
|
157
|
+
*/
|
|
158
|
+
putStrategyState<T>(courseId: string, strategyKey: string, data: T): Promise<void>;
|
|
159
|
+
|
|
160
|
+
/**
|
|
161
|
+
* Delete strategy-specific state for a course.
|
|
162
|
+
*
|
|
163
|
+
* @param courseId - The course this state applies to
|
|
164
|
+
* @param strategyKey - Unique key identifying the strategy (typically class name)
|
|
165
|
+
*/
|
|
166
|
+
deleteStrategyState(courseId: string, strategyKey: string): Promise<void>;
|
|
135
167
|
}
|
|
136
168
|
|
|
137
169
|
/**
|