@aprimediet/codewalker 1.2.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.
- package/README.md +45 -5
- package/package.json +1 -1
- package/prompts/codewalker.md +8 -1
- package/skills/codewalker/SKILL.md +165 -28
- package/src/analyze/analyzer.test.ts +214 -0
- package/src/analyze/analyzer.ts +290 -0
- package/src/analyze/cards.test.ts +156 -0
- package/src/analyze/cards.ts +110 -0
- package/src/analyze/coverage.test.ts +158 -0
- package/src/analyze/coverage.ts +98 -0
- package/src/analyze/debt.test.ts +111 -0
- package/src/analyze/debt.ts +180 -0
- package/src/analyze/review.test.ts +127 -0
- package/src/analyze/review.ts +127 -0
- package/src/cards.test.ts +123 -1
- package/src/cards.ts +53 -0
- package/src/db.test.ts +484 -8
- package/src/db.ts +398 -2
- package/src/enrich.test.ts +102 -0
- package/src/enrich.ts +107 -0
- package/src/format.test.ts +148 -0
- package/src/format.ts +13 -0
- package/src/index.contract.test.ts +62 -0
- package/src/index.ts +427 -9
- package/src/indexer.heal.test.ts +90 -0
- package/src/indexer.ts +9 -1
- package/src/notes-cards.test.ts +99 -0
- package/src/notes-cards.ts +92 -0
- package/src/notes.test.ts +172 -0
- package/src/notes.ts +151 -0
- package/src/project.test.ts +21 -1
- package/src/project.ts +9 -1
- package/src/query.test.ts +152 -1
- package/src/query.ts +15 -6
- package/src/types.ts +46 -2
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, upsertLibrary, upsertLibSymbol, deleteLibrary, searchLibSymbols } from './db.ts';
|
|
6
|
+
import { openDb, bootstrapDb, upsertSymbol, searchSymbols, getMeta, setMeta, deleteFileSymbols, upsertLibrary, upsertLibSymbol, deleteLibrary, searchLibSymbols, upsertNote, searchNotes, deleteNote, updateSymbolSummary, selectUnenrichedSymbols, upsertFinding, deleteFindingsForFile, searchFindings } from './db.ts';
|
|
7
7
|
|
|
8
8
|
describe('db.ts', () => {
|
|
9
9
|
let tmpDir: string;
|
|
@@ -47,11 +47,11 @@ describe('db.ts', () => {
|
|
|
47
47
|
// No error = idempotent
|
|
48
48
|
});
|
|
49
49
|
|
|
50
|
-
it('sets user_version to
|
|
50
|
+
it('sets user_version to 4 (v1.4 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(
|
|
54
|
+
expect(version).toBe(4);
|
|
55
55
|
db.close();
|
|
56
56
|
});
|
|
57
57
|
|
|
@@ -67,8 +67,8 @@ describe('db.ts', () => {
|
|
|
67
67
|
db.close();
|
|
68
68
|
});
|
|
69
69
|
|
|
70
|
-
it('does not destroy existing
|
|
71
|
-
// Bootstrap
|
|
70
|
+
it('does not destroy existing tables (additive upgrade, v2 → notes in v3)', () => {
|
|
71
|
+
// Bootstrap then re-bootstrap (simulate upgrade)
|
|
72
72
|
const db = new Database(dbPath);
|
|
73
73
|
bootstrapDb(db);
|
|
74
74
|
upsertSymbol(db, {
|
|
@@ -76,16 +76,32 @@ describe('db.ts', () => {
|
|
|
76
76
|
line_start: 1, line_end: 1, signature: '', doc: '', summary: '', card_path: '',
|
|
77
77
|
});
|
|
78
78
|
|
|
79
|
-
// Call bootstrapDb again (simulates upgrade to
|
|
79
|
+
// Call bootstrapDb again (simulates upgrade to v3 adds notes tables)
|
|
80
80
|
bootstrapDb(db);
|
|
81
81
|
|
|
82
82
|
// Symbol still there
|
|
83
83
|
const symbols = searchSymbols(db, 'keep', undefined, 10);
|
|
84
84
|
expect(symbols).toHaveLength(1);
|
|
85
85
|
|
|
86
|
-
//
|
|
86
|
+
// Notes tables exist
|
|
87
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('
|
|
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);
|
|
92
|
+
|
|
93
|
+
// Analysis tables exist (v1.4 additive upgrade)
|
|
94
|
+
expect(tables.map(t => t.name)).toContain('analysis');
|
|
95
|
+
const analysisFts = db.prepare("SELECT name FROM sqlite_master WHERE name='analysis_fts'").all() as { name: string }[];
|
|
96
|
+
expect(analysisFts.length).toBe(1);
|
|
97
|
+
|
|
98
|
+
// Analysis triggers exist
|
|
99
|
+
const triggers = db.prepare("SELECT name FROM sqlite_master WHERE type='trigger'").all() as { name: string }[];
|
|
100
|
+
const triggerNames = triggers.map(t => t.name);
|
|
101
|
+
expect(triggerNames).toContain('analysis_ai');
|
|
102
|
+
expect(triggerNames).toContain('analysis_ad');
|
|
103
|
+
expect(triggerNames).toContain('analysis_au');
|
|
104
|
+
|
|
89
105
|
db.close();
|
|
90
106
|
});
|
|
91
107
|
});
|
|
@@ -319,6 +335,466 @@ describe('db.ts', () => {
|
|
|
319
335
|
});
|
|
320
336
|
});
|
|
321
337
|
|
|
338
|
+
describe('notes CRUD + FTS via triggers', () => {
|
|
339
|
+
it('creates notes + notes_fts on bootstrap', () => {
|
|
340
|
+
const db = openDb(dbPath);
|
|
341
|
+
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all() as { name: string }[];
|
|
342
|
+
expect(tables.map(t => t.name)).toContain('notes');
|
|
343
|
+
|
|
344
|
+
const ftsTables = db.prepare("SELECT name FROM sqlite_master WHERE name='notes_fts'").all() as { name: string }[];
|
|
345
|
+
expect(ftsTables.length).toBe(1);
|
|
346
|
+
|
|
347
|
+
// Triggers exist
|
|
348
|
+
const triggers = db.prepare("SELECT name FROM sqlite_master WHERE type='trigger'").all() as { name: string }[];
|
|
349
|
+
const triggerNames = triggers.map(t => t.name);
|
|
350
|
+
expect(triggerNames).toContain('notes_ai');
|
|
351
|
+
expect(triggerNames).toContain('notes_ad');
|
|
352
|
+
expect(triggerNames).toContain('notes_au');
|
|
353
|
+
db.close();
|
|
354
|
+
});
|
|
355
|
+
|
|
356
|
+
it('upsertNote inserts a note and FTS MATCH finds it', () => {
|
|
357
|
+
const db = openDb(dbPath);
|
|
358
|
+
|
|
359
|
+
const id = upsertNote(db, {
|
|
360
|
+
note_kind: 'glossary',
|
|
361
|
+
title: 'Idempotency Key',
|
|
362
|
+
body: 'A client-supplied key that makes a retried POST safe to replay.',
|
|
363
|
+
tags: 'api,payments',
|
|
364
|
+
related: 'createCharge',
|
|
365
|
+
card_path: '/entries/glossary/idempotency-key.md',
|
|
366
|
+
});
|
|
367
|
+
|
|
368
|
+
expect(typeof id).toBe('number');
|
|
369
|
+
expect(id).toBeGreaterThan(0);
|
|
370
|
+
|
|
371
|
+
// FTS search
|
|
372
|
+
const results = searchNotes(db, 'idempotency');
|
|
373
|
+
expect(results).toHaveLength(1);
|
|
374
|
+
expect(results[0]!.name).toBe('Idempotency Key');
|
|
375
|
+
expect(results[0]!.note_kind).toBe('glossary');
|
|
376
|
+
expect(results[0]!.source).toBe('note');
|
|
377
|
+
|
|
378
|
+
db.close();
|
|
379
|
+
});
|
|
380
|
+
|
|
381
|
+
it('upsertNote upserts on (note_kind, title) — updating a note refreshes FTS via notes_au', () => {
|
|
382
|
+
const db = openDb(dbPath);
|
|
383
|
+
|
|
384
|
+
upsertNote(db, {
|
|
385
|
+
note_kind: 'glossary',
|
|
386
|
+
title: 'Retry Key',
|
|
387
|
+
body: 'Old description',
|
|
388
|
+
tags: '',
|
|
389
|
+
related: '',
|
|
390
|
+
card_path: '/entries/glossary/retry-key.md',
|
|
391
|
+
});
|
|
392
|
+
|
|
393
|
+
// Update the body
|
|
394
|
+
upsertNote(db, {
|
|
395
|
+
note_kind: 'glossary',
|
|
396
|
+
title: 'Retry Key',
|
|
397
|
+
body: 'New improved description with uniqueTermXYZ',
|
|
398
|
+
tags: 'updated',
|
|
399
|
+
related: '',
|
|
400
|
+
card_path: '/entries/glossary/retry-key.md',
|
|
401
|
+
});
|
|
402
|
+
|
|
403
|
+
// Should find by new text (proves notes_au fired the reindex)
|
|
404
|
+
const results = searchNotes(db, 'uniqueTermXYZ');
|
|
405
|
+
expect(results).toHaveLength(1);
|
|
406
|
+
expect(results[0]!.summary).toContain('uniqueTermXYZ');
|
|
407
|
+
|
|
408
|
+
// Should NOT have duplicates (upsert behavior)
|
|
409
|
+
const all = searchNotes(db, '');
|
|
410
|
+
expect(all).toHaveLength(1);
|
|
411
|
+
|
|
412
|
+
db.close();
|
|
413
|
+
});
|
|
414
|
+
|
|
415
|
+
it('deleteNote removes a note from both base table and FTS', () => {
|
|
416
|
+
const db = openDb(dbPath);
|
|
417
|
+
|
|
418
|
+
upsertNote(db, {
|
|
419
|
+
note_kind: 'glossary',
|
|
420
|
+
title: 'Temp Term',
|
|
421
|
+
body: 'Will be deleted',
|
|
422
|
+
tags: '',
|
|
423
|
+
related: '',
|
|
424
|
+
card_path: '',
|
|
425
|
+
});
|
|
426
|
+
|
|
427
|
+
deleteNote(db, 'glossary', 'Temp Term');
|
|
428
|
+
|
|
429
|
+
const results = searchNotes(db, 'Temp');
|
|
430
|
+
expect(results).toHaveLength(0);
|
|
431
|
+
|
|
432
|
+
// Verify base table is also empty
|
|
433
|
+
const row = db.prepare("SELECT * FROM notes WHERE title = ?").get('Temp Term');
|
|
434
|
+
expect(row).toBeUndefined();
|
|
435
|
+
|
|
436
|
+
db.close();
|
|
437
|
+
});
|
|
438
|
+
|
|
439
|
+
it('searchNotes empty query returns all notes ordered by title', () => {
|
|
440
|
+
const db = openDb(dbPath);
|
|
441
|
+
|
|
442
|
+
upsertNote(db, {
|
|
443
|
+
note_kind: 'glossary', title: 'Zebra', body: '', tags: '', related: '', card_path: '',
|
|
444
|
+
});
|
|
445
|
+
upsertNote(db, {
|
|
446
|
+
note_kind: 'decision', title: 'Alpha decision', body: '', tags: '', related: '', card_path: '',
|
|
447
|
+
});
|
|
448
|
+
|
|
449
|
+
const results = searchNotes(db, '');
|
|
450
|
+
expect(results).toHaveLength(2);
|
|
451
|
+
expect(results[0]!.name).toBe('Alpha decision');
|
|
452
|
+
expect(results[1]!.name).toBe('Zebra');
|
|
453
|
+
|
|
454
|
+
db.close();
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
it('searchNotes with kindFilter narrows by note_kind', () => {
|
|
458
|
+
const db = openDb(dbPath);
|
|
459
|
+
|
|
460
|
+
upsertNote(db, {
|
|
461
|
+
note_kind: 'glossary', title: 'Term', body: '', tags: '', related: '', card_path: '',
|
|
462
|
+
});
|
|
463
|
+
upsertNote(db, {
|
|
464
|
+
note_kind: 'decision', title: 'Decide', body: '', tags: '', related: '', card_path: '',
|
|
465
|
+
});
|
|
466
|
+
|
|
467
|
+
const glossaryResults = searchNotes(db, '', 'glossary');
|
|
468
|
+
expect(glossaryResults).toHaveLength(1);
|
|
469
|
+
expect(glossaryResults[0]!.name).toBe('Term');
|
|
470
|
+
|
|
471
|
+
const decisionResults = searchNotes(db, '', 'decision');
|
|
472
|
+
expect(decisionResults).toHaveLength(1);
|
|
473
|
+
expect(decisionResults[0]!.name).toBe('Decide');
|
|
474
|
+
|
|
475
|
+
db.close();
|
|
476
|
+
});
|
|
477
|
+
});
|
|
478
|
+
|
|
479
|
+
describe('updateSymbolSummary', () => {
|
|
480
|
+
it('sets symbols.summary for a matching card_path and symbols_au reindexes FTS', () => {
|
|
481
|
+
const db = openDb(dbPath);
|
|
482
|
+
|
|
483
|
+
upsertSymbol(db, {
|
|
484
|
+
name: 'myFunc', kind: 'function', file_path: 'src/a.ts',
|
|
485
|
+
line_start: 1, line_end: 10, signature: '', doc: 'Old doc', summary: '',
|
|
486
|
+
card_path: '/cards/myFunc.md',
|
|
487
|
+
});
|
|
488
|
+
|
|
489
|
+
const result = updateSymbolSummary(db, '/cards/myFunc.md', 'A function that does X');
|
|
490
|
+
expect(result).toBe(true);
|
|
491
|
+
|
|
492
|
+
// Query FTS for the summary word — proves symbols_au trigger reindexed
|
|
493
|
+
const syms = searchSymbols(db, 'does X', undefined, 10);
|
|
494
|
+
expect(syms).toHaveLength(1);
|
|
495
|
+
expect(syms[0]!.name).toBe('myFunc');
|
|
496
|
+
expect(syms[0]!.summary).toBe('A function that does X');
|
|
497
|
+
|
|
498
|
+
db.close();
|
|
499
|
+
});
|
|
500
|
+
|
|
501
|
+
it('returns false when no symbol matches card_path', () => {
|
|
502
|
+
const db = openDb(dbPath);
|
|
503
|
+
const result = updateSymbolSummary(db, '/nonexistent.md', 'summary');
|
|
504
|
+
expect(result).toBe(false);
|
|
505
|
+
db.close();
|
|
506
|
+
});
|
|
507
|
+
});
|
|
508
|
+
|
|
509
|
+
describe('selectUnenrichedSymbols', () => {
|
|
510
|
+
it('selects symbols with empty summary under a path prefix', () => {
|
|
511
|
+
const db = openDb(dbPath);
|
|
512
|
+
|
|
513
|
+
upsertSymbol(db, {
|
|
514
|
+
name: 'enriched', kind: 'function', file_path: 'src/auth/token.ts',
|
|
515
|
+
line_start: 1, line_end: 5, signature: '', doc: '', summary: 'Already done',
|
|
516
|
+
card_path: '/cards/enriched.md',
|
|
517
|
+
});
|
|
518
|
+
upsertSymbol(db, {
|
|
519
|
+
name: 'unenriched', kind: 'function', file_path: 'src/auth/token.ts',
|
|
520
|
+
line_start: 10, line_end: 20, signature: '', doc: '', summary: '',
|
|
521
|
+
card_path: '/cards/unenriched.md',
|
|
522
|
+
});
|
|
523
|
+
upsertSymbol(db, {
|
|
524
|
+
name: 'other', kind: 'function', file_path: 'src/other/util.ts',
|
|
525
|
+
line_start: 1, line_end: 1, signature: '', doc: '', summary: '',
|
|
526
|
+
card_path: '/cards/other.md',
|
|
527
|
+
});
|
|
528
|
+
|
|
529
|
+
const results = selectUnenrichedSymbols(db, 'src/auth/', 10);
|
|
530
|
+
expect(results).toHaveLength(1);
|
|
531
|
+
expect(results[0]!.name).toBe('unenriched');
|
|
532
|
+
expect(results[0]!.card_path).toBe('/cards/unenriched.md');
|
|
533
|
+
|
|
534
|
+
db.close();
|
|
535
|
+
});
|
|
536
|
+
|
|
537
|
+
it('returns empty for a prefix with no unenriched symbols', () => {
|
|
538
|
+
const db = openDb(dbPath);
|
|
539
|
+
|
|
540
|
+
upsertSymbol(db, {
|
|
541
|
+
name: 'done', kind: 'function', file_path: 'src/done.ts',
|
|
542
|
+
line_start: 1, line_end: 1, signature: '', doc: '', summary: 'Has summary',
|
|
543
|
+
card_path: '',
|
|
544
|
+
});
|
|
545
|
+
|
|
546
|
+
const results = selectUnenrichedSymbols(db, 'src/done.ts', 10);
|
|
547
|
+
expect(results).toHaveLength(0);
|
|
548
|
+
|
|
549
|
+
db.close();
|
|
550
|
+
});
|
|
551
|
+
|
|
552
|
+
it('respects limit parameter', () => {
|
|
553
|
+
const db = openDb(dbPath);
|
|
554
|
+
|
|
555
|
+
for (let i = 0; i < 5; i++) {
|
|
556
|
+
upsertSymbol(db, {
|
|
557
|
+
name: `sym${i}`, kind: 'function', file_path: 'src/a.ts',
|
|
558
|
+
line_start: i, line_end: i, signature: '', doc: '', summary: '',
|
|
559
|
+
card_path: '',
|
|
560
|
+
});
|
|
561
|
+
}
|
|
562
|
+
|
|
563
|
+
const results = selectUnenrichedSymbols(db, 'src/', 3);
|
|
564
|
+
expect(results).toHaveLength(3);
|
|
565
|
+
|
|
566
|
+
db.close();
|
|
567
|
+
});
|
|
568
|
+
|
|
569
|
+
it('returns card_path, name, kind, file_path, line_start, line_end for each result', () => {
|
|
570
|
+
const db = openDb(dbPath);
|
|
571
|
+
|
|
572
|
+
upsertSymbol(db, {
|
|
573
|
+
name: 'myFunc', kind: 'function', file_path: 'src/test.ts',
|
|
574
|
+
line_start: 10, line_end: 20, signature: '', doc: '', summary: '',
|
|
575
|
+
card_path: '/cards/test.md',
|
|
576
|
+
});
|
|
577
|
+
|
|
578
|
+
const results = selectUnenrichedSymbols(db, 'src/', 10);
|
|
579
|
+
expect(results).toHaveLength(1);
|
|
580
|
+
expect(results[0]!.name).toBe('myFunc');
|
|
581
|
+
expect(results[0]!.kind).toBe('function');
|
|
582
|
+
expect(results[0]!.file_path).toBe('src/test.ts');
|
|
583
|
+
expect(results[0]!.line_start).toBe(10);
|
|
584
|
+
expect(results[0]!.line_end).toBe(20);
|
|
585
|
+
expect(results[0]!.card_path).toBe('/cards/test.md');
|
|
586
|
+
|
|
587
|
+
db.close();
|
|
588
|
+
});
|
|
589
|
+
});
|
|
590
|
+
|
|
591
|
+
describe('analysis CRUD + FTS via triggers', () => {
|
|
592
|
+
it('creates analysis + analysis_fts tables on bootstrap', () => {
|
|
593
|
+
const db = openDb(dbPath);
|
|
594
|
+
const tables = db.prepare("SELECT name FROM sqlite_master WHERE type='table'").all() as { name: string }[];
|
|
595
|
+
expect(tables.map(t => t.name)).toContain('analysis');
|
|
596
|
+
|
|
597
|
+
const ftsTables = db.prepare("SELECT name FROM sqlite_master WHERE name='analysis_fts'").all() as { name: string }[];
|
|
598
|
+
expect(ftsTables.length).toBe(1);
|
|
599
|
+
|
|
600
|
+
const triggers = db.prepare("SELECT name FROM sqlite_master WHERE type='trigger'").all() as { name: string }[];
|
|
601
|
+
const triggerNames = triggers.map(t => t.name);
|
|
602
|
+
expect(triggerNames).toContain('analysis_ai');
|
|
603
|
+
expect(triggerNames).toContain('analysis_ad');
|
|
604
|
+
expect(triggerNames).toContain('analysis_au');
|
|
605
|
+
db.close();
|
|
606
|
+
});
|
|
607
|
+
|
|
608
|
+
it('upsertFinding inserts a finding and FTS MATCH finds it', () => {
|
|
609
|
+
const db = openDb(dbPath);
|
|
610
|
+
|
|
611
|
+
const id = upsertFinding(db, {
|
|
612
|
+
finding_kind: 'coverage',
|
|
613
|
+
title: 'Low coverage: src/auth/token.ts',
|
|
614
|
+
severity: 'warn',
|
|
615
|
+
file_path: 'src/auth/token.ts',
|
|
616
|
+
line_start: 0,
|
|
617
|
+
line_end: 0,
|
|
618
|
+
metric: '38% (24/63 lines)',
|
|
619
|
+
body: 'Auth token refresh path is under-tested — 38% line coverage.',
|
|
620
|
+
related: 'refreshToken, token.ts:42-71',
|
|
621
|
+
card_path: '/entries/analysis/coverage/low-coverage-src-auth-token.md',
|
|
622
|
+
});
|
|
623
|
+
|
|
624
|
+
expect(typeof id).toBe('number');
|
|
625
|
+
expect(id).toBeGreaterThan(0);
|
|
626
|
+
|
|
627
|
+
// FTS search
|
|
628
|
+
const results = searchFindings(db, 'coverage');
|
|
629
|
+
expect(results).toHaveLength(1);
|
|
630
|
+
expect(results[0]!.name).toBe('Low coverage: src/auth/token.ts');
|
|
631
|
+
expect(results[0]!.finding_kind).toBe('coverage');
|
|
632
|
+
expect(results[0]!.severity).toBe('warn');
|
|
633
|
+
expect(results[0]!.source).toBe('analysis');
|
|
634
|
+
|
|
635
|
+
db.close();
|
|
636
|
+
});
|
|
637
|
+
|
|
638
|
+
it('upsertFinding upserts on (finding_kind, file_path, title) — update refreshes FTS via analysis_au', () => {
|
|
639
|
+
const db = openDb(dbPath);
|
|
640
|
+
|
|
641
|
+
upsertFinding(db, {
|
|
642
|
+
finding_kind: 'coverage',
|
|
643
|
+
title: 'Low coverage: token.ts',
|
|
644
|
+
severity: 'warn',
|
|
645
|
+
file_path: 'src/auth/token.ts',
|
|
646
|
+
line_start: 0, line_end: 0,
|
|
647
|
+
metric: '38%',
|
|
648
|
+
body: 'Old description',
|
|
649
|
+
related: '',
|
|
650
|
+
card_path: '/cards/coverage-token.md',
|
|
651
|
+
});
|
|
652
|
+
|
|
653
|
+
// Update
|
|
654
|
+
upsertFinding(db, {
|
|
655
|
+
finding_kind: 'coverage',
|
|
656
|
+
title: 'Low coverage: token.ts',
|
|
657
|
+
severity: 'high',
|
|
658
|
+
file_path: 'src/auth/token.ts',
|
|
659
|
+
line_start: 0, line_end: 0,
|
|
660
|
+
metric: '25%',
|
|
661
|
+
body: 'Worse now with uniqueTermFindABC',
|
|
662
|
+
related: '',
|
|
663
|
+
card_path: '/cards/coverage-token.md',
|
|
664
|
+
});
|
|
665
|
+
|
|
666
|
+
// Search for new text (proves analysis_au fired)
|
|
667
|
+
const results = searchFindings(db, 'uniqueTermFindABC');
|
|
668
|
+
expect(results).toHaveLength(1);
|
|
669
|
+
expect(results[0]!.metric).toBe('25%');
|
|
670
|
+
|
|
671
|
+
// No duplicate
|
|
672
|
+
const all = searchFindings(db, '');
|
|
673
|
+
expect(all).toHaveLength(1);
|
|
674
|
+
|
|
675
|
+
db.close();
|
|
676
|
+
});
|
|
677
|
+
|
|
678
|
+
it('deleteFindingsForFile removes findings and FTS rows for a file', () => {
|
|
679
|
+
const db = openDb(dbPath);
|
|
680
|
+
|
|
681
|
+
upsertFinding(db, {
|
|
682
|
+
finding_kind: 'debt', title: 'TODO: fix me', severity: 'info',
|
|
683
|
+
file_path: 'src/a.ts', line_start: 5, line_end: 5,
|
|
684
|
+
metric: 'TODO', body: 'FixThisUniqueXYZ123', related: '',
|
|
685
|
+
card_path: '/cards/debt-a.md',
|
|
686
|
+
});
|
|
687
|
+
upsertFinding(db, {
|
|
688
|
+
finding_kind: 'debt', title: 'HACK: workaround', severity: 'high',
|
|
689
|
+
file_path: 'src/b.ts', line_start: 10, line_end: 10,
|
|
690
|
+
metric: 'HACK', body: 'Ugly workaround', related: '',
|
|
691
|
+
card_path: '/cards/debt-b.md',
|
|
692
|
+
});
|
|
693
|
+
|
|
694
|
+
deleteFindingsForFile(db, 'debt', 'src/a.ts');
|
|
695
|
+
|
|
696
|
+
const all = searchFindings(db, '');
|
|
697
|
+
expect(all).toHaveLength(1);
|
|
698
|
+
expect(all[0]!.name).toBe('HACK: workaround');
|
|
699
|
+
|
|
700
|
+
// FTS should be clean too — no result for the deleted finding's unique text
|
|
701
|
+
const ftsResults = searchFindings(db, 'FixThisUniqueXYZ123');
|
|
702
|
+
expect(ftsResults).toHaveLength(0);
|
|
703
|
+
|
|
704
|
+
db.close();
|
|
705
|
+
});
|
|
706
|
+
|
|
707
|
+
it('searchFindings empty query returns all findings ordered by severity, title', () => {
|
|
708
|
+
const db = openDb(dbPath);
|
|
709
|
+
|
|
710
|
+
upsertFinding(db, {
|
|
711
|
+
finding_kind: 'debt', title: 'Alpha debt', severity: 'info',
|
|
712
|
+
file_path: 'src/a.ts', line_start: 0, line_end: 0,
|
|
713
|
+
metric: 'TODO', body: '', related: '', card_path: '',
|
|
714
|
+
});
|
|
715
|
+
upsertFinding(db, {
|
|
716
|
+
finding_kind: 'coverage', title: 'Zebra coverage', severity: 'high',
|
|
717
|
+
file_path: 'src/b.ts', line_start: 0, line_end: 0,
|
|
718
|
+
metric: '10%', body: '', related: '', card_path: '',
|
|
719
|
+
});
|
|
720
|
+
|
|
721
|
+
const results = searchFindings(db, '');
|
|
722
|
+
expect(results.length).toBeGreaterThanOrEqual(2);
|
|
723
|
+
|
|
724
|
+
db.close();
|
|
725
|
+
});
|
|
726
|
+
|
|
727
|
+
it('searchFindings with kindFilter narrows by finding_kind', () => {
|
|
728
|
+
const db = openDb(dbPath);
|
|
729
|
+
|
|
730
|
+
upsertFinding(db, {
|
|
731
|
+
finding_kind: 'coverage', title: 'Coverage gap', severity: 'warn',
|
|
732
|
+
file_path: 'src/a.ts', line_start: 0, line_end: 0,
|
|
733
|
+
metric: '50%', body: '', related: '', card_path: '',
|
|
734
|
+
});
|
|
735
|
+
upsertFinding(db, {
|
|
736
|
+
finding_kind: 'debt', title: 'Debt item', severity: 'high',
|
|
737
|
+
file_path: 'src/b.ts', line_start: 0, line_end: 0,
|
|
738
|
+
metric: 'TODO', body: '', related: '', card_path: '',
|
|
739
|
+
});
|
|
740
|
+
|
|
741
|
+
const coverageResults = searchFindings(db, '', 'coverage');
|
|
742
|
+
expect(coverageResults).toHaveLength(1);
|
|
743
|
+
expect(coverageResults[0]!.finding_kind).toBe('coverage');
|
|
744
|
+
|
|
745
|
+
db.close();
|
|
746
|
+
});
|
|
747
|
+
|
|
748
|
+
it('searchFindings with query returns bm25-ranked results', () => {
|
|
749
|
+
const db = openDb(dbPath);
|
|
750
|
+
|
|
751
|
+
upsertFinding(db, {
|
|
752
|
+
finding_kind: 'debt', title: 'General', severity: 'info',
|
|
753
|
+
file_path: 'src/a.ts', line_start: 0, line_end: 0,
|
|
754
|
+
metric: 'TODO', body: 'Some text about authentication token refresh', related: '', card_path: '',
|
|
755
|
+
});
|
|
756
|
+
upsertFinding(db, {
|
|
757
|
+
finding_kind: 'debt', title: 'Token issue', severity: 'high',
|
|
758
|
+
file_path: 'src/b.ts', line_start: 0, line_end: 0,
|
|
759
|
+
metric: 'FIXME', body: 'Token validation is weak', related: '', card_path: '',
|
|
760
|
+
});
|
|
761
|
+
|
|
762
|
+
const results = searchFindings(db, 'token');
|
|
763
|
+
expect(results).toHaveLength(2);
|
|
764
|
+
|
|
765
|
+
db.close();
|
|
766
|
+
});
|
|
767
|
+
});
|
|
768
|
+
|
|
769
|
+
describe('convention notes', () => {
|
|
770
|
+
it('upsertNote accepts convention note_kind and it is searchable', () => {
|
|
771
|
+
const db = openDb(dbPath);
|
|
772
|
+
|
|
773
|
+
const id = upsertNote(db, {
|
|
774
|
+
note_kind: 'convention',
|
|
775
|
+
title: 'Use functional components',
|
|
776
|
+
body: 'All React components must be pure functions, not classes.',
|
|
777
|
+
tags: 'react,style',
|
|
778
|
+
related: 'Component, renderFunction',
|
|
779
|
+
card_path: '/entries/conventions/use-functional-components.md',
|
|
780
|
+
});
|
|
781
|
+
|
|
782
|
+
expect(id).toBeGreaterThan(0);
|
|
783
|
+
|
|
784
|
+
// Search by note_kind
|
|
785
|
+
const results = searchNotes(db, '', 'convention');
|
|
786
|
+
expect(results).toHaveLength(1);
|
|
787
|
+
expect(results[0]!.name).toBe('Use functional components');
|
|
788
|
+
expect(results[0]!.note_kind).toBe('convention');
|
|
789
|
+
|
|
790
|
+
// FTS search
|
|
791
|
+
const ftsResults = searchNotes(db, 'functional');
|
|
792
|
+
expect(ftsResults).toHaveLength(1);
|
|
793
|
+
|
|
794
|
+
db.close();
|
|
795
|
+
});
|
|
796
|
+
});
|
|
797
|
+
|
|
322
798
|
describe('meta', () => {
|
|
323
799
|
it('setMeta and getMeta round-trip values', () => {
|
|
324
800
|
const db = openDb(dbPath);
|