drill-srs 0.1.0 → 0.1.1

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/README.md CHANGED
@@ -14,15 +14,58 @@ Drill helps you memorize anything using [spaced repetition](https://en.wikipedia
14
14
  - **Deck organization** - Organize cards into decks using directories
15
15
  - **Statistics** - Track learning progress, review forecasts, retention rates
16
16
 
17
+ ## Screenshots
18
+
19
+ <details>
20
+ <summary>Main Menu</summary>
21
+
22
+ ![Main Menu](./docs/home.png)
23
+ </details>
24
+
25
+ <details>
26
+ <summary>Study Mode - Question</summary>
27
+
28
+ ![Study Mode - Question](./docs/question.png)
29
+ </details>
30
+
31
+ <details>
32
+ <summary>Browse Decks</summary>
33
+
34
+ ![Browse Decks](./docs/decks.png)
35
+ </details>
36
+
37
+ <details>
38
+ <summary>Statistics Summary</summary>
39
+
40
+ ![Statistics Summary](./docs/summary.png)
41
+ </details>
42
+
43
+ <details>
44
+ <summary>Review Forecast</summary>
45
+
46
+ ![Review Forecast](./docs/forecast.png)
47
+ </details>
48
+
17
49
  ## Installation
18
50
 
51
+ ### Install globally via npm
52
+
53
+ ```bash
54
+ npm install -g drill-srs
55
+ drill
56
+ ```
57
+
58
+ ### Or clone and run locally
59
+
19
60
  ```bash
61
+ git clone https://github.com/yourusername/drill.git
62
+ cd drill
20
63
  npm install
21
64
  npm run build
22
65
  npm start
23
66
  ```
24
67
 
25
- Or for development:
68
+ For development:
26
69
  ```bash
27
70
  npm run start
28
71
  ```
package/docs/decks.png ADDED
Binary file
Binary file
package/docs/home.png ADDED
Binary file
Binary file
Binary file
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "drill-srs",
3
- "version": "0.1.0",
3
+ "version": "0.1.1",
4
4
  "type": "module",
5
5
  "main": "dist/index.js",
6
6
  "bin": {
@@ -14,13 +14,31 @@
14
14
  "test:watch": "vitest",
15
15
  "prepublishOnly": "npm run build"
16
16
  },
17
- "keywords": ["flashcards", "spaced-repetition", "srs", "cli", "terminal", "learning", "memorization", "markdown"],
17
+ "keywords": [
18
+ "flashcards",
19
+ "spaced-repetition",
20
+ "srs",
21
+ "cli",
22
+ "terminal",
23
+ "learning",
24
+ "memorization",
25
+ "markdown"
26
+ ],
18
27
  "author": "",
19
28
  "license": "ISC",
20
29
  "description": "Lightweight file-based spaced repetition system using Markdown flashcards",
30
+ "repository": {
31
+ "type": "git",
32
+ "url": "git+https://github.com/jim-at-jibba/drill.git"
33
+ },
34
+ "bugs": {
35
+ "url": "https://github.com/jim-at-jibba/drill/issues"
36
+ },
37
+ "homepage": "https://github.com/jim-at-jibba/drill#readme",
21
38
  "dependencies": {
22
39
  "cli-highlight": "^2.1.11",
23
40
  "date-fns": "^4.1.0",
41
+ "drill-srs": "^0.1.0",
24
42
  "glob": "^13.0.0",
25
43
  "gray-matter": "^4.0.3",
26
44
  "ink": "^5.2.1",
Binary file
@@ -1,578 +0,0 @@
1
- Perfect! Now I have a solid understanding of the SM-2 algorithm. Let me create a comprehensive analysis document for you:
2
-
3
- # drill Deep Analysis for JavaScript/Ink Implementation
4
-
5
- Based on my analysis of the drill repository and spaced repetition algorithms, here's a comprehensive breakdown:
6
-
7
- ## 1. Core Data Structures
8
-
9
- ### Card Model
10
- ```javascript
11
- // Card data structure (from card.go analysis and markdown format)
12
- const Card = {
13
- // Identity
14
- id: String, // Unique identifier (typically filename or UUID)
15
- title: String, // Card title (from # heading)
16
- filePath: String, // Path to markdown file
17
- deckName: String, // Parent deck name (directory name)
18
-
19
- // Content
20
- question: String, // Content under ## Question section
21
- answer: String, // Content under ## Answer section
22
-
23
- // Metadata (YAML frontmatter)
24
- tags: Array<String>, // Categories/topics
25
- created: Date, // Creation timestamp (YYYY-MM-DD)
26
- lastReviewed: Date, // Last review timestamp
27
- nextReview: Date, // Calculated next review date
28
-
29
- // SM-2 Algorithm Fields
30
- reviewInterval: Number, // Days until next review (default: 0)
31
- easeFactor: Number, // Difficulty multiplier (default: 2.5, min: 1.3)
32
- repetitionCount: Number, // Successful reviews in a row (default: 0)
33
- difficulty: Number // User perception 0-5 (optional, for stats)
34
- }
35
- ```
36
-
37
- ### Deck Model
38
- ```javascript
39
- const Deck = {
40
- name: String, // Directory name
41
- path: String, // Full directory path
42
- cards: Array<Card>, // All cards in deck
43
- totalCards: Number, // Card count
44
- dueCards: Number, // Cards due today
45
- newCards: Number, // Never reviewed cards
46
- learningCards: Number // Cards in learning phase
47
- }
48
- ```
49
-
50
- ## 2. File Format Structure
51
-
52
- ### Markdown File Template
53
- ```markdown
54
- ---
55
- tags: [tag1, tag2, tag3]
56
- created: 2025-04-02
57
- last_reviewed: 2025-11-20
58
- review_interval: 7
59
- easeFactor: 2.6
60
- repetitionCount: 3
61
- difficulty: 4
62
- ---
63
-
64
- # Card Title
65
-
66
- ## Question
67
-
68
- Your question content here.
69
- Can be multiline with **markdown** formatting.
70
-
71
- ## Answer
72
-
73
- Your answer here with:
74
- - Lists
75
- - Code blocks
76
- - Tables
77
- - Any markdown content
78
- ```
79
-
80
- ### Parsing Strategy
81
- 1. **Split frontmatter from content** (between `---` markers)
82
- 2. **Parse YAML** to extract metadata fields
83
- 3. **Extract sections** by finding `## Question` and `## Answer` headers
84
- 4. **Parse title** from first `#` heading
85
- 5. **Preserve markdown** for rich rendering
86
-
87
- ## 3. SM-2 Spaced Repetition Algorithm
88
-
89
- ### Core Algorithm Implementation
90
-
91
- ```javascript
92
- /**
93
- * SM-2 Algorithm for calculating next review
94
- * @param {Card} card - Current card state
95
- * @param {Number} quality - User rating 0-5
96
- * @returns {Object} Updated { interval, repetitions, easeFactor, nextReview }
97
- */
98
- function calculateSM2(card, quality) {
99
- // Validate quality (0-5 scale)
100
- if (quality < 0 || quality > 5) {
101
- throw new Error('Quality must be between 0 and 5');
102
- }
103
-
104
- // Get current values (with defaults for new cards)
105
- let repetitions = card.repetitionCount || 0;
106
- let easeFactor = card.easeFactor || 2.5;
107
- let interval = card.reviewInterval || 0;
108
-
109
- // Step 1: Update ease factor based on quality
110
- // Formula: EF' = EF + (0.1 - (5-q) * (0.08 + (5-q) * 0.02))
111
- easeFactor = easeFactor + (0.1 - (5 - quality) * (0.08 + (5 - quality) * 0.02));
112
-
113
- // Ensure ease factor never goes below 1.3
114
- easeFactor = Math.max(1.3, easeFactor);
115
-
116
- // Step 2: Update repetition count
117
- if (quality < 3) {
118
- // Failed recall - reset to beginning
119
- repetitions = 0;
120
- interval = 0;
121
- } else {
122
- // Successful recall - increment
123
- repetitions += 1;
124
- }
125
-
126
- // Step 3: Calculate new interval based on repetition count
127
- if (repetitions === 0) {
128
- interval = 0; // Review immediately
129
- } else if (repetitions === 1) {
130
- interval = 1; // Review tomorrow
131
- } else if (repetitions === 2) {
132
- interval = 6; // Review in 6 days
133
- } else {
134
- // For subsequent reviews, multiply previous interval by ease factor
135
- interval = Math.round(interval * easeFactor);
136
- }
137
-
138
- // Step 4: Calculate next review date
139
- const now = new Date();
140
- const nextReview = new Date(now.getTime() + interval * 24 * 60 * 60 * 1000);
141
-
142
- return {
143
- interval,
144
- repetitions,
145
- easeFactor,
146
- nextReview
147
- };
148
- }
149
- ```
150
-
151
- ### Quality Rating Scale (1-5 in drill, 0-5 in SM-2)
152
-
153
- ```javascript
154
- const QUALITY_RATINGS = {
155
- 1: 'Blackout', // Complete failure (SM-2: 0)
156
- 2: 'Wrong', // Incorrect but recognized (SM-2: 1-2)
157
- 3: 'Hard', // Correct with difficulty (SM-2: 3)
158
- 4: 'Good', // Correct with some effort (SM-2: 4)
159
- 5: 'Easy' // Perfect recall (SM-2: 5)
160
- };
161
-
162
- // drill uses 1-5, but you'll need to map to 0-5 internally
163
- // or adjust the algorithm formulas accordingly
164
- ```
165
-
166
- ### Algorithm Behavior Patterns
167
-
168
- 1. **New cards** (repetitions = 0):
169
- - First review: 1 day interval
170
- - Second review: 6 days interval
171
- - Subsequent: exponential growth based on ease factor
172
-
173
- 2. **Failed cards** (quality < 3):
174
- - Reset repetitions to 0
175
- - Start learning cycle over
176
- - Ease factor decreases (makes future intervals shorter)
177
-
178
- 3. **Successful cards** (quality ≥ 3):
179
- - Increment repetitions
180
- - Interval grows exponentially
181
- - Ease factor adjusts based on difficulty
182
-
183
- 4. **Ease factor dynamics**:
184
- - Quality 5 (Easy): EF increases by ~0.1
185
- - Quality 4 (Good): EF stays roughly same
186
- - Quality 3 (Hard): EF decreases by ~0.14
187
- - Quality 1-2: EF decreases significantly
188
-
189
- ## 4. Data Store Architecture
190
-
191
- ### File System Structure
192
- ```
193
- ~/drill/
194
- ├── programming/ # Deck (directory)
195
- │ ├── algorithms.md # Card
196
- │ ├── data-structures.md
197
- │ └── patterns.md
198
- ├── languages/ # Another deck
199
- │ ├── spanish-basics.md
200
- │ └── french-verbs.md
201
- └── history/
202
- └── world-war-2.md
203
- ```
204
-
205
- ### Store Operations
206
-
207
- ```javascript
208
- class CardStore {
209
- constructor(baseDir) {
210
- this.baseDir = baseDir; // e.g., ~/drill
211
- this.decks = new Map();
212
- }
213
-
214
- // Load all decks from filesystem
215
- async loadDecks() {
216
- // 1. Read directories in baseDir
217
- // 2. For each directory, create Deck object
218
- // 3. Read all .md files in directory
219
- // 4. Parse each markdown file into Card
220
- // 5. Store in this.decks Map
221
- }
222
-
223
- // Get cards due for review today
224
- getDueCards(deckName = null) {
225
- const today = new Date();
226
- today.setHours(0, 0, 0, 0); // Start of day
227
-
228
- // Filter cards where nextReview <= today
229
- // If deckName specified, filter by deck
230
- // Sort by nextReview (oldest first)
231
- }
232
-
233
- // Get new cards (never reviewed)
234
- getNewCards(deckName = null) {
235
- // Filter cards where repetitionCount === 0
236
- // and lastReviewed is null/undefined
237
- }
238
-
239
- // Save card after review
240
- async saveCard(card) {
241
- // 1. Construct markdown with updated frontmatter
242
- // 2. Write to card.filePath
243
- // 3. Update in-memory deck
244
- }
245
-
246
- // Get statistics
247
- getStats(deckName = null) {
248
- // Calculate:
249
- // - Total cards
250
- // - Due today
251
- // - New cards
252
- // - Learning cards (repetitionCount < 3)
253
- // - Mature cards (repetitionCount >= 3)
254
- // - Retention rate
255
- }
256
- }
257
- ```
258
-
259
- ### Markdown Parser
260
-
261
- ```javascript
262
- function parseMarkdownCard(filePath, content) {
263
- // 1. Split by frontmatter delimiters (---)
264
- const parts = content.split(/^---$/m);
265
-
266
- // 2. Parse YAML frontmatter
267
- const frontmatter = yaml.parse(parts[1]);
268
-
269
- // 3. Extract content sections
270
- const bodyContent = parts[2];
271
-
272
- // 4. Find title (first # heading)
273
- const titleMatch = bodyContent.match(/^#\s+(.+)$/m);
274
- const title = titleMatch ? titleMatch[1] : 'Untitled';
275
-
276
- // 5. Extract Question section
277
- const questionMatch = bodyContent.match(/##\s+Question\s*\n([\s\S]*?)(?=##|$)/);
278
- const question = questionMatch ? questionMatch[1].trim() : '';
279
-
280
- // 6. Extract Answer section
281
- const answerMatch = bodyContent.match(/##\s+Answer\s*\n([\s\S]*?)$/);
282
- const answer = answerMatch ? answerMatch[1].trim() : '';
283
-
284
- return {
285
- id: path.basename(filePath, '.md'),
286
- filePath,
287
- title,
288
- question,
289
- answer,
290
- tags: frontmatter.tags || [],
291
- created: new Date(frontmatter.created),
292
- lastReviewed: frontmatter.last_reviewed ? new Date(frontmatter.last_reviewed) : null,
293
- reviewInterval: frontmatter.review_interval || 0,
294
- easeFactor: frontmatter.easeFactor || 2.5,
295
- repetitionCount: frontmatter.repetitionCount || 0,
296
- nextReview: calculateNextReview(frontmatter)
297
- };
298
- }
299
- ```
300
-
301
- ### Markdown Writer
302
-
303
- ```javascript
304
- function serializeCard(card) {
305
- // 1. Build YAML frontmatter
306
- const frontmatter = {
307
- tags: card.tags,
308
- created: formatDate(card.created),
309
- last_reviewed: card.lastReviewed ? formatDate(card.lastReviewed) : null,
310
- review_interval: card.reviewInterval,
311
- easeFactor: card.easeFactor,
312
- repetitionCount: card.repetitionCount
313
- };
314
-
315
- const yaml = stringifyYAML(frontmatter);
316
-
317
- // 2. Build markdown body
318
- const markdown = `---
319
- ${yaml}---
320
-
321
- # ${card.title}
322
-
323
- ## Question
324
-
325
- ${card.question}
326
-
327
- ## Answer
328
-
329
- ${card.answer}
330
- `;
331
-
332
- return markdown;
333
- }
334
- ```
335
-
336
- ## 5. UI Architecture for Ink
337
-
338
- ### Screen Flow
339
- ```
340
- Main Menu
341
- ├─> Study Session (due cards)
342
- ├─> Browse Decks
343
- │ └─> Deck Detail
344
- │ └─> Study Deck
345
- └─> Statistics
346
- ├─> Summary View
347
- ├─> Deck Review
348
- └─> Review Forecast
349
- ```
350
-
351
- ### Key Ink Components
352
-
353
- ```javascript
354
- // Main App Component
355
- function App() {
356
- const [screen, setScreen] = useState('main-menu');
357
- const [store, setStore] = useState(null);
358
-
359
- useEffect(() => {
360
- // Load decks on mount
361
- const cardStore = new CardStore(config.baseDir);
362
- await cardStore.loadDecks();
363
- setStore(cardStore);
364
- }, []);
365
-
366
- // Route to different screens
367
- switch (screen) {
368
- case 'main-menu':
369
- return <MainMenu />;
370
- case 'study':
371
- return <StudyScreen />;
372
- case 'browse':
373
- return <BrowseDecks />;
374
- case 'stats':
375
- return <StatsScreen />;
376
- }
377
- }
378
-
379
- // Study Screen Component
380
- function StudyScreen({ deck, store }) {
381
- const [currentCard, setCurrentCard] = useState(null);
382
- const [showAnswer, setShowAnswer] = useState(false);
383
- const [dueCards, setDueCards] = useState([]);
384
-
385
- useInput((input, key) => {
386
- if (input === ' ' && !showAnswer) {
387
- setShowAnswer(true);
388
- }
389
-
390
- if (showAnswer && input >= '1' && input <= '5') {
391
- handleRating(parseInt(input));
392
- }
393
-
394
- if (input === 'q') {
395
- exit();
396
- }
397
- });
398
-
399
- const handleRating = async (quality) => {
400
- // Calculate SM-2 update
401
- const updated = calculateSM2(currentCard, quality);
402
-
403
- // Update card
404
- currentCard.reviewInterval = updated.interval;
405
- currentCard.repetitionCount = updated.repetitions;
406
- currentCard.easeFactor = updated.easeFactor;
407
- currentCard.nextReview = updated.nextReview;
408
- currentCard.lastReviewed = new Date();
409
-
410
- // Save to file
411
- await store.saveCard(currentCard);
412
-
413
- // Move to next card
414
- loadNextCard();
415
- setShowAnswer(false);
416
- };
417
-
418
- return (
419
- <Box flexDirection="column">
420
- <Text bold>{currentCard.title}</Text>
421
- <Box marginTop={1}>
422
- <Text>{currentCard.question}</Text>
423
- </Box>
424
-
425
- {showAnswer && (
426
- <Box marginTop={1}>
427
- <Text color="green">{currentCard.answer}</Text>
428
- <Box marginTop={1}>
429
- <Text>Rate your recall: [1] Blackout [2] Wrong [3] Hard [4] Good [5] Easy</Text>
430
- </Box>
431
- </Box>
432
- )}
433
-
434
- {!showAnswer && (
435
- <Box marginTop={1}>
436
- <Text color="gray">Press SPACE to reveal answer</Text>
437
- </Box>
438
- )}
439
- </Box>
440
- );
441
- }
442
- ```
443
-
444
- ## 6. Key JavaScript Libraries
445
-
446
- ### Required Dependencies
447
-
448
- ```json
449
- {
450
- "dependencies": {
451
- "ink": "^4.0.0", // Terminal UI framework
452
- "ink-markdown": "^1.0.0", // Markdown rendering in Ink
453
- "react": "^18.0.0", // Required by Ink
454
- "yaml": "^2.0.0", // YAML parsing for frontmatter
455
- "gray-matter": "^4.0.0", // Frontmatter extraction (alternative)
456
- "date-fns": "^3.0.0", // Date manipulation
457
- "glob": "^10.0.0", // File pattern matching
458
- "ink-text-input": "^5.0.0", // Text input component
459
- "ink-select-input": "^5.0.0", // Selection lists
460
- "ink-spinner": "^5.0.0" // Loading indicators
461
- }
462
- }
463
- ```
464
-
465
- ## 7. Project Structure
466
-
467
- ```
468
- gocard-js/
469
- ├── src/
470
- │ ├── index.js # CLI entry point
471
- │ ├── models/
472
- │ │ ├── Card.js # Card class
473
- │ │ └── Deck.js # Deck class
474
- │ ├── store/
475
- │ │ ├── CardStore.js # File system operations
476
- │ │ ├── parser.js # Markdown parsing
477
- │ │ └── writer.js # Markdown serialization
478
- │ ├── srs/
479
- │ │ └── sm2.js # SM-2 algorithm
480
- │ ├── ui/
481
- │ │ ├── App.js # Main Ink app
482
- │ │ ├── MainMenu.js # Main menu screen
483
- │ │ ├── StudyScreen.js # Study interface
484
- │ │ ├── BrowseDecks.js # Deck browser
485
- │ │ └── StatsScreen.js # Statistics view
486
- │ └── utils/
487
- │ ├── config.js # Configuration
488
- │ └── dates.js # Date helpers
489
- ├── package.json
490
- └── README.md
491
- ```
492
-
493
- ## 8. Implementation Checklist
494
-
495
- ### Phase 1: Core Functionality
496
- - [ ] Implement Card and Deck models
497
- - [ ] Build markdown parser (frontmatter + sections)
498
- - [ ] Build markdown writer (serialize cards)
499
- - [ ] Implement CardStore (file operations)
500
- - [ ] Implement SM-2 algorithm
501
-
502
- ### Phase 2: Basic UI
503
- - [ ] Create Main Menu with Ink
504
- - [ ] Build Study Screen (show question → answer → rating)
505
- - [ ] Implement keyboard shortcuts
506
- - [ ] Add markdown rendering in terminal
507
-
508
- ### Phase 3: Advanced Features
509
- - [ ] Deck browser
510
- - [ ] Statistics screens (summary, deck review, forecast)
511
- - [ ] Configuration management
512
- - [ ] Error handling and validation
513
-
514
- ### Phase 4: Polish
515
- - [ ] Add loading states
516
- - [ ] Improve markdown rendering (code highlighting)
517
- - [ ] Add help screens
518
- - [ ] Write tests
519
- - [ ] Documentation
520
-
521
- ## 9. Critical Implementation Details
522
-
523
- ### Date Handling
524
- - Store dates in ISO format (YYYY-MM-DD) in frontmatter
525
- - Calculate "due" by comparing `nextReview` to current date (start of day)
526
- - Update `lastReviewed` to current date after each review
527
-
528
- ### File Watching (Optional Enhancement)
529
- - Watch markdown files for external changes
530
- - Reload decks when files modified
531
- - Useful for Git-based workflows
532
-
533
- ### Performance Considerations
534
- - Cache parsed cards in memory
535
- - Only re-parse files when modified
536
- - Use async I/O for all file operations
537
-
538
- ### SM-2 Edge Cases
539
- - Minimum ease factor: 1.3 (prevents intervals from becoming too short)
540
- - Quality < 3: Reset repetitions but keep ease factor change
541
- - First two intervals are fixed (1 day, 6 days)
542
-
543
- ## 10. Example Usage Flow
544
-
545
- ```bash
546
- # Install
547
- npm install -g gocard-js
548
-
549
- # Initialize
550
- mkdir ~/drill
551
- gocard-js --dir ~/drill
552
-
553
- # Screens flow:
554
- # 1. Main Menu → Select "Study"
555
- # 2. Study Screen → Shows due card question
556
- # 3. Press SPACE → Reveals answer
557
- # 4. Press 1-5 → Rate difficulty, moves to next card
558
- # 5. When done → Back to main menu
559
- # 6. Browse Decks → See all decks and stats
560
- # 7. Statistics → View learning progress
561
- ```
562
-
563
- ***
564
-
565
- This should give you everything you need to build a faithful JavaScript/Ink implementation of drill! The key is maintaining the same file format (markdown + YAML frontmatter), implementing the SM-2 algorithm correctly, and building a clean Ink-based interface. Would you like me to elaborate on any specific section?
566
-
567
- Sources
568
- [1] GitHub - DavidMiserak/drill: A lightweight file-based spaced repetition system (SRS) that uses plain Markdown files for flashcards. Perfect for developers who prefer text files, Git version control, and keyboard-driven interfaces. https://github.com/DavidMiserak/drill
569
- [2] The Anki SM-2 Spaced Repetition Algorithm https://help.remnote.com/en/articles/6026144-the-anki-sm-2-spaced-repetition-algorithm
570
- [3] thyagoluciano/sm2: SM-2 is a simple spaced repetition ... https://github.com/thyagoluciano/sm2
571
- [4] Spaced Repetition Algorithm: A Three‐Day Journey from ... https://www.reddit.com/r/Anki/comments/17u01ge/spaced_repetition_algorithm_a_threeday_journey/
572
- [5] java - Spaced repetition algorithm from SuperMemo (SM-2) https://stackoverflow.com/questions/49047159/spaced-repetition-algorithm-from-supermemo-sm-2
573
- [6] SuperMemo https://en.wikipedia.org/wiki/SuperMemo
574
- [7] The best spaced repetition time intervals according ... - Traverse https://traverse.link/spaced-repetition/the-optimal-spaced-repetition-schedule
575
- [8] FSRS vs SM2: How Spaced Repetition Algorithms Work https://www.youtube.com/watch?v=v2asudkSFek
576
- [9] A-Factor https://supermemo.guru/wiki/A-Factor
577
- [10] Spaced repetition algorithm metric https://supermemopedia.com/wiki/Spaced_repetition_algorithm_metric
578
-
Binary file
package/docs/summery.png DELETED
Binary file