@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/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
|
-
|
|
40
|
-
|
|
41
|
-
|
|
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 —
|
|
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
package/prompts/codewalker.md
CHANGED
|
@@ -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
|
-
|
|
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,
|
|
5
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
16
|
-
instead of grepping the whole repo.
|
|
22
|
+
## Tools (agent-facing)
|
|
17
23
|
|
|
18
|
-
|
|
19
|
-
```
|
|
20
|
-
/codewalker scan
|
|
21
|
-
```
|
|
22
|
-
(first run) or `/codewalker sync` (incremental update).
|
|
24
|
+
### `codewalker_query` — Search the index
|
|
23
25
|
|
|
24
|
-
|
|
25
|
-
|
|
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
|
-
|
|
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
|
-
|
|
30
|
-
|
|
31
|
-
|
|
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
|
-
|
|
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
|
-
##
|
|
168
|
+
## Details
|
|
42
169
|
|
|
43
|
-
|
|
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
|
+
});
|