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.
- checksums.yaml +4 -4
- data/.gitignore +59 -0
- data/.rubocop.yml +79 -0
- data/.ruby-version +1 -0
- data/.tool-versions +1 -0
- data/CHANGELOG.md +173 -0
- data/Gemfile +21 -0
- data/Makefile +20 -0
- data/README.md +160 -32
- data/Rakefile +174 -0
- data/anki_generator.gemspec +42 -0
- data/bin/anki_generator +3 -2
- data/docs/CI_SETUP.md +120 -0
- data/docs/architecture/current-v1.3.0.architecture.json +315 -0
- data/docs/architecture/current-v1.3.0.html +14990 -0
- data/docs/architecture/current-v1.3.0.visual-check.json +548 -0
- data/docs/architecture/phase3-proposed.architecture.json +310 -0
- data/docs/architecture/phase3-proposed.html +15001 -0
- data/docs/architecture/phase3-proposed.visual-check.json +548 -0
- data/docs/phase3-draft.md +86 -0
- data/examples/example_class.rb +13 -0
- data/examples/manual_cards.yaml +5 -0
- data/examples/study_prompt.txt +3 -0
- data/input/input.yaml.example +3 -0
- data/lib/anki_generator/anki_connect_client.rb +85 -0
- data/lib/anki_generator/apkg_schema.rb +257 -0
- data/lib/anki_generator/apkg_writer.rb +149 -0
- data/lib/anki_generator/card.rb +83 -0
- data/lib/anki_generator/cli.rb +183 -0
- data/lib/anki_generator/client_factory.rb +20 -0
- data/lib/anki_generator/commands/create_ai_template.rb +39 -0
- data/lib/anki_generator/commands/generate_deck.rb +43 -0
- data/lib/anki_generator/commands/generate_yaml.rb +63 -0
- data/lib/anki_generator/commands/import.rb +75 -0
- data/lib/anki_generator/commands/prompt_based.rb +74 -0
- data/lib/anki_generator/commands/prompt_to_deck.rb +84 -0
- data/lib/anki_generator/commands/push.rb +32 -0
- data/lib/anki_generator/commands/serve.rb +99 -0
- data/lib/anki_generator/commands/test_api.rb +33 -0
- data/lib/anki_generator/deck_builder.rb +184 -0
- data/lib/anki_generator/errors.rb +22 -0
- data/lib/anki_generator/file_processor.rb +154 -0
- data/lib/anki_generator/importers/csv.rb +52 -0
- data/lib/anki_generator/importers/markdown.rb +72 -0
- data/lib/anki_generator/prompt_builder.rb +77 -0
- data/lib/anki_generator/server.rb +98 -0
- data/lib/anki_generator/ui.rb +28 -0
- data/lib/anki_generator/version.rb +5 -0
- data/lib/anki_generator.rb +18 -114
- data/prompt.txt +5 -0
- metadata +100 -43
- data/lib/anki_cli.rb +0 -259
- data/lib/file_processor.rb +0 -156
- 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.
|