@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 CHANGED
@@ -21,12 +21,36 @@ source files ──→ [ctags / regex] ──→ Symbol[]
21
21
  index.db (SQLite + FTS5, disposable)
22
22
 
23
23
  codewalker_query (compact results)
24
+
25
+ node_modules ──→ [.d.ts extract] ──→ LibSymbol[]
26
+
27
+ renderLibCard() → lib cards (version-pinned)
28
+
29
+ index.db (lib_symbols + FTS5)
30
+
31
+ agent knowledge ──→ codewalker_enrich / codewalker_note
32
+
33
+ cards (enrichment, glossary, decisions, conventions)
34
+
35
+ index.db (notes + FTS5, unified query)
36
+
37
+ coverage/*.info ──→ [coverage parser] ─┐
38
+ source files ─────→ [debt scanner] ────┤
39
+ agent review ─────→ codewalker_finding ─┤
40
+
41
+ analysis/*.md cards
42
+
43
+ index.db (analysis + FTS5, unified query)
24
44
  ```
25
45
 
26
46
  - **Cards are the source of truth** — markdown in `~/.pi/projects/<id>/codewalker/entries/`.
27
47
  - **SQLite + FTS5 is a disposable index** — rebuildable from cards at any time.
28
48
  - **ctags primary, regex fallback** — ctags used when available, regex for TS/JS/Py/Go.
49
+ - **Library layer** — extracts API surface from `node_modules` `.d.ts` files (version-pinned).
50
+ - **Semantic + bridge layer** — agent-driven enrichment, glossary terms, and decision notes.
51
+ - **Analysis layer** — mechanical coverage and debt scanning + agent-driven best-practice review.
29
52
  - **Git-anchored** — stale index detected per query.
53
+ - **Report, don't gate** — all analysis findings are advisory cards, never a build failure.
30
54
 
31
55
  ## Commands
32
56
 
@@ -35,10 +59,26 @@ source files ──→ [ctags / regex] ──→ Symbol[]
35
59
  | `/codewalker scan` | Full (re)build — walks project tree, extracts symbols, writes cards, populates DB |
36
60
  | `/codewalker sync` | Git-anchored incremental — reindexes only changed files |
37
61
  | `/codewalker query <text>` | Search the index (compact results) |
38
-
39
- ## Tool
40
-
41
- The model can call `codewalker_query` directly. Returns `content` (compact text) + `details` (full rows).
62
+ | `/codewalker enrich <path>` | Select unenriched symbols under `<path>` and write summaries |
63
+ | `/codewalker analyze [path]` | Mechanical coverage + debt analysis (reads lcov.info/coverage-final.json if present) |
64
+ | `/codewalker review <path>` | Agent-driven best-practice review against conventions/decisions (capped at 25 files) |
65
+ | `/codewalker findings [query]` | Search analysis findings with optional `--kind=coverage|debt|practice` filter |
66
+ | `/codewalker conventions [query]` | Search coding conventions |
67
+ | `/codewalker glossary [query]` | Search glossary terms |
68
+ | `/codewalker decisions [query]` | Search decision notes |
69
+ | `/codewalker libs [--dev]` | Index all direct dependencies from node_modules |
70
+ | `/codewalker lib <pkg> [query]` | Search a specific library's API symbols |
71
+
72
+ ## Tools
73
+
74
+ The model can call:
75
+
76
+ | Tool | Description |
77
+ |------|-------------|
78
+ | `codewalker_query` | Search code symbols, libraries, notes, and analysis findings with FTS5 |
79
+ | `codewalker_enrich` | Write a one-line semantic summary back to a symbol's card + DB |
80
+ | `codewalker_note` | Write a glossary term, decision note, or coding convention |
81
+ | `codewalker_finding` | Write a coverage, debt, or best-practice analysis finding |
42
82
 
43
83
  ## Install
44
84
 
@@ -56,7 +96,7 @@ pi -e ./node_modules/@aprimediet/codewalker/index.ts
56
96
 
57
97
  ```bash
58
98
  npm install
59
- npm test # vitest — 86+ tests across 12 test files
99
+ npm test # vitest — 295+ tests across 25 test files
60
100
  npm run test:watch # watch mode
61
101
  ```
62
102
 
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@aprimediet/codewalker",
3
- "version": "1.2.0",
3
+ "version": "1.4.0",
4
4
  "type": "module",
5
5
  "description": "Queryable, token-economical project & code index for the pi coding agent.",
6
6
  "keywords": ["pi-package"],
@@ -1,7 +1,14 @@
1
1
  You have access to the codewalker code index for this project. Before reading or grepping
2
2
  files to find symbols (functions, consts, classes, types), use `codewalker_query` to
3
3
  look them up. The query returns compact facts — name, kind, file:line, and a one-line
4
- summary.
4
+ summary. Use `source='all'` to also surface glossary terms, decision notes, and analysis
5
+ findings.
5
6
 
6
7
  - If the index is stale (shown in the result), run `/codewalker sync`.
7
8
  - For a full index, run `/codewalker scan`.
9
+ - After reading an unfamiliar symbol, call `codewalker_enrich` to cache a summary.
10
+ - When you discover a design decision or domain term, write it with `codewalker_note`.
11
+ - When you learn a coding convention, record it with `codewalker_note type=convention`.
12
+ - Run `/codewalker analyze` for a health snapshot (coverage gaps, debt markers).
13
+ - Use `/codewalker review <path>` to review files against conventions and decisions,
14
+ writing findings with `codewalker_finding`. Findings are advisory — report, don't gate.
@@ -1,43 +1,180 @@
1
- # Codewalker — Queryable Code Index
1
+ ---
2
+ name: codewalker
3
+ description: >
4
+ Queryable code index + knowledge base for understanding codebases. Finds symbol
5
+ definitions, function summaries, const/class/type/interface lookups, library API
6
+ searches, glossary terms, and design decisions — before blindly grepping or reading
7
+ files. Use FIRST when exploring unfamiliar code.
8
+ ---
9
+
10
+ # Codewalker — Queryable Code Index + Knowledge Base
2
11
 
3
12
  **Use this skill when:** you need to understand a codebase — find where a symbol is defined,
4
- understand what a function does, or check if a const/class/type exists — BEFORE editing
5
- files or grepping through the repo.
13
+ understand what a function does, check if a const/class/type/interface exists, search
14
+ library APIs, look up glossary terms or past design decisions — **before** blindly
15
+ grepping or reading files.
6
16
 
7
- ## Workflow
17
+ Codewalker is a token-economical project index that surfaces compact facts instead of
18
+ pulling whole files into your LLM context.
8
19
 
9
- 1. **Always query first** before editing unfamiliar code:
10
- ```
11
- /codewalker query "<symbol-name or concept>"
12
- ```
13
- This returns compact facts: `name · kind · file:line · one-line summary`.
20
+ ---
14
21
 
15
- 2. **If the query returns relevant hits**, use `file:line` to read only the span you need
16
- instead of grepping the whole repo.
22
+ ## Tools (agent-facing)
17
23
 
18
- 3. **If the query returns no hits**, the index may be stale or missing. Run:
19
- ```
20
- /codewalker scan
21
- ```
22
- (first run) or `/codewalker sync` (incremental update).
24
+ ### `codewalker_query` Search the index
23
25
 
24
- 4. **When results include a staleness warning** (`indexed @abc, HEAD @def`), run
25
- `/codewalker sync` before trusting the results.
26
+ Use this **first** before reading or grepping files. Returns one-line-per-hit:
27
+ `name · kind · file:line · one-line-summary`
26
28
 
27
- ## Why
29
+ Parameters:
30
+ - `query` — symbol name or concept keywords
31
+ - `kind` — optional filter: `function|const|class|type|method|enum|interface|glossary|decision`
32
+ - `limit` — max hits (default 10)
33
+ - `source` — scope: `code` (default, source symbols), `libs` (library APIs), `notes` (glossary + decisions), `all` (everything)
34
+
35
+ ### `codewalker_enrich` — Write a semantic summary
36
+
37
+ Call this **after** reading a symbol's source span. Caches a one-line (≤120 char)
38
+ plain-English summary so future queries surface meaning, not just names.
39
+
40
+ Parameters:
41
+ - `card` — `card_path` of the symbol (from the enrich worklist or query results)
42
+ - `summary` — one-line description of what it does
43
+
44
+ ### `codewalker_note` — Save domain knowledge
45
+
46
+ Write a glossary term, design decision, or coding convention. Persists as a markdown
47
+ card + FTS index so future queries find it.
28
48
 
29
- The index is built out-of-band (mechanical ctags/regex pass) so you never pay the file-scan
30
- cost inside the LLM context. Queries return compact, ranked facts — tens of tokens instead
31
- of thousands.
49
+ Parameters:
50
+ - `type` `glossary` | `decision` | `convention`
51
+ - `title` — glossary term, decision title, or convention name
52
+ - `body` — definition, rationale, or convention description
53
+ - `tags` — optional comma-separated tags
54
+ - `related` — optional comma-separated symbol names or `file:line` refs
32
55
 
33
- ## Commands
56
+ ### `codewalker_finding` — Write an analysis finding
57
+
58
+ Write a coverage, debt, or best-practice finding. Persists as a markdown card under
59
+ `entries/analysis/<kind>/` + FTS index. Called by the agent during a review pass.
60
+
61
+ Parameters:
62
+ - `kind` — `coverage` | `debt` | `practice`
63
+ - `title` — short finding label
64
+ - `file` — optional file or `file:line` the finding is about
65
+ - `severity` — `info` | `warn` | `high` (default `info`)
66
+ - `body` — finding detail + why it matters, grounded in conventions/decisions
67
+ - `metric` — optional metric string, e.g. `'42%'`, `'fn length 180'`
68
+ - `related` — optional comma-separated symbol names or `file:line` refs
69
+
70
+ ---
71
+
72
+ ## Commands (human-facing)
34
73
 
35
74
  | Command | Purpose |
36
75
  |---------|---------|
37
- | `/codewalker scan` | Full (re)build of the code index |
38
- | `/codewalker sync` | Git-anchored incremental update |
39
- | `/codewalker query <text>` | Search symbols by name or keyword |
76
+ | `/codewalker scan` | Full (re)build of the code index from scratch |
77
+ | `/codewalker sync` | Git-anchored incremental update (fast) |
78
+ | `/codewalker query <text>` | Search code symbols by name or keyword |
79
+ | `/codewalker enrich <path> [--max=N]` | List unenriched symbols under `path` for annotation |
80
+ | `/codewalker analyze [path]` | Mechanical coverage + debt analysis (reads lcov.info if present) |
81
+ | `/codewalker review <path> [--max=N]` | Agent-driven best-practice review against conventions (capped 25 files) |
82
+ | `/codewalker findings [query] [--kind=KIND]` | Search analysis findings |
83
+ | `/codewalker conventions [query]` | Search coding conventions |
84
+ | `/codewalker glossary [query]` | Search glossary terms |
85
+ | `/codewalker decisions [query]` | Search decision notes |
86
+ | `/codewalker libs [--dev]` | Index all direct npm dependencies (--dev includes devDeps) |
87
+ | `/codewalker lib <pkg> [query]` | Search a specific library's exported API symbols |
88
+ | `/codewalker help` | Show this help |
89
+
90
+ ---
91
+
92
+ ## Workflow
93
+
94
+ ### 1. Before editing unfamiliar code
95
+ ```
96
+ /codewalker query "<symbol-name>"
97
+ ```
98
+ Or call `codewalker_query` directly from agent conversation.
99
+ If hits are relevant, use the `file:line` to read only the span you need.
100
+
101
+ ### 2. If the index is stale
102
+ Results include a staleness warning like:
103
+ `⚠ Index is stale (3 file(s) changed since last index): indexed @abc1234, HEAD @def5678`
104
+ → Run `/codewalker sync` first, then query again.
105
+
106
+ ### 3. First time in a project
107
+ ```
108
+ /codewalker scan
109
+ ```
110
+ This does a full build (ctags primary, regex fallback) and sets up the SQLite+FTS5 database.
111
+
112
+ ### 4. Annotating symbols for future clarity
113
+ After reading a symbol you didn't understand, call `codewalker_enrich` with a summary.
114
+ This builds up the codebase knowledge map over time.
115
+
116
+ ### 5. Capturing domain knowledge
117
+ When you discover a project-specific concept or learn why a decision was made:
118
+ ```
119
+ codewalker_note(type="glossary", title="term", body="definition")
120
+ codewalker_note(type="decision", title="why X", body="rationale")
121
+ ```
122
+ These become searchable via `codewalker_query` with `source='notes'` or `source='all'`.
123
+
124
+ ### 6. Capturing coding conventions
125
+ When you learn a project-specific coding convention, record it so reviews can measure
126
+ against it:
127
+ ```
128
+ codewalker_note(type="convention", title="Use functional components",
129
+ body="All React components must be pure functions, not classes.")
130
+ ```
131
+ Then search them with `/codewalker conventions [query]`.
132
+
133
+ ### 7. Running a health snapshot
134
+ ```
135
+ /codewalker analyze
136
+ ```
137
+ This parses `coverage/lcov.info` (if present) and scans source files for
138
+ TODO/FIXME/HACK markers, `@ts-ignore`, oversized files, and long functions.
139
+ Results are queryable via `/codewalker findings [query]` or
140
+ `codewalker_query source='analysis'`.
141
+
142
+ ### 8. Reviewing a subsystem against conventions
143
+ ```
144
+ /codewalker review src/auth
145
+ ```
146
+ This selects files under `src/auth` (capped at 25) and produces a worklist for the
147
+ agent to review each file against the project's conventions and decisions. The agent
148
+ calls `codewalker_finding` to write findings back.
149
+
150
+ ### 9. Exploring library APIs
151
+ ```
152
+ /codewalker libs # index dependencies
153
+ /codewalker lib express # search express exports
154
+ /codewalker lib lodash get # search lodash for 'get'
155
+ ```
156
+
157
+ ---
158
+
159
+ ## Why
160
+
161
+ The index is built out-of-band (mechanical ctags/regex pass) so you never pay the
162
+ file-scan cost inside the LLM context. Queries return compact, ranked facts — tens of
163
+ tokens instead of thousands. The note system (glossary + decisions) captures conceptual
164
+ knowledge your future self will thank you for.
165
+
166
+ ---
40
167
 
41
- ## Tool
168
+ ## Details
42
169
 
43
- The model can also call `codewalker_query` directly (same behavior as the command).
170
+ - **Staleness detection**: every query result includes git-anchored staleness info
171
+ comparing the indexed commit against HEAD
172
+ - **FTS5 ranking**: results use bm25 relevance scoring; `code` → `libs` → `notes` order
173
+ when using `source='all'`
174
+ - **Cards as source of truth**: every symbol and note is stored as a markdown card file.
175
+ The DB is rebuilt from cards on `scan` — cards are the durable artifact
176
+ - **Source filter**: use `source='libs'` to search only library APIs, `source='notes'`
177
+ for glossary/decisions, `source='analysis'` for findings, `source='all'` for everything
178
+ at once
179
+ - **Report, don't gate**: analysis findings are advisory cards and FTS rows, never a CI
180
+ gate or build failure
@@ -0,0 +1,214 @@
1
+ import { describe, it, expect, beforeEach, afterEach } from 'vitest';
2
+ import * as fs from 'node:fs';
3
+ import * as path from 'node:path';
4
+ import * as os from 'node:os';
5
+ import { runAnalyze, rebuildAnalysisDbFromCards } from './analyzer.ts';
6
+ import { openDb, searchFindings } from '../db.ts';
7
+
8
+ describe('analyzer.ts', () => {
9
+ let tmpDir: string;
10
+ let projectRoot: string;
11
+ let analysisDir: string;
12
+ let dbPath: string;
13
+
14
+ beforeEach(() => {
15
+ tmpDir = fs.mkdtempSync(path.join(os.tmpdir(), 'cw-analyze-'));
16
+ projectRoot = path.join(tmpDir, 'project');
17
+ analysisDir = path.join(tmpDir, 'codewalker', 'entries', 'analysis');
18
+ const dbDir = path.join(tmpDir, 'codewalker');
19
+ dbPath = path.join(dbDir, 'index.db');
20
+ fs.mkdirSync(analysisDir, { recursive: true });
21
+ fs.mkdirSync(dbDir, { recursive: true });
22
+ });
23
+
24
+ afterEach(() => {
25
+ fs.rmSync(tmpDir, { recursive: true, force: true });
26
+ });
27
+
28
+ it('runAnalyze with no coverage file still produces debt findings from source scan', () => {
29
+ // Create a source file with debt markers
30
+ const srcDir = path.join(projectRoot, 'src');
31
+ fs.mkdirSync(srcDir, { recursive: true });
32
+ fs.writeFileSync(
33
+ path.join(srcDir, 'a.ts'),
34
+ '// normal line\nconst x = 1;\n// TODO: fix this\nfunction y() { return x; }\n',
35
+ 'utf-8'
36
+ );
37
+
38
+ runAnalyze({
39
+ projectRoot,
40
+ analysisDir,
41
+ dbPath,
42
+ });
43
+
44
+ // Should have debt findings
45
+ const db = openDb(dbPath);
46
+ const findings = searchFindings(db, '');
47
+ expect(findings.length).toBeGreaterThan(0);
48
+
49
+ // At least one should be a debt finding
50
+ const debtFindings = findings.filter(f => f.finding_kind === 'debt');
51
+ expect(debtFindings.length).toBeGreaterThan(0);
52
+
53
+ // Cards should be written
54
+ const debtDir = path.join(analysisDir, 'debt');
55
+ expect(fs.existsSync(debtDir)).toBe(true);
56
+ const cardFiles = fs.readdirSync(debtDir).filter(f => f.endsWith('.md'));
57
+ expect(cardFiles.length).toBeGreaterThan(0);
58
+
59
+ db.close();
60
+ });
61
+
62
+ it('runAnalyze parses coverage/lcov.info if present', () => {
63
+ const srcDir = path.join(projectRoot, 'src');
64
+ fs.mkdirSync(srcDir, { recursive: true });
65
+ fs.writeFileSync(path.join(srcDir, 'token.ts'), 'line1\nline2\nline3\n', 'utf-8');
66
+
67
+ // Write coverage file
68
+ const coverageDir = path.join(projectRoot, 'coverage');
69
+ fs.mkdirSync(coverageDir, { recursive: true });
70
+ fs.writeFileSync(
71
+ path.join(coverageDir, 'lcov.info'),
72
+ 'SF:src/token.ts\nDA:1,1\nDA:2,0\nDA:3,1\nLF:3\nLH:2\nend_of_record\n',
73
+ 'utf-8'
74
+ );
75
+
76
+ runAnalyze({
77
+ projectRoot,
78
+ analysisDir,
79
+ dbPath,
80
+ });
81
+
82
+ // Should have coverage findings
83
+ const db = openDb(dbPath);
84
+ const findings = searchFindings(db, '');
85
+ const coverageFindings = findings.filter(f => f.finding_kind === 'coverage');
86
+ expect(coverageFindings.length).toBeGreaterThan(0);
87
+
88
+ // Card files should be written
89
+ const coverageDir2 = path.join(analysisDir, 'coverage');
90
+ expect(fs.existsSync(coverageDir2)).toBe(true);
91
+
92
+ db.close();
93
+ });
94
+
95
+ it('runAnalyze parses coverage/coverage-final.json if lcov.info is absent', () => {
96
+ const srcDir = path.join(projectRoot, 'src');
97
+ fs.mkdirSync(srcDir, { recursive: true });
98
+ fs.writeFileSync(path.join(srcDir, 'token.ts'), 'line1\nline2\n', 'utf-8');
99
+
100
+ // Write coverage-final.json
101
+ const coverageDir = path.join(projectRoot, 'coverage');
102
+ fs.mkdirSync(coverageDir, { recursive: true });
103
+ fs.writeFileSync(
104
+ path.join(coverageDir, 'coverage-final.json'),
105
+ JSON.stringify({
106
+ 'src/token.ts': {
107
+ path: 'src/token.ts',
108
+ statementMap: { '0': { start: { line: 1, column: 0 }, end: { line: 1, column: 5 } }, '1': { start: { line: 2, column: 0 }, end: { line: 2, column: 5 } } },
109
+ fnMap: {}, branchMap: {},
110
+ s: { '0': 1, '1': 0 },
111
+ f: {}, b: {},
112
+ },
113
+ }),
114
+ 'utf-8'
115
+ );
116
+
117
+ runAnalyze({
118
+ projectRoot,
119
+ analysisDir,
120
+ dbPath,
121
+ });
122
+
123
+ const db = openDb(dbPath);
124
+ const findings = searchFindings(db, '');
125
+ const coverageFindings = findings.filter(f => f.finding_kind === 'coverage');
126
+ expect(coverageFindings.length).toBeGreaterThan(0);
127
+ db.close();
128
+ });
129
+
130
+ it('re-running analyze on same project does not duplicate findings', () => {
131
+ const srcDir = path.join(projectRoot, 'src');
132
+ fs.mkdirSync(srcDir, { recursive: true });
133
+ fs.writeFileSync(path.join(srcDir, 'a.ts'), '// TODO: once\n', 'utf-8');
134
+
135
+ runAnalyze({ projectRoot, analysisDir, dbPath });
136
+ runAnalyze({ projectRoot, analysisDir, dbPath }); // second run
137
+
138
+ const db = openDb(dbPath);
139
+ const findings = searchFindings(db, '');
140
+ // All findings should be unique — no duplicates per (finding_kind, file_path, title)
141
+ const titles = findings.map(f => f.name);
142
+ const uniqueTitles = new Set(titles);
143
+ expect(titles.length).toBe(uniqueTitles.size);
144
+ db.close();
145
+ });
146
+
147
+ it('rebuildAnalysisDbFromCards reconstructs the DB from card files alone', () => {
148
+ // First run to create cards
149
+ const srcDir = path.join(projectRoot, 'src');
150
+ fs.mkdirSync(srcDir, { recursive: true });
151
+ fs.writeFileSync(path.join(srcDir, 'a.ts'), '// TODO: rebuild this\nconst x = 1;\n', 'utf-8');
152
+
153
+ runAnalyze({ projectRoot, analysisDir, dbPath });
154
+
155
+ // Verify there are cards on disk
156
+ const debtDir = path.join(analysisDir, 'debt');
157
+ const cardFiles = fs.existsSync(debtDir) ? fs.readdirSync(debtDir).filter(f => f.endsWith('.md')) : [];
158
+ expect(cardFiles.length).toBeGreaterThan(0);
159
+
160
+ // Destroy DB and rebuild
161
+ const db = openDb(dbPath);
162
+ db.prepare('DELETE FROM analysis').run();
163
+ const beforeRebuild = searchFindings(db, '');
164
+ expect(beforeRebuild).toHaveLength(0);
165
+ db.close();
166
+
167
+ // Rebuild from cards
168
+ rebuildAnalysisDbFromCards(dbPath, analysisDir);
169
+
170
+ // Verify findings are back
171
+ const db2 = openDb(dbPath);
172
+ const afterRebuild = searchFindings(db2, '');
173
+ expect(afterRebuild.length).toBeGreaterThan(0);
174
+ expect(afterRebuild[0]!.finding_kind).toBe('debt');
175
+ db2.close();
176
+ });
177
+
178
+ it('runAnalyze reports when no coverage data is found without erroring', () => {
179
+ // No coverage files, no source files
180
+ const srcDir = path.join(projectRoot, 'src');
181
+ fs.mkdirSync(srcDir, { recursive: true });
182
+
183
+ // Should not throw
184
+ expect(() => runAnalyze({ projectRoot, analysisDir, dbPath })).not.toThrow();
185
+
186
+ // With no source files either, findings should be empty
187
+ const db = openDb(dbPath);
188
+ const findings = searchFindings(db, '');
189
+ expect(findings).toHaveLength(0);
190
+ db.close();
191
+ });
192
+
193
+ it('runAnalyze respects a custom path filter for debt scanning', () => {
194
+ const srcDir = path.join(projectRoot, 'src');
195
+ fs.mkdirSync(srcDir, { recursive: true });
196
+ fs.writeFileSync(path.join(srcDir, 'a.ts'), '// TODO: file a\n', 'utf-8');
197
+ fs.writeFileSync(path.join(srcDir, 'b.ts'), '// TODO: file b\n', 'utf-8');
198
+
199
+ runAnalyze({
200
+ projectRoot,
201
+ analysisDir,
202
+ dbPath,
203
+ pathFilter: 'src/a.ts',
204
+ });
205
+
206
+ const db = openDb(dbPath);
207
+ const findings = searchFindings(db, '');
208
+ const aFindings = findings.filter(f => f.file_path?.endsWith('a.ts'));
209
+ const bFindings = findings.filter(f => f.file_path?.endsWith('b.ts'));
210
+ expect(aFindings.length).toBeGreaterThan(0);
211
+ expect(bFindings).toHaveLength(0);
212
+ db.close();
213
+ });
214
+ });