@aprimediet/codewalker 1.1.0 → 1.3.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.
package/src/db.test.ts CHANGED
@@ -3,7 +3,7 @@ import * as fs from 'node:fs';
3
3
  import * as path from 'node:path';
4
4
  import * as os from 'node:os';
5
5
  import Database from 'better-sqlite3';
6
- import { openDb, bootstrapDb, upsertSymbol, searchSymbols, getMeta, setMeta, deleteFileSymbols } from './db.ts';
6
+ import { openDb, bootstrapDb, upsertSymbol, searchSymbols, getMeta, setMeta, deleteFileSymbols, upsertLibrary, upsertLibSymbol, deleteLibrary, searchLibSymbols, upsertNote, searchNotes, deleteNote, updateSymbolSummary, selectUnenrichedSymbols } from './db.ts';
7
7
 
8
8
  describe('db.ts', () => {
9
9
  let tmpDir: string;
@@ -47,11 +47,48 @@ describe('db.ts', () => {
47
47
  // No error = idempotent
48
48
  });
49
49
 
50
- it('sets user_version to 1', () => {
50
+ it('sets user_version to 3 (v1.3 schema)', () => {
51
51
  const db = new Database(dbPath);
52
52
  bootstrapDb(db);
53
53
  const version = db.pragma('user_version', { simple: true }) as number;
54
- expect(version).toBe(1);
54
+ expect(version).toBe(3);
55
+ db.close();
56
+ });
57
+
58
+ it('creates libraries, lib_symbols, and lib_symbols_fts tables', () => {
59
+ const db = openDb(dbPath);
60
+ const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all() as { name: string }[];
61
+ const tableNames = tables.map(t => t.name);
62
+ expect(tableNames).toContain('libraries');
63
+ expect(tableNames).toContain('lib_symbols');
64
+
65
+ const ftsTables = db.prepare("SELECT name FROM sqlite_master WHERE name='lib_symbols_fts'").all() as { name: string }[];
66
+ expect(ftsTables.length).toBe(1);
67
+ db.close();
68
+ });
69
+
70
+ it('does not destroy existing tables (additive upgrade, v2 → notes in v3)', () => {
71
+ // Bootstrap then re-bootstrap (simulate upgrade)
72
+ const db = new Database(dbPath);
73
+ bootstrapDb(db);
74
+ upsertSymbol(db, {
75
+ name: 'keep', kind: 'function', file_path: 'src/a.ts',
76
+ line_start: 1, line_end: 1, signature: '', doc: '', summary: '', card_path: '',
77
+ });
78
+
79
+ // Call bootstrapDb again (simulates upgrade to v3 adds notes tables)
80
+ bootstrapDb(db);
81
+
82
+ // Symbol still there
83
+ const symbols = searchSymbols(db, 'keep', undefined, 10);
84
+ expect(symbols).toHaveLength(1);
85
+
86
+ // Notes tables exist
87
+ const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all() as { name: string }[];
88
+ expect(tables.map(t => t.name)).toContain('notes');
89
+
90
+ const ftsTables = db.prepare("SELECT name FROM sqlite_master WHERE name='notes_fts'").all() as { name: string }[];
91
+ expect(ftsTables.length).toBe(1);
55
92
  db.close();
56
93
  });
57
94
  });
@@ -173,6 +210,371 @@ describe('db.ts', () => {
173
210
  });
174
211
  });
175
212
 
213
+ describe('library CRUD', () => {
214
+ it('upsertLibrary inserts or updates a library record', () => {
215
+ const db = openDb(dbPath);
216
+ upsertLibrary(db, { name: 'hono', version: '4.6.3', source: 'node_modules', dts_path: '/a.d.ts', readme: 'Hono web framework' });
217
+
218
+ const row = db.prepare("SELECT * FROM libraries WHERE name = ?").get('hono') as any;
219
+ expect(row).not.toBeUndefined();
220
+ expect(row.version).toBe('4.6.3');
221
+ expect(row.source).toBe('node_modules');
222
+ db.close();
223
+ });
224
+
225
+ it('upsertLibSymbol inserts a lib symbol and it is FTS-searchable', () => {
226
+ const db = openDb(dbPath);
227
+ upsertLibSymbol(db, {
228
+ lib: 'hono', version: '4.6.3', name: 'createMiddleware',
229
+ kind: 'function', signature: 'export declare function createMiddleware(...)',
230
+ doc: 'Define a typed middleware handler.', summary: 'Define a typed middleware handler.',
231
+ card_path: '/cards/hono/createMiddleware.md',
232
+ });
233
+
234
+ const results = searchLibSymbols(db, 'createMiddleware', undefined, 10);
235
+ expect(results).toHaveLength(1);
236
+ expect(results[0]!.name).toBe('createMiddleware');
237
+ expect(results[0]!.lib).toBe('hono');
238
+ expect(results[0]!.version).toBe('4.6.3');
239
+ expect(results[0]!.source).toBe('lib');
240
+
241
+ db.close();
242
+ });
243
+
244
+ it('searchLibSymbols empty query returns all symbols ordered by name', () => {
245
+ const db = openDb(dbPath);
246
+ upsertLibSymbol(db, {
247
+ lib: 'hono', version: '4.6.3', name: 'zMiddleware',
248
+ kind: 'function', signature: '', doc: '', summary: '', card_path: '',
249
+ });
250
+ upsertLibSymbol(db, {
251
+ lib: 'hono', version: '4.6.3', name: 'aRouter',
252
+ kind: 'function', signature: '', doc: '', summary: '', card_path: '',
253
+ });
254
+
255
+ const results = searchLibSymbols(db, '', undefined, 10);
256
+ expect(results).toHaveLength(2);
257
+ expect(results[0]!.name).toBe('aRouter');
258
+ expect(results[1]!.name).toBe('zMiddleware');
259
+ db.close();
260
+ });
261
+
262
+ it('deleteLibrary removes symbols and FTS rows for all versions of a lib', () => {
263
+ const db = openDb(dbPath);
264
+
265
+ upsertLibSymbol(db, {
266
+ lib: 'hono', version: '4.6.3', name: 'createMiddleware',
267
+ kind: 'function', signature: '', doc: '', summary: '', card_path: '',
268
+ });
269
+ upsertLibSymbol(db, {
270
+ lib: 'hono', version: '4.5.0', name: 'oldFunc',
271
+ kind: 'function', signature: '', doc: '', summary: '', card_path: '',
272
+ });
273
+
274
+ deleteLibrary(db, 'hono');
275
+
276
+ const results = searchLibSymbols(db, '', undefined, 10);
277
+ expect(results).toHaveLength(0);
278
+
279
+ const libRow = db.prepare("SELECT * FROM libraries WHERE name = ?").get('hono') as any;
280
+ expect(libRow).toBeUndefined();
281
+
282
+ db.close();
283
+ });
284
+
285
+ it('re-inserting same (lib, name) does not create duplicates', () => {
286
+ const db = openDb(dbPath);
287
+
288
+ upsertLibSymbol(db, {
289
+ lib: 'hono', version: '4.6.3', name: 'createMiddleware',
290
+ kind: 'function', signature: 'v1', doc: '', summary: '', card_path: '',
291
+ });
292
+ upsertLibSymbol(db, {
293
+ lib: 'hono', version: '4.6.3', name: 'createMiddleware',
294
+ kind: 'function', signature: 'v2', doc: '', summary: '', card_path: '',
295
+ });
296
+
297
+ const results = searchLibSymbols(db, 'createMiddleware', undefined, 10);
298
+ expect(results).toHaveLength(1);
299
+ // Latest signature
300
+ expect(results[0]!.signature).toBe('v2');
301
+
302
+ db.close();
303
+ });
304
+
305
+ it('kind filter narrows lib symbol results', () => {
306
+ const db = openDb(dbPath);
307
+
308
+ upsertLibSymbol(db, {
309
+ lib: 'hono', version: '4.6.3', name: 'myFunc',
310
+ kind: 'function', signature: '', doc: '', summary: '', card_path: '',
311
+ });
312
+ upsertLibSymbol(db, {
313
+ lib: 'hono', version: '4.6.3', name: 'MyType',
314
+ kind: 'type', signature: '', doc: '', summary: '', card_path: '',
315
+ });
316
+
317
+ const funcs = searchLibSymbols(db, '', 'function', 10);
318
+ expect(funcs).toHaveLength(1);
319
+ expect(funcs[0]!.name).toBe('myFunc');
320
+
321
+ db.close();
322
+ });
323
+ });
324
+
325
+ describe('notes CRUD + FTS via triggers', () => {
326
+ it('creates notes + notes_fts on bootstrap', () => {
327
+ const db = openDb(dbPath);
328
+ const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all() as { name: string }[];
329
+ expect(tables.map(t => t.name)).toContain('notes');
330
+
331
+ const ftsTables = db.prepare("SELECT name FROM sqlite_master WHERE name='notes_fts'").all() as { name: string }[];
332
+ expect(ftsTables.length).toBe(1);
333
+
334
+ // Triggers exist
335
+ const triggers = db.prepare("SELECT name FROM sqlite_master WHERE type='trigger'").all() as { name: string }[];
336
+ const triggerNames = triggers.map(t => t.name);
337
+ expect(triggerNames).toContain('notes_ai');
338
+ expect(triggerNames).toContain('notes_ad');
339
+ expect(triggerNames).toContain('notes_au');
340
+ db.close();
341
+ });
342
+
343
+ it('upsertNote inserts a note and FTS MATCH finds it', () => {
344
+ const db = openDb(dbPath);
345
+
346
+ const id = upsertNote(db, {
347
+ note_kind: 'glossary',
348
+ title: 'Idempotency Key',
349
+ body: 'A client-supplied key that makes a retried POST safe to replay.',
350
+ tags: 'api,payments',
351
+ related: 'createCharge',
352
+ card_path: '/entries/glossary/idempotency-key.md',
353
+ });
354
+
355
+ expect(typeof id).toBe('number');
356
+ expect(id).toBeGreaterThan(0);
357
+
358
+ // FTS search
359
+ const results = searchNotes(db, 'idempotency');
360
+ expect(results).toHaveLength(1);
361
+ expect(results[0]!.name).toBe('Idempotency Key');
362
+ expect(results[0]!.note_kind).toBe('glossary');
363
+ expect(results[0]!.source).toBe('note');
364
+
365
+ db.close();
366
+ });
367
+
368
+ it('upsertNote upserts on (note_kind, title) — updating a note refreshes FTS via notes_au', () => {
369
+ const db = openDb(dbPath);
370
+
371
+ upsertNote(db, {
372
+ note_kind: 'glossary',
373
+ title: 'Retry Key',
374
+ body: 'Old description',
375
+ tags: '',
376
+ related: '',
377
+ card_path: '/entries/glossary/retry-key.md',
378
+ });
379
+
380
+ // Update the body
381
+ upsertNote(db, {
382
+ note_kind: 'glossary',
383
+ title: 'Retry Key',
384
+ body: 'New improved description with uniqueTermXYZ',
385
+ tags: 'updated',
386
+ related: '',
387
+ card_path: '/entries/glossary/retry-key.md',
388
+ });
389
+
390
+ // Should find by new text (proves notes_au fired the reindex)
391
+ const results = searchNotes(db, 'uniqueTermXYZ');
392
+ expect(results).toHaveLength(1);
393
+ expect(results[0]!.summary).toContain('uniqueTermXYZ');
394
+
395
+ // Should NOT have duplicates (upsert behavior)
396
+ const all = searchNotes(db, '');
397
+ expect(all).toHaveLength(1);
398
+
399
+ db.close();
400
+ });
401
+
402
+ it('deleteNote removes a note from both base table and FTS', () => {
403
+ const db = openDb(dbPath);
404
+
405
+ upsertNote(db, {
406
+ note_kind: 'glossary',
407
+ title: 'Temp Term',
408
+ body: 'Will be deleted',
409
+ tags: '',
410
+ related: '',
411
+ card_path: '',
412
+ });
413
+
414
+ deleteNote(db, 'glossary', 'Temp Term');
415
+
416
+ const results = searchNotes(db, 'Temp');
417
+ expect(results).toHaveLength(0);
418
+
419
+ // Verify base table is also empty
420
+ const row = db.prepare("SELECT * FROM notes WHERE title = ?").get('Temp Term');
421
+ expect(row).toBeUndefined();
422
+
423
+ db.close();
424
+ });
425
+
426
+ it('searchNotes empty query returns all notes ordered by title', () => {
427
+ const db = openDb(dbPath);
428
+
429
+ upsertNote(db, {
430
+ note_kind: 'glossary', title: 'Zebra', body: '', tags: '', related: '', card_path: '',
431
+ });
432
+ upsertNote(db, {
433
+ note_kind: 'decision', title: 'Alpha decision', body: '', tags: '', related: '', card_path: '',
434
+ });
435
+
436
+ const results = searchNotes(db, '');
437
+ expect(results).toHaveLength(2);
438
+ expect(results[0]!.name).toBe('Alpha decision');
439
+ expect(results[1]!.name).toBe('Zebra');
440
+
441
+ db.close();
442
+ });
443
+
444
+ it('searchNotes with kindFilter narrows by note_kind', () => {
445
+ const db = openDb(dbPath);
446
+
447
+ upsertNote(db, {
448
+ note_kind: 'glossary', title: 'Term', body: '', tags: '', related: '', card_path: '',
449
+ });
450
+ upsertNote(db, {
451
+ note_kind: 'decision', title: 'Decide', body: '', tags: '', related: '', card_path: '',
452
+ });
453
+
454
+ const glossaryResults = searchNotes(db, '', 'glossary');
455
+ expect(glossaryResults).toHaveLength(1);
456
+ expect(glossaryResults[0]!.name).toBe('Term');
457
+
458
+ const decisionResults = searchNotes(db, '', 'decision');
459
+ expect(decisionResults).toHaveLength(1);
460
+ expect(decisionResults[0]!.name).toBe('Decide');
461
+
462
+ db.close();
463
+ });
464
+ });
465
+
466
+ describe('updateSymbolSummary', () => {
467
+ it('sets symbols.summary for a matching card_path and symbols_au reindexes FTS', () => {
468
+ const db = openDb(dbPath);
469
+
470
+ upsertSymbol(db, {
471
+ name: 'myFunc', kind: 'function', file_path: 'src/a.ts',
472
+ line_start: 1, line_end: 10, signature: '', doc: 'Old doc', summary: '',
473
+ card_path: '/cards/myFunc.md',
474
+ });
475
+
476
+ const result = updateSymbolSummary(db, '/cards/myFunc.md', 'A function that does X');
477
+ expect(result).toBe(true);
478
+
479
+ // Query FTS for the summary word — proves symbols_au trigger reindexed
480
+ const syms = searchSymbols(db, 'does X', undefined, 10);
481
+ expect(syms).toHaveLength(1);
482
+ expect(syms[0]!.name).toBe('myFunc');
483
+ expect(syms[0]!.summary).toBe('A function that does X');
484
+
485
+ db.close();
486
+ });
487
+
488
+ it('returns false when no symbol matches card_path', () => {
489
+ const db = openDb(dbPath);
490
+ const result = updateSymbolSummary(db, '/nonexistent.md', 'summary');
491
+ expect(result).toBe(false);
492
+ db.close();
493
+ });
494
+ });
495
+
496
+ describe('selectUnenrichedSymbols', () => {
497
+ it('selects symbols with empty summary under a path prefix', () => {
498
+ const db = openDb(dbPath);
499
+
500
+ upsertSymbol(db, {
501
+ name: 'enriched', kind: 'function', file_path: 'src/auth/token.ts',
502
+ line_start: 1, line_end: 5, signature: '', doc: '', summary: 'Already done',
503
+ card_path: '/cards/enriched.md',
504
+ });
505
+ upsertSymbol(db, {
506
+ name: 'unenriched', kind: 'function', file_path: 'src/auth/token.ts',
507
+ line_start: 10, line_end: 20, signature: '', doc: '', summary: '',
508
+ card_path: '/cards/unenriched.md',
509
+ });
510
+ upsertSymbol(db, {
511
+ name: 'other', kind: 'function', file_path: 'src/other/util.ts',
512
+ line_start: 1, line_end: 1, signature: '', doc: '', summary: '',
513
+ card_path: '/cards/other.md',
514
+ });
515
+
516
+ const results = selectUnenrichedSymbols(db, 'src/auth/', 10);
517
+ expect(results).toHaveLength(1);
518
+ expect(results[0]!.name).toBe('unenriched');
519
+ expect(results[0]!.card_path).toBe('/cards/unenriched.md');
520
+
521
+ db.close();
522
+ });
523
+
524
+ it('returns empty for a prefix with no unenriched symbols', () => {
525
+ const db = openDb(dbPath);
526
+
527
+ upsertSymbol(db, {
528
+ name: 'done', kind: 'function', file_path: 'src/done.ts',
529
+ line_start: 1, line_end: 1, signature: '', doc: '', summary: 'Has summary',
530
+ card_path: '',
531
+ });
532
+
533
+ const results = selectUnenrichedSymbols(db, 'src/done.ts', 10);
534
+ expect(results).toHaveLength(0);
535
+
536
+ db.close();
537
+ });
538
+
539
+ it('respects limit parameter', () => {
540
+ const db = openDb(dbPath);
541
+
542
+ for (let i = 0; i < 5; i++) {
543
+ upsertSymbol(db, {
544
+ name: `sym${i}`, kind: 'function', file_path: 'src/a.ts',
545
+ line_start: i, line_end: i, signature: '', doc: '', summary: '',
546
+ card_path: '',
547
+ });
548
+ }
549
+
550
+ const results = selectUnenrichedSymbols(db, 'src/', 3);
551
+ expect(results).toHaveLength(3);
552
+
553
+ db.close();
554
+ });
555
+
556
+ it('returns card_path, name, kind, file_path, line_start, line_end for each result', () => {
557
+ const db = openDb(dbPath);
558
+
559
+ upsertSymbol(db, {
560
+ name: 'myFunc', kind: 'function', file_path: 'src/test.ts',
561
+ line_start: 10, line_end: 20, signature: '', doc: '', summary: '',
562
+ card_path: '/cards/test.md',
563
+ });
564
+
565
+ const results = selectUnenrichedSymbols(db, 'src/', 10);
566
+ expect(results).toHaveLength(1);
567
+ expect(results[0]!.name).toBe('myFunc');
568
+ expect(results[0]!.kind).toBe('function');
569
+ expect(results[0]!.file_path).toBe('src/test.ts');
570
+ expect(results[0]!.line_start).toBe(10);
571
+ expect(results[0]!.line_end).toBe(20);
572
+ expect(results[0]!.card_path).toBe('/cards/test.md');
573
+
574
+ db.close();
575
+ });
576
+ });
577
+
176
578
  describe('meta', () => {
177
579
  it('setMeta and getMeta round-trip values', () => {
178
580
  const db = openDb(dbPath);