anki_generator 1.1.0 → 1.4.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +59 -0
  3. data/.rubocop.yml +79 -0
  4. data/.ruby-version +1 -0
  5. data/.tool-versions +1 -0
  6. data/CHANGELOG.md +173 -0
  7. data/Gemfile +21 -0
  8. data/Makefile +20 -0
  9. data/README.md +160 -32
  10. data/Rakefile +174 -0
  11. data/anki_generator.gemspec +42 -0
  12. data/bin/anki_generator +3 -2
  13. data/docs/CI_SETUP.md +120 -0
  14. data/docs/architecture/current-v1.3.0.architecture.json +315 -0
  15. data/docs/architecture/current-v1.3.0.html +14990 -0
  16. data/docs/architecture/current-v1.3.0.visual-check.json +548 -0
  17. data/docs/architecture/phase3-proposed.architecture.json +310 -0
  18. data/docs/architecture/phase3-proposed.html +15001 -0
  19. data/docs/architecture/phase3-proposed.visual-check.json +548 -0
  20. data/docs/phase3-draft.md +86 -0
  21. data/examples/example_class.rb +13 -0
  22. data/examples/manual_cards.yaml +5 -0
  23. data/examples/study_prompt.txt +3 -0
  24. data/input/input.yaml.example +3 -0
  25. data/lib/anki_generator/anki_connect_client.rb +85 -0
  26. data/lib/anki_generator/apkg_schema.rb +257 -0
  27. data/lib/anki_generator/apkg_writer.rb +149 -0
  28. data/lib/anki_generator/card.rb +83 -0
  29. data/lib/anki_generator/cli.rb +183 -0
  30. data/lib/anki_generator/client_factory.rb +20 -0
  31. data/lib/anki_generator/commands/create_ai_template.rb +39 -0
  32. data/lib/anki_generator/commands/generate_deck.rb +43 -0
  33. data/lib/anki_generator/commands/generate_yaml.rb +63 -0
  34. data/lib/anki_generator/commands/import.rb +75 -0
  35. data/lib/anki_generator/commands/prompt_based.rb +74 -0
  36. data/lib/anki_generator/commands/prompt_to_deck.rb +84 -0
  37. data/lib/anki_generator/commands/push.rb +32 -0
  38. data/lib/anki_generator/commands/serve.rb +99 -0
  39. data/lib/anki_generator/commands/test_api.rb +33 -0
  40. data/lib/anki_generator/deck_builder.rb +184 -0
  41. data/lib/anki_generator/errors.rb +22 -0
  42. data/lib/anki_generator/file_processor.rb +154 -0
  43. data/lib/anki_generator/importers/csv.rb +52 -0
  44. data/lib/anki_generator/importers/markdown.rb +72 -0
  45. data/lib/anki_generator/prompt_builder.rb +77 -0
  46. data/lib/anki_generator/server.rb +98 -0
  47. data/lib/anki_generator/ui.rb +28 -0
  48. data/lib/anki_generator/version.rb +5 -0
  49. data/lib/anki_generator.rb +18 -114
  50. data/prompt.txt +5 -0
  51. metadata +100 -43
  52. data/lib/anki_cli.rb +0 -259
  53. data/lib/file_processor.rb +0 -156
  54. data/lib/openrouter_client.rb +0 -158
@@ -0,0 +1,548 @@
1
+ {
2
+ "schemaVersion": 1,
3
+ "ok": true,
4
+ "command": "visual-check",
5
+ "evidenceKind": "automated-browser",
6
+ "status": "pass",
7
+ "visualReview": "pending",
8
+ "artifact": {
9
+ "path": "/Users/chevass/Projects/anki-cards/docs/architecture/phase3-proposed.html",
10
+ "sha256": "3db14a8cd0b7db5aff607b7b4a54b5a180e0f89c8c57c62ed70be728178afe7b",
11
+ "bytes": 811239
12
+ },
13
+ "state": {
14
+ "detail": "read",
15
+ "motion": "still"
16
+ },
17
+ "chrome": {
18
+ "status": "available",
19
+ "executable": "/Applications/Google Chrome.app/Contents/MacOS/Google Chrome"
20
+ },
21
+ "diagnostics": [],
22
+ "containment": {
23
+ "status": "pass",
24
+ "viewports": [
25
+ {
26
+ "width": 1440,
27
+ "height": 900,
28
+ "theme": "light",
29
+ "innerWidth": 1440,
30
+ "innerHeight": 900,
31
+ "scrollWidth": 1440,
32
+ "scrollHeight": 900,
33
+ "overflowX": false,
34
+ "overflowY": false,
35
+ "ok": true,
36
+ "readerWidth": 960,
37
+ "diagramWidth": 930,
38
+ "viewBoxWidth": 1070,
39
+ "minimumProjectedNodeTextPx": 7.822429906542056,
40
+ "minimumProjectedNodeText": "offline cache · Kotlin",
41
+ "minimumProjectedNodeTextDetail": "context",
42
+ "minimumRequiredNodeTextPx": 6,
43
+ "readabilityOk": true,
44
+ "hasLegend": true,
45
+ "hasNavigationDock": true,
46
+ "legendDockIntersectionArea": 0,
47
+ "dockStageIntersectionArea": 0,
48
+ "dockStageGap": 10.21875,
49
+ "requiredDockStageGap": 10,
50
+ "viewerChromeStageOk": true,
51
+ "viewerChromeReserve": 51,
52
+ "viewerChromeActive": true,
53
+ "viewerChromeOk": true,
54
+ "resolvedTheme": "light"
55
+ },
56
+ {
57
+ "width": 1600,
58
+ "height": 1000,
59
+ "theme": "light",
60
+ "innerWidth": 1600,
61
+ "innerHeight": 1000,
62
+ "scrollWidth": 1600,
63
+ "scrollHeight": 1000,
64
+ "overflowX": false,
65
+ "overflowY": false,
66
+ "ok": true,
67
+ "readerWidth": 983,
68
+ "diagramWidth": 953,
69
+ "viewBoxWidth": 1070,
70
+ "minimumProjectedNodeTextPx": 8.01588785046729,
71
+ "minimumProjectedNodeText": "offline cache · Kotlin",
72
+ "minimumProjectedNodeTextDetail": "context",
73
+ "minimumRequiredNodeTextPx": 6,
74
+ "readabilityOk": true,
75
+ "hasLegend": true,
76
+ "hasNavigationDock": true,
77
+ "legendDockIntersectionArea": 0,
78
+ "dockStageIntersectionArea": 0,
79
+ "dockStageGap": 10.21875,
80
+ "requiredDockStageGap": 10,
81
+ "viewerChromeStageOk": true,
82
+ "viewerChromeReserve": 51,
83
+ "viewerChromeActive": true,
84
+ "viewerChromeOk": true,
85
+ "resolvedTheme": "light"
86
+ },
87
+ {
88
+ "width": 1920,
89
+ "height": 1080,
90
+ "theme": "light",
91
+ "innerWidth": 1920,
92
+ "innerHeight": 1080,
93
+ "scrollWidth": 1920,
94
+ "scrollHeight": 1080,
95
+ "overflowX": false,
96
+ "overflowY": false,
97
+ "ok": true,
98
+ "readerWidth": 1231,
99
+ "diagramWidth": 1201,
100
+ "viewBoxWidth": 1070,
101
+ "minimumProjectedNodeTextPx": 9,
102
+ "minimumProjectedNodeText": "offline cache · Kotlin",
103
+ "minimumProjectedNodeTextDetail": "context",
104
+ "minimumRequiredNodeTextPx": 6,
105
+ "readabilityOk": true,
106
+ "hasLegend": true,
107
+ "hasNavigationDock": true,
108
+ "legendDockIntersectionArea": 0,
109
+ "dockStageIntersectionArea": 0,
110
+ "dockStageGap": 10.21875,
111
+ "requiredDockStageGap": 10,
112
+ "viewerChromeStageOk": true,
113
+ "viewerChromeReserve": 51,
114
+ "viewerChromeActive": true,
115
+ "viewerChromeOk": true,
116
+ "resolvedTheme": "light"
117
+ },
118
+ {
119
+ "width": 2048,
120
+ "height": 1320,
121
+ "theme": "light",
122
+ "innerWidth": 2048,
123
+ "innerHeight": 1320,
124
+ "scrollWidth": 2048,
125
+ "scrollHeight": 1320,
126
+ "overflowX": false,
127
+ "overflowY": false,
128
+ "ok": true,
129
+ "readerWidth": 1669,
130
+ "diagramWidth": 1619,
131
+ "viewBoxWidth": 1070,
132
+ "minimumProjectedNodeTextPx": 9,
133
+ "minimumProjectedNodeText": "offline cache · Kotlin",
134
+ "minimumProjectedNodeTextDetail": "context",
135
+ "minimumRequiredNodeTextPx": 6,
136
+ "readabilityOk": true,
137
+ "hasLegend": true,
138
+ "hasNavigationDock": true,
139
+ "legendDockIntersectionArea": 0,
140
+ "dockStageIntersectionArea": 0,
141
+ "dockStageGap": 10.21875,
142
+ "requiredDockStageGap": 10,
143
+ "viewerChromeStageOk": true,
144
+ "viewerChromeReserve": 41,
145
+ "viewerChromeActive": true,
146
+ "viewerChromeOk": true,
147
+ "resolvedTheme": "light"
148
+ }
149
+ ]
150
+ },
151
+ "readability": {
152
+ "status": "pass",
153
+ "minimumProjectedNodeTextPx": 6,
154
+ "viewports": [
155
+ {
156
+ "width": 1440,
157
+ "height": 900,
158
+ "theme": "light",
159
+ "innerWidth": 1440,
160
+ "innerHeight": 900,
161
+ "scrollWidth": 1440,
162
+ "scrollHeight": 900,
163
+ "overflowX": false,
164
+ "overflowY": false,
165
+ "ok": true,
166
+ "readerWidth": 960,
167
+ "diagramWidth": 930,
168
+ "viewBoxWidth": 1070,
169
+ "minimumProjectedNodeTextPx": 7.822429906542056,
170
+ "minimumProjectedNodeText": "offline cache · Kotlin",
171
+ "minimumProjectedNodeTextDetail": "context",
172
+ "minimumRequiredNodeTextPx": 6,
173
+ "readabilityOk": true,
174
+ "hasLegend": true,
175
+ "hasNavigationDock": true,
176
+ "legendDockIntersectionArea": 0,
177
+ "dockStageIntersectionArea": 0,
178
+ "dockStageGap": 10.21875,
179
+ "requiredDockStageGap": 10,
180
+ "viewerChromeStageOk": true,
181
+ "viewerChromeReserve": 51,
182
+ "viewerChromeActive": true,
183
+ "viewerChromeOk": true,
184
+ "resolvedTheme": "light"
185
+ },
186
+ {
187
+ "width": 1600,
188
+ "height": 1000,
189
+ "theme": "light",
190
+ "innerWidth": 1600,
191
+ "innerHeight": 1000,
192
+ "scrollWidth": 1600,
193
+ "scrollHeight": 1000,
194
+ "overflowX": false,
195
+ "overflowY": false,
196
+ "ok": true,
197
+ "readerWidth": 983,
198
+ "diagramWidth": 953,
199
+ "viewBoxWidth": 1070,
200
+ "minimumProjectedNodeTextPx": 8.01588785046729,
201
+ "minimumProjectedNodeText": "offline cache · Kotlin",
202
+ "minimumProjectedNodeTextDetail": "context",
203
+ "minimumRequiredNodeTextPx": 6,
204
+ "readabilityOk": true,
205
+ "hasLegend": true,
206
+ "hasNavigationDock": true,
207
+ "legendDockIntersectionArea": 0,
208
+ "dockStageIntersectionArea": 0,
209
+ "dockStageGap": 10.21875,
210
+ "requiredDockStageGap": 10,
211
+ "viewerChromeStageOk": true,
212
+ "viewerChromeReserve": 51,
213
+ "viewerChromeActive": true,
214
+ "viewerChromeOk": true,
215
+ "resolvedTheme": "light"
216
+ },
217
+ {
218
+ "width": 1920,
219
+ "height": 1080,
220
+ "theme": "light",
221
+ "innerWidth": 1920,
222
+ "innerHeight": 1080,
223
+ "scrollWidth": 1920,
224
+ "scrollHeight": 1080,
225
+ "overflowX": false,
226
+ "overflowY": false,
227
+ "ok": true,
228
+ "readerWidth": 1231,
229
+ "diagramWidth": 1201,
230
+ "viewBoxWidth": 1070,
231
+ "minimumProjectedNodeTextPx": 9,
232
+ "minimumProjectedNodeText": "offline cache · Kotlin",
233
+ "minimumProjectedNodeTextDetail": "context",
234
+ "minimumRequiredNodeTextPx": 6,
235
+ "readabilityOk": true,
236
+ "hasLegend": true,
237
+ "hasNavigationDock": true,
238
+ "legendDockIntersectionArea": 0,
239
+ "dockStageIntersectionArea": 0,
240
+ "dockStageGap": 10.21875,
241
+ "requiredDockStageGap": 10,
242
+ "viewerChromeStageOk": true,
243
+ "viewerChromeReserve": 51,
244
+ "viewerChromeActive": true,
245
+ "viewerChromeOk": true,
246
+ "resolvedTheme": "light"
247
+ },
248
+ {
249
+ "width": 2048,
250
+ "height": 1320,
251
+ "theme": "light",
252
+ "innerWidth": 2048,
253
+ "innerHeight": 1320,
254
+ "scrollWidth": 2048,
255
+ "scrollHeight": 1320,
256
+ "overflowX": false,
257
+ "overflowY": false,
258
+ "ok": true,
259
+ "readerWidth": 1669,
260
+ "diagramWidth": 1619,
261
+ "viewBoxWidth": 1070,
262
+ "minimumProjectedNodeTextPx": 9,
263
+ "minimumProjectedNodeText": "offline cache · Kotlin",
264
+ "minimumProjectedNodeTextDetail": "context",
265
+ "minimumRequiredNodeTextPx": 6,
266
+ "readabilityOk": true,
267
+ "hasLegend": true,
268
+ "hasNavigationDock": true,
269
+ "legendDockIntersectionArea": 0,
270
+ "dockStageIntersectionArea": 0,
271
+ "dockStageGap": 10.21875,
272
+ "requiredDockStageGap": 10,
273
+ "viewerChromeStageOk": true,
274
+ "viewerChromeReserve": 41,
275
+ "viewerChromeActive": true,
276
+ "viewerChromeOk": true,
277
+ "resolvedTheme": "light"
278
+ }
279
+ ]
280
+ },
281
+ "viewerChrome": {
282
+ "status": "pass",
283
+ "viewports": [
284
+ {
285
+ "width": 1440,
286
+ "height": 900,
287
+ "theme": "light",
288
+ "innerWidth": 1440,
289
+ "innerHeight": 900,
290
+ "scrollWidth": 1440,
291
+ "scrollHeight": 900,
292
+ "overflowX": false,
293
+ "overflowY": false,
294
+ "ok": true,
295
+ "readerWidth": 960,
296
+ "diagramWidth": 930,
297
+ "viewBoxWidth": 1070,
298
+ "minimumProjectedNodeTextPx": 7.822429906542056,
299
+ "minimumProjectedNodeText": "offline cache · Kotlin",
300
+ "minimumProjectedNodeTextDetail": "context",
301
+ "minimumRequiredNodeTextPx": 6,
302
+ "readabilityOk": true,
303
+ "hasLegend": true,
304
+ "hasNavigationDock": true,
305
+ "legendDockIntersectionArea": 0,
306
+ "dockStageIntersectionArea": 0,
307
+ "dockStageGap": 10.21875,
308
+ "requiredDockStageGap": 10,
309
+ "viewerChromeStageOk": true,
310
+ "viewerChromeReserve": 51,
311
+ "viewerChromeActive": true,
312
+ "viewerChromeOk": true,
313
+ "resolvedTheme": "light"
314
+ },
315
+ {
316
+ "width": 1600,
317
+ "height": 1000,
318
+ "theme": "light",
319
+ "innerWidth": 1600,
320
+ "innerHeight": 1000,
321
+ "scrollWidth": 1600,
322
+ "scrollHeight": 1000,
323
+ "overflowX": false,
324
+ "overflowY": false,
325
+ "ok": true,
326
+ "readerWidth": 983,
327
+ "diagramWidth": 953,
328
+ "viewBoxWidth": 1070,
329
+ "minimumProjectedNodeTextPx": 8.01588785046729,
330
+ "minimumProjectedNodeText": "offline cache · Kotlin",
331
+ "minimumProjectedNodeTextDetail": "context",
332
+ "minimumRequiredNodeTextPx": 6,
333
+ "readabilityOk": true,
334
+ "hasLegend": true,
335
+ "hasNavigationDock": true,
336
+ "legendDockIntersectionArea": 0,
337
+ "dockStageIntersectionArea": 0,
338
+ "dockStageGap": 10.21875,
339
+ "requiredDockStageGap": 10,
340
+ "viewerChromeStageOk": true,
341
+ "viewerChromeReserve": 51,
342
+ "viewerChromeActive": true,
343
+ "viewerChromeOk": true,
344
+ "resolvedTheme": "light"
345
+ },
346
+ {
347
+ "width": 1920,
348
+ "height": 1080,
349
+ "theme": "light",
350
+ "innerWidth": 1920,
351
+ "innerHeight": 1080,
352
+ "scrollWidth": 1920,
353
+ "scrollHeight": 1080,
354
+ "overflowX": false,
355
+ "overflowY": false,
356
+ "ok": true,
357
+ "readerWidth": 1231,
358
+ "diagramWidth": 1201,
359
+ "viewBoxWidth": 1070,
360
+ "minimumProjectedNodeTextPx": 9,
361
+ "minimumProjectedNodeText": "offline cache · Kotlin",
362
+ "minimumProjectedNodeTextDetail": "context",
363
+ "minimumRequiredNodeTextPx": 6,
364
+ "readabilityOk": true,
365
+ "hasLegend": true,
366
+ "hasNavigationDock": true,
367
+ "legendDockIntersectionArea": 0,
368
+ "dockStageIntersectionArea": 0,
369
+ "dockStageGap": 10.21875,
370
+ "requiredDockStageGap": 10,
371
+ "viewerChromeStageOk": true,
372
+ "viewerChromeReserve": 51,
373
+ "viewerChromeActive": true,
374
+ "viewerChromeOk": true,
375
+ "resolvedTheme": "light"
376
+ },
377
+ {
378
+ "width": 2048,
379
+ "height": 1320,
380
+ "theme": "light",
381
+ "innerWidth": 2048,
382
+ "innerHeight": 1320,
383
+ "scrollWidth": 2048,
384
+ "scrollHeight": 1320,
385
+ "overflowX": false,
386
+ "overflowY": false,
387
+ "ok": true,
388
+ "readerWidth": 1669,
389
+ "diagramWidth": 1619,
390
+ "viewBoxWidth": 1070,
391
+ "minimumProjectedNodeTextPx": 9,
392
+ "minimumProjectedNodeText": "offline cache · Kotlin",
393
+ "minimumProjectedNodeTextDetail": "context",
394
+ "minimumRequiredNodeTextPx": 6,
395
+ "readabilityOk": true,
396
+ "hasLegend": true,
397
+ "hasNavigationDock": true,
398
+ "legendDockIntersectionArea": 0,
399
+ "dockStageIntersectionArea": 0,
400
+ "dockStageGap": 10.21875,
401
+ "requiredDockStageGap": 10,
402
+ "viewerChromeStageOk": true,
403
+ "viewerChromeReserve": 41,
404
+ "viewerChromeActive": true,
405
+ "viewerChromeOk": true,
406
+ "resolvedTheme": "light"
407
+ }
408
+ ]
409
+ },
410
+ "captures": {
411
+ "status": "pass",
412
+ "screenshots": [
413
+ {
414
+ "width": 1440,
415
+ "height": 900,
416
+ "theme": "light",
417
+ "innerWidth": 1440,
418
+ "innerHeight": 900,
419
+ "scrollWidth": 1440,
420
+ "scrollHeight": 900,
421
+ "overflowX": false,
422
+ "overflowY": false,
423
+ "ok": true,
424
+ "readerWidth": 960,
425
+ "diagramWidth": 930,
426
+ "viewBoxWidth": 1070,
427
+ "minimumProjectedNodeTextPx": 7.822429906542056,
428
+ "minimumProjectedNodeText": "offline cache · Kotlin",
429
+ "minimumProjectedNodeTextDetail": "context",
430
+ "minimumRequiredNodeTextPx": 6,
431
+ "readabilityOk": true,
432
+ "hasLegend": true,
433
+ "hasNavigationDock": true,
434
+ "legendDockIntersectionArea": 0,
435
+ "dockStageIntersectionArea": 0,
436
+ "dockStageGap": 10.21875,
437
+ "requiredDockStageGap": 10,
438
+ "viewerChromeStageOk": true,
439
+ "viewerChromeReserve": 51,
440
+ "viewerChromeActive": true,
441
+ "viewerChromeOk": true,
442
+ "resolvedTheme": "light",
443
+ "file": "phase3-proposed.visual-check.1440x900.light.png"
444
+ },
445
+ {
446
+ "width": 1440,
447
+ "height": 900,
448
+ "theme": "dark",
449
+ "innerWidth": 1440,
450
+ "innerHeight": 900,
451
+ "scrollWidth": 1440,
452
+ "scrollHeight": 900,
453
+ "overflowX": false,
454
+ "overflowY": false,
455
+ "ok": true,
456
+ "readerWidth": 960,
457
+ "diagramWidth": 930,
458
+ "viewBoxWidth": 1070,
459
+ "minimumProjectedNodeTextPx": 7.822429906542056,
460
+ "minimumProjectedNodeText": "offline cache · Kotlin",
461
+ "minimumProjectedNodeTextDetail": "context",
462
+ "minimumRequiredNodeTextPx": 6,
463
+ "readabilityOk": true,
464
+ "hasLegend": true,
465
+ "hasNavigationDock": true,
466
+ "legendDockIntersectionArea": 0,
467
+ "dockStageIntersectionArea": 0,
468
+ "dockStageGap": 10.21875,
469
+ "requiredDockStageGap": 10,
470
+ "viewerChromeStageOk": true,
471
+ "viewerChromeReserve": 51,
472
+ "viewerChromeActive": true,
473
+ "viewerChromeOk": true,
474
+ "resolvedTheme": "dark",
475
+ "file": "phase3-proposed.visual-check.1440x900.dark.png"
476
+ },
477
+ {
478
+ "width": 2048,
479
+ "height": 1320,
480
+ "theme": "light",
481
+ "innerWidth": 2048,
482
+ "innerHeight": 1320,
483
+ "scrollWidth": 2048,
484
+ "scrollHeight": 1320,
485
+ "overflowX": false,
486
+ "overflowY": false,
487
+ "ok": true,
488
+ "readerWidth": 1669,
489
+ "diagramWidth": 1619,
490
+ "viewBoxWidth": 1070,
491
+ "minimumProjectedNodeTextPx": 9,
492
+ "minimumProjectedNodeText": "offline cache · Kotlin",
493
+ "minimumProjectedNodeTextDetail": "context",
494
+ "minimumRequiredNodeTextPx": 6,
495
+ "readabilityOk": true,
496
+ "hasLegend": true,
497
+ "hasNavigationDock": true,
498
+ "legendDockIntersectionArea": 0,
499
+ "dockStageIntersectionArea": 0,
500
+ "dockStageGap": 10.21875,
501
+ "requiredDockStageGap": 10,
502
+ "viewerChromeStageOk": true,
503
+ "viewerChromeReserve": 41,
504
+ "viewerChromeActive": true,
505
+ "viewerChromeOk": true,
506
+ "resolvedTheme": "light",
507
+ "file": "phase3-proposed.visual-check.2048x1320.light.png"
508
+ },
509
+ {
510
+ "width": 2048,
511
+ "height": 1320,
512
+ "theme": "dark",
513
+ "innerWidth": 2048,
514
+ "innerHeight": 1320,
515
+ "scrollWidth": 2048,
516
+ "scrollHeight": 1320,
517
+ "overflowX": false,
518
+ "overflowY": false,
519
+ "ok": true,
520
+ "readerWidth": 1669,
521
+ "diagramWidth": 1619,
522
+ "viewBoxWidth": 1070,
523
+ "minimumProjectedNodeTextPx": 9,
524
+ "minimumProjectedNodeText": "offline cache · Kotlin",
525
+ "minimumProjectedNodeTextDetail": "context",
526
+ "minimumRequiredNodeTextPx": 6,
527
+ "readabilityOk": true,
528
+ "hasLegend": true,
529
+ "hasNavigationDock": true,
530
+ "legendDockIntersectionArea": 0,
531
+ "dockStageIntersectionArea": 0,
532
+ "dockStageGap": 10.21875,
533
+ "requiredDockStageGap": 10,
534
+ "viewerChromeStageOk": true,
535
+ "viewerChromeReserve": 41,
536
+ "viewerChromeActive": true,
537
+ "viewerChromeOk": true,
538
+ "resolvedTheme": "dark",
539
+ "file": "phase3-proposed.visual-check.2048x1320.dark.png"
540
+ }
541
+ ],
542
+ "contactSheet": "phase3-proposed.visual-check.html"
543
+ },
544
+ "sidecars": {
545
+ "receipt": "phase3-proposed.visual-check.json",
546
+ "contactSheet": "phase3-proposed.visual-check.html"
547
+ }
548
+ }
@@ -0,0 +1,86 @@
1
+ # Phase 3 Draft — Hosted Sync Service + Mobile Apps
2
+
3
+ Status: draft for discussion (2026-09-30)
4
+ Diagrams: [current v1.3.0](architecture/current-v1.3.0.html) · [Phase 3 proposed](architecture/phase3-proposed.html)
5
+
6
+ ## 1. Goal
7
+
8
+ Seb generates decks with the gem (as today, free/local). The service adds one thing the gem cannot do: **decks that follow the user across devices** — generated server-side, visible seconds later in Android/iOS apps, studyable offline.
9
+
10
+ Non-goals for Phase 3:
11
+
12
+ - Replacing Anki. We export `.apkg` (Anki interop stays), but the apps render cards themselves — no Anki dependency on mobile.
13
+ - Multi-tenant SaaS scale. This is one developer + users measured in the hundreds, designed to run at near-zero cost.
14
+
15
+ ## 2. Guiding constraints
16
+
17
+ 1. **Cheap**: one small VPS (or free-tier container) + managed/free Postgres. No Kubernetes, no microservices.
18
+ 2. **Reuse the gem**: the generation logic already exists and is tested (`AnkiGenerator::DeckBuilder`, `ClientFactory`, `ApkgWriter`). The service wraps it; it does not rewrite it.
19
+ 3. **Offline first**: studying must work with zero connectivity; sync is a delta exchange, not a live requirement.
20
+ 4. **Same quality bar**: Ruby 3.3, minitest, SimpleCov floor, RuboCop zero offenses, no AI-slop patterns (small classes, DI, explicit errors).
21
+
22
+ ## 3. Architecture (see diagram)
23
+
24
+ ```
25
+ Android / iOS / Web ──HTTPS+JSON──> Sync API (Ruby, thin) ──> PostgreSQL
26
+ │ enqueue
27
+ v
28
+ Solid Queue ──> Generation worker
29
+ │ │ chat completions
30
+ │ v
31
+ │ OpenRouter / Ollama
32
+ └──<── insert cards
33
+ Sync API ──> Object storage (.apkg exports, optional)
34
+ ```
35
+
36
+ - **Sync API**: Sinatra (or Rails API-mode if it earns its keep). Auth via magic-link token (JWT). JSON endpoints only.
37
+ - **Generation worker**: a second process running the same codebase; consumes queue jobs, calls `ClientFactory.build(...)`, writes cards back to Postgres. Retries with backoff already exist in the client.
38
+ - **Web client**: the existing `serve` UI, hosted, with the API as its backend instead of in-process calls.
39
+ - **Object storage**: only for `.apkg` exports (S3-compatible or local disk on the VPS); the apps themselves read cards from Postgres via the API.
40
+
41
+ ## 4. Data model (initial)
42
+
43
+ ```
44
+ users id, email, passwordless token fields, created_at
45
+ decks id, user_id, name, source (manual|ai|import), updated_at
46
+ cards id, deck_id, front, back, tags[], cloze text, ord position
47
+ review_state card_id, ease, interval, due_at, reps, lapses (per user)
48
+ gen_jobs id, user_id, deck_id, prompt, provider, model, status, error
49
+ ```
50
+
51
+ Cards are content; `review_state` is per-user scheduling (SM-2 to start — same algorithm Anki uses, ~30 lines, no gem needed).
52
+
53
+ ## 5. Sync protocol (v1: dumb deltas)
54
+
55
+ Keep it boring; upgrade only when measurements say so.
56
+
57
+ - Client keeps a local SQLite mirror and a `last_synced_at` cursor.
58
+ - `GET /sync?since=t` → changed/ deleted cards, deck metadata, server time.
59
+ - `POST /sync` → client deltas (edits, review states) upserted by `(deck, client_uuid)`; conflicts: **last-write-wins** on content, review states merge by reps.
60
+ - Generation: `POST /generate` → `202 Accepted` + `job_id`; client polls `GET /jobs/:id` or receives a push token ping; finished job's cards appear in the next sync.
61
+
62
+ ## 6. Cost estimate (hobbyist scale)
63
+
64
+ | Piece | Choice | ~Cost |
65
+ |---|---|---|
66
+ | API + worker + queue | 1 small VPS (Hetzner CX11-ish) or Fly.io free allowances | $0–5/mo |
67
+ | PostgreSQL | same VPS, or Neon/Supabase free tier | $0/mo |
68
+ | LLM | OpenRouter pay-per-use (~$0.1–1/mo at personal scale) or Ollama sidecar on the VPS | $0–1/mo |
69
+ | Object storage | local disk; S3 free tier if exported decks grow | $0/mo |
70
+ | **Total** | | **$0–6/mo** |
71
+
72
+ ## 7. Milestones
73
+
74
+ 1. **M1 — API skeleton**: auth (magic link), decks/cards CRUD, minitest + RuboCop from day one. Deploy to VPS with CI.
75
+ 2. **M2 — Sync**: delta protocol, cursor, conflict rules; web client switched to the API.
76
+ 3. **M3 — Generation service**: Solid Queue + worker reusing the gem; job status endpoint.
77
+ 4. **M4 — Android app**: offline SQLite cache, review screen (SM-2), sync adapter. Kotlin + SQLDelight.
78
+ 5. **M5 — iOS app**: Swift + GRDB, same protocol. (Or start with a shared KMP core if M4 made that tempting.)
79
+ 6. **M6 — Polish**: `.apkg` export endpoint, push notifications, shareable read-only decks.
80
+
81
+ ## 8. Open questions
82
+
83
+ - **Anki compatibility layer**: expose per-deck `.apkg` (cheap — reuse `ApkgWriter`) so users can also import into real Anki. Likely yes, M6.
84
+ - **Collaboration/shared decks**: out of scope until single-user sync is proven.
85
+ - **KMP vs separate mobile codebases**: decide at M4; protocol is plain JSON so both stay possible.
86
+ - **Billing**: none for personal use; if it ever trends, cap generation per user per day rather than building payments.
@@ -0,0 +1,13 @@
1
+ class Calculator
2
+ attr_reader :history
3
+
4
+ def initialize
5
+ @history = []
6
+ end
7
+
8
+ def add(a, b)
9
+ result = a + b
10
+ @history << "#{a} + #{b} = #{result}"
11
+ result
12
+ end
13
+ end
@@ -0,0 +1,5 @@
1
+ - front: "What is a Ruby class?"
2
+ back: "A class is a blueprint for creating objects with shared attributes and methods"
3
+
4
+ - front: "How do you define a method in Ruby?"
5
+ back: "Use the 'def' keyword followed by the method name and optional parameters"
@@ -0,0 +1,3 @@
1
+ Create flashcards about Ruby programming fundamentals.
2
+ Focus on basic syntax, data types, control structures, and object-oriented concepts.
3
+ Make the questions practical and suitable for beginners.
@@ -0,0 +1,3 @@
1
+ - front: What is O-notation?
2
+ back: |
3
+ O-notation (Big O) is defined as: \( f(n) = O(g(n)) \) if there exists \( n_0 \in \mathbb{N} \) and \( c \in \mathbb{R}, c > 0 \) such that for all \( n > n_0 \), \( f(n) \leq c \cdot g(n) \).