projscan 0.9.0 → 0.9.2
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 +72 -72
- package/dist/analyzers/deadCodeCheck.d.ts +2 -2
- package/dist/analyzers/deadCodeCheck.js +4 -4
- package/dist/analyzers/unusedDependencyCheck.js +1 -1
- package/dist/cli/index.js +11 -11
- package/dist/core/ast.d.ts +1 -1
- package/dist/core/ast.js +2 -2
- package/dist/core/auditRunner.d.ts +1 -1
- package/dist/core/auditRunner.js +6 -6
- package/dist/core/coverageJoin.d.ts +1 -1
- package/dist/core/coverageJoin.js +1 -1
- package/dist/core/coverageParser.js +3 -3
- package/dist/core/dependencyAnalyzer.js +6 -6
- package/dist/core/embeddings.d.ts +1 -1
- package/dist/core/embeddings.js +2 -2
- package/dist/core/fileInspector.js +6 -6
- package/dist/core/hotspotAnalyzer.js +2 -2
- package/dist/core/importGraph.d.ts +1 -1
- package/dist/core/importGraph.js +1 -1
- package/dist/core/indexCache.d.ts +2 -2
- package/dist/core/indexCache.js +4 -4
- package/dist/core/outdatedDetector.d.ts +1 -1
- package/dist/core/outdatedDetector.js +1 -1
- package/dist/core/searchIndex.js +2 -2
- package/dist/core/semanticSearch.d.ts +1 -1
- package/dist/core/semanticSearch.js +2 -2
- package/dist/core/upgradePreview.d.ts +12 -0
- package/dist/core/upgradePreview.js +55 -3
- package/dist/core/upgradePreview.js.map +1 -1
- package/dist/fixes/prettierFix.js +1 -1
- package/dist/fixes/testFix.js +1 -1
- package/dist/index.d.ts +1 -1
- package/dist/index.js +1 -1
- package/dist/index.js.map +1 -1
- package/dist/mcp/chunker.d.ts +1 -1
- package/dist/mcp/chunker.js +1 -1
- package/dist/mcp/pagination.d.ts +2 -2
- package/dist/mcp/pagination.js +2 -2
- package/dist/mcp/progress.d.ts +1 -1
- package/dist/mcp/prompts.js +3 -3
- package/dist/mcp/server.js +1 -1
- package/dist/mcp/tokenBudget.d.ts +1 -1
- package/dist/mcp/tokenBudget.js +2 -2
- package/dist/mcp/tools.js +8 -8
- package/dist/reporters/consoleReporter.js +11 -11
- package/dist/reporters/markdownReporter.js +14 -14
- package/dist/reporters/sarifReporter.js +1 -1
- package/dist/utils/banner.d.ts +3 -3
- package/dist/utils/banner.js +9 -9
- package/dist/utils/config.js +1 -1
- package/dist/utils/packageJsonLocator.d.ts +1 -1
- package/dist/utils/packageJsonLocator.js +1 -1
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -6,7 +6,7 @@
|
|
|
6
6
|
[](https://github.com/abhiyoheswaran1/projscan/blob/main/LICENSE)
|
|
7
7
|
[](https://nodejs.org)
|
|
8
8
|
|
|
9
|
-
**Agent-first code intelligence.** An MCP server that lets AI coding agents (Claude Code, Cursor, Windsurf) query your codebase
|
|
9
|
+
**Agent-first code intelligence.** An MCP server that lets AI coding agents (Claude Code, Cursor, Windsurf) query your codebase - with a CLI for humans on the side.
|
|
10
10
|
|
|
11
11
|
[AI Agent Quick Start](#ai-agent-integration-mcp) · [CLI Quick Start](#quick-start) · [Commands](#commands) · [Full Guide](docs/GUIDE.md) · [Roadmap](docs/ROADMAP.md)
|
|
12
12
|
|
|
@@ -18,9 +18,9 @@
|
|
|
18
18
|
|
|
19
19
|
## Why?
|
|
20
20
|
|
|
21
|
-
AI coding agents are becoming the primary interface to code. Today, when you ask your agent *"which files implement auth?"* or *"what breaks if I bump React from 18 to 19?"*
|
|
21
|
+
AI coding agents are becoming the primary interface to code. Today, when you ask your agent *"which files implement auth?"* or *"what breaks if I bump React from 18 to 19?"* - it either guesses from names, or it shells out to grep and reads raw output not built for it.
|
|
22
22
|
|
|
23
|
-
**projscan is the first code-intelligence tool built for agents, not for humans.** Your agent gets a fast, AST-accurate, context-budget-aware view of your codebase through 13 structured MCP tools. It can query the import graph, find symbol definitions, preview upgrades, rank hotspots
|
|
23
|
+
**projscan is the first code-intelligence tool built for agents, not for humans.** Your agent gets a fast, AST-accurate, context-budget-aware view of your codebase through 13 structured MCP tools. It can query the import graph, find symbol definitions, preview upgrades, rank hotspots - without loading the file tree into its context.
|
|
24
24
|
|
|
25
25
|
Humans get the same thing through the CLI.
|
|
26
26
|
|
|
@@ -61,7 +61,7 @@ projscan # Full project analysis
|
|
|
61
61
|
projscan doctor # Health check
|
|
62
62
|
projscan hotspots # Rank files by risk (churn × complexity × issues × ownership)
|
|
63
63
|
projscan search <query> # BM25-ranked search (content + symbols + path)
|
|
64
|
-
projscan file <path> # Drill into a file
|
|
64
|
+
projscan file <path> # Drill into a file - purpose, risk, ownership, issues
|
|
65
65
|
projscan fix # Auto-fix detected issues
|
|
66
66
|
projscan ci # CI health gate (exits 1 on low score)
|
|
67
67
|
projscan ci --changed-only # Gate only on this PR's diff
|
|
@@ -69,7 +69,7 @@ projscan ci --format sarif # SARIF 2.1.0 for GitHub Code Scanning
|
|
|
69
69
|
projscan outdated # Declared-vs-installed drift (offline)
|
|
70
70
|
projscan audit # npm audit, normalized + SARIF-ready
|
|
71
71
|
projscan upgrade <pkg> # Preview upgrade impact (local CHANGELOG + importers)
|
|
72
|
-
projscan coverage # Coverage × hotspots
|
|
72
|
+
projscan coverage # Coverage × hotspots - scariest untested files
|
|
73
73
|
projscan diff # Compare health + hotspot trends against a baseline
|
|
74
74
|
projscan diagram # Architecture visualization
|
|
75
75
|
projscan structure # Directory tree
|
|
@@ -84,22 +84,22 @@ For a comprehensive walkthrough, see the **[Full Guide](docs/GUIDE.md)**.
|
|
|
84
84
|
|
|
85
85
|
| Command | Description |
|
|
86
86
|
|---------|-------------|
|
|
87
|
-
| `projscan analyze` | Full analysis
|
|
88
|
-
| `projscan doctor` | Health check
|
|
89
|
-
| `projscan hotspots` | Rank files by risk
|
|
90
|
-
| `projscan search <query>` | **BM25-ranked search**
|
|
91
|
-
| `projscan file <path>` | Drill into a file
|
|
87
|
+
| `projscan analyze` | Full analysis - languages, frameworks, dependencies, issues |
|
|
88
|
+
| `projscan doctor` | Health check - missing tooling, architecture smells, security risks |
|
|
89
|
+
| `projscan hotspots` | Rank files by risk - churn × complexity × issues × ownership |
|
|
90
|
+
| `projscan search <query>` | **BM25-ranked search** - content + symbols + path, with excerpts |
|
|
91
|
+
| `projscan file <path>` | Drill into a file - purpose, risk, ownership, related issues |
|
|
92
92
|
| `projscan fix` | Auto-fix issues (ESLint, Prettier, Vitest, .editorconfig) |
|
|
93
|
-
| `projscan ci` | CI health gate
|
|
93
|
+
| `projscan ci` | CI health gate - SARIF output, `--changed-only` PR-diff mode, exits 1 if score below threshold |
|
|
94
94
|
| `projscan diff` | Compare current health **and hotspot trends** against a baseline |
|
|
95
95
|
| `projscan explain <file>` | Explain a file's purpose, imports, exports, and issues |
|
|
96
96
|
| `projscan diagram` | ASCII architecture diagram of your project |
|
|
97
97
|
| `projscan structure` | Directory tree with file counts |
|
|
98
|
-
| `projscan dependencies` | Dependency analysis
|
|
98
|
+
| `projscan dependencies` | Dependency analysis - counts, risks, recommendations |
|
|
99
99
|
| `projscan outdated` | Declared-vs-installed drift check (offline) |
|
|
100
|
-
| `projscan audit` | `npm audit`-powered vulnerability report
|
|
101
|
-
| `projscan upgrade <pkg>` | Preview upgrade impact
|
|
102
|
-
| `projscan coverage` | **Coverage × hotspots
|
|
100
|
+
| `projscan audit` | `npm audit`-powered vulnerability report - SARIF-ready for Code Scanning |
|
|
101
|
+
| `projscan upgrade <pkg>` | Preview upgrade impact - local CHANGELOG + importer list, offline |
|
|
102
|
+
| `projscan coverage` | **Coverage × hotspots - rank the scariest untested files** |
|
|
103
103
|
| `projscan badge` | Generate a health score badge for your README |
|
|
104
104
|
| `projscan mcp` | Run as an MCP server for AI coding agents (Claude Code, Cursor, …) |
|
|
105
105
|
|
|
@@ -112,31 +112,31 @@ projscan --help
|
|
|
112
112
|
### Command Screenshots
|
|
113
113
|
|
|
114
114
|
<details>
|
|
115
|
-
<summary><strong>projscan structure</strong>
|
|
115
|
+
<summary><strong>projscan structure</strong> - Directory tree with file counts</summary>
|
|
116
116
|
|
|
117
117
|
<img src="docs/npx%20projscan%20structure.png" alt="npx projscan structure" width="700">
|
|
118
118
|
</details>
|
|
119
119
|
|
|
120
120
|
<details>
|
|
121
|
-
<summary><strong>projscan diagram</strong>
|
|
121
|
+
<summary><strong>projscan diagram</strong> - Architecture visualization</summary>
|
|
122
122
|
|
|
123
123
|
<img src="docs/npx%20projscan%20diagram.png" alt="npx projscan diagram" width="700">
|
|
124
124
|
</details>
|
|
125
125
|
|
|
126
126
|
<details>
|
|
127
|
-
<summary><strong>projscan dependencies</strong>
|
|
127
|
+
<summary><strong>projscan dependencies</strong> - Dependency analysis</summary>
|
|
128
128
|
|
|
129
129
|
<img src="docs/npx%20projscan%20dependencies.png" alt="npx projscan dependencies" width="700">
|
|
130
130
|
</details>
|
|
131
131
|
|
|
132
132
|
<details>
|
|
133
|
-
<summary><strong>projscan explain</strong>
|
|
133
|
+
<summary><strong>projscan explain</strong> - File explanation</summary>
|
|
134
134
|
|
|
135
135
|
<img src="docs/npx%20projscan%20explain.png" alt="npx projscan explain" width="700">
|
|
136
136
|
</details>
|
|
137
137
|
|
|
138
138
|
<details>
|
|
139
|
-
<summary><strong>projscan badge</strong>
|
|
139
|
+
<summary><strong>projscan badge</strong> - Health badge generation</summary>
|
|
140
140
|
|
|
141
141
|
<img src="docs/npx%20projscan%20badge.png" alt="npx projscan badge" width="700">
|
|
142
142
|
</details>
|
|
@@ -172,11 +172,11 @@ Every `projscan doctor` run calculates a health score (0–100) and letter grade
|
|
|
172
172
|
|
|
173
173
|
| Grade | Score | Meaning |
|
|
174
174
|
|-------|-------|---------|
|
|
175
|
-
| A | 90–100 | Excellent
|
|
176
|
-
| B | 80–89 | Good
|
|
177
|
-
| C | 70–79 | Fair
|
|
178
|
-
| D | 60–69 | Poor
|
|
179
|
-
| F | < 60 | Critical
|
|
175
|
+
| A | 90–100 | Excellent - project follows best practices |
|
|
176
|
+
| B | 80–89 | Good - minor improvements possible |
|
|
177
|
+
| C | 70–79 | Fair - several issues to address |
|
|
178
|
+
| D | 60–69 | Poor - significant issues found |
|
|
179
|
+
| F | < 60 | Critical - major issues need attention |
|
|
180
180
|
|
|
181
181
|
Generate a badge for your README:
|
|
182
182
|
|
|
@@ -202,15 +202,15 @@ This outputs a [shields.io](https://shields.io) badge URL and markdown snippet y
|
|
|
202
202
|
- Excessive, deprecated, or wildcard-versioned dependencies
|
|
203
203
|
- Missing lockfile
|
|
204
204
|
- Committed `.env` files and private keys (security)
|
|
205
|
-
- Hardcoded secrets
|
|
205
|
+
- Hardcoded secrets - AWS keys, GitHub tokens, Slack tokens, generic passwords (security)
|
|
206
206
|
- Missing `.env` in `.gitignore` (security)
|
|
207
207
|
|
|
208
208
|
## Performance
|
|
209
209
|
|
|
210
210
|
- **5,000 files** analyzed in under 1.5 seconds
|
|
211
211
|
- **20,000 files** analyzed in under 3 seconds
|
|
212
|
-
- **Zero network requests**
|
|
213
|
-
- **4 runtime dependencies**
|
|
212
|
+
- **Zero network requests** - everything runs locally
|
|
213
|
+
- **4 runtime dependencies** - minimal footprint
|
|
214
214
|
|
|
215
215
|
## CI/CD Integration
|
|
216
216
|
|
|
@@ -264,7 +264,7 @@ If you'd rather not upload SARIF, [`.github/projscan-ci.yml`](.github/projscan-c
|
|
|
264
264
|
|
|
265
265
|
## Configuration (`.projscanrc`)
|
|
266
266
|
|
|
267
|
-
Drop a `.projscanrc.json` at your repo root to set defaults
|
|
267
|
+
Drop a `.projscanrc.json` at your repo root to set defaults - CLI flags always win over config. A `"projscan"` key in `package.json` and plain `.projscanrc` are also supported.
|
|
268
268
|
|
|
269
269
|
```json
|
|
270
270
|
{
|
|
@@ -284,12 +284,12 @@ Drop a `.projscanrc.json` at your repo root to set defaults — CLI flags always
|
|
|
284
284
|
|
|
285
285
|
Fields:
|
|
286
286
|
|
|
287
|
-
- `minScore`
|
|
288
|
-
- `baseRef`
|
|
289
|
-
- `ignore`
|
|
290
|
-
- `disableRules`
|
|
291
|
-
- `severityOverrides`
|
|
292
|
-
- `hotspots.limit` / `hotspots.since`
|
|
287
|
+
- `minScore` - default `ci` threshold (0–100)
|
|
288
|
+
- `baseRef` - default base ref for `--changed-only`
|
|
289
|
+
- `ignore` - extra glob patterns added to the built-in ignore list
|
|
290
|
+
- `disableRules` - silence rules by id; supports wildcard `prefix-*`
|
|
291
|
+
- `severityOverrides` - remap a rule's severity (`info` / `warning` / `error`)
|
|
292
|
+
- `hotspots.limit` / `hotspots.since` - defaults for the `hotspots` command
|
|
293
293
|
|
|
294
294
|
## Tracking Health Over Time
|
|
295
295
|
|
|
@@ -304,9 +304,9 @@ projscan diff --format markdown # Markdown diff for PRs
|
|
|
304
304
|
|
|
305
305
|
<img src="docs/npx%20projscan%20diff%20--save-baseline.png" alt="npx projscan diff --save-baseline" width="700">
|
|
306
306
|
|
|
307
|
-
## Hotspots
|
|
307
|
+
## Hotspots - Where to Fix First
|
|
308
308
|
|
|
309
|
-
A flat health score doesn't tell you what to do. **`projscan hotspots`** combines `git log` churn, file complexity, open issues, recency, and **ownership** into a single risk score per file
|
|
309
|
+
A flat health score doesn't tell you what to do. **`projscan hotspots`** combines `git log` churn, file complexity, open issues, recency, and **ownership** into a single risk score per file - so you know where refactoring or review will actually pay off.
|
|
310
310
|
|
|
311
311
|
```bash
|
|
312
312
|
projscan hotspots # Top 10 hotspots
|
|
@@ -324,7 +324,7 @@ Hotspot ranking follows the classic Feathers "churn × complexity" heuristic wit
|
|
|
324
324
|
projscan file src/cli/index.ts
|
|
325
325
|
```
|
|
326
326
|
|
|
327
|
-
Combines the file's purpose, imports, exports, hotspot risk, ownership, and every open issue that references it
|
|
327
|
+
Combines the file's purpose, imports, exports, hotspot risk, ownership, and every open issue that references it - the natural follow-up to `projscan hotspots`.
|
|
328
328
|
|
|
329
329
|
### Track Trends Over Time
|
|
330
330
|
|
|
@@ -338,7 +338,7 @@ The baseline file now captures top hotspots too, so `diff` surfaces files that a
|
|
|
338
338
|
|
|
339
339
|
## Dependency Health
|
|
340
340
|
|
|
341
|
-
projscan ships three focused commands for keeping your dependency graph healthy
|
|
341
|
+
projscan ships three focused commands for keeping your dependency graph healthy - all **offline** by default, no registry calls.
|
|
342
342
|
|
|
343
343
|
```bash
|
|
344
344
|
projscan outdated # Which declared deps drift from what's installed?
|
|
@@ -351,9 +351,9 @@ projscan upgrade chalk --format markdown # Paste-ready review comment
|
|
|
351
351
|
|
|
352
352
|
### What each one tells you
|
|
353
353
|
|
|
354
|
-
- **`outdated`**
|
|
355
|
-
- **`audit`**
|
|
356
|
-
- **`upgrade <pkg>`**
|
|
354
|
+
- **`outdated`** - reads `package.json` and `node_modules/<pkg>/package.json` to classify drift (`major` / `minor` / `patch` / `same` / `unknown`). No network.
|
|
355
|
+
- **`audit`** - wraps `npm audit --json`, normalizes the output, and emits SARIF with per-finding rules anchored to `package.json`. Graceful fallback message for yarn/pnpm projects.
|
|
356
|
+
- **`upgrade <pkg>`** - reads `node_modules/<pkg>/CHANGELOG.md`, slices the section between your installed version and the previous one, flags `BREAKING CHANGE` / `deprecated` / `removed support` markers, and lists every file in your repo that imports the package. All offline.
|
|
357
357
|
|
|
358
358
|
### Unused dependencies (automatic in `doctor`)
|
|
359
359
|
|
|
@@ -361,9 +361,9 @@ projscan upgrade chalk --format markdown # Paste-ready review comment
|
|
|
361
361
|
|
|
362
362
|
Implicit-use packages (typescript, eslint/prettier plugins, `@types/*`, and anything invoked from a `package.json` script) are allowlisted. Override via `.projscanrc` → `disableRules` if projscan flags something that is used but not imported.
|
|
363
363
|
|
|
364
|
-
## Coverage × Hotspots
|
|
364
|
+
## Coverage × Hotspots - Scariest Untested Files
|
|
365
365
|
|
|
366
|
-
`projscan coverage` joins your test coverage with the hotspot ranking. A file with high churn and low coverage is where a bug is most likely to bite you
|
|
366
|
+
`projscan coverage` joins your test coverage with the hotspot ranking. A file with high churn and low coverage is where a bug is most likely to bite you - so that's where you want tests first.
|
|
367
367
|
|
|
368
368
|
```bash
|
|
369
369
|
projscan coverage # Top 30 scariest untested files
|
|
@@ -371,23 +371,23 @@ projscan coverage --format markdown # Paste into a tech-debt ticket
|
|
|
371
371
|
projscan coverage --format json # Machine-readable for dashboards
|
|
372
372
|
```
|
|
373
373
|
|
|
374
|
-
**How it decides "scariest":** `priority = riskScore × (0.3 + 0.7 × uncoveredFraction)`
|
|
374
|
+
**How it decides "scariest":** `priority = riskScore × (0.3 + 0.7 × uncoveredFraction)` - so a file with 50 risk and 10% coverage outranks a file with 50 risk and 95% coverage.
|
|
375
375
|
|
|
376
376
|
**Which coverage files are supported:**
|
|
377
377
|
|
|
378
|
-
- `coverage/lcov.info` (lcov
|
|
378
|
+
- `coverage/lcov.info` (lcov - Vitest, Jest, c8)
|
|
379
379
|
- `coverage/coverage-final.json` (Istanbul per-file detail)
|
|
380
380
|
- `coverage/coverage-summary.json` (Istanbul summary)
|
|
381
381
|
|
|
382
|
-
Coverage is also automatically joined into `projscan hotspots` when one of those files exists
|
|
382
|
+
Coverage is also automatically joined into `projscan hotspots` when one of those files exists - no flag needed. Uncovered churning files get a score bump and a `low coverage (X%)` reason tag.
|
|
383
383
|
|
|
384
384
|
### Dead-code detection (automatic in `doctor`)
|
|
385
385
|
|
|
386
|
-
`projscan doctor` now flags source files whose exports nothing imports
|
|
386
|
+
`projscan doctor` now flags source files whose exports nothing imports - dead code left over from refactors or utilities that were never wired up. Respects `package.json` public entry points (`main`, `exports`, `bin`, `types`), skips test files and barrel (`index`) files.
|
|
387
387
|
|
|
388
388
|
## AI Agent Integration (MCP)
|
|
389
389
|
|
|
390
|
-
**This is the primary way to use projscan.** `projscan mcp` starts an [MCP](https://modelcontextprotocol.io) server over stdio so AI coding agents can query your codebase with real structural accuracy
|
|
390
|
+
**This is the primary way to use projscan.** `projscan mcp` starts an [MCP](https://modelcontextprotocol.io) server over stdio so AI coding agents can query your codebase with real structural accuracy - not regex, not grep.
|
|
391
391
|
|
|
392
392
|
### Claude Code
|
|
393
393
|
|
|
@@ -419,28 +419,28 @@ claude mcp add projscan -- npx projscan mcp
|
|
|
419
419
|
|
|
420
420
|
### The 13 MCP tools
|
|
421
421
|
|
|
422
|
-
**Structural (0.6.0
|
|
423
|
-
- **`projscan_graph`**
|
|
424
|
-
- **`projscan_search`**
|
|
422
|
+
**Structural (0.6.0 - new, agent-native):**
|
|
423
|
+
- **`projscan_graph`** - query the AST-based code graph. Directions: `imports`, `exports`, `importers`, `symbol_defs`, `package_importers`. Millisecond responses on a warm cache.
|
|
424
|
+
- **`projscan_search`** - fast search across `symbols` (exported names), `files` (path substring), or `content` (source substring with line + excerpt). Replaces the temptation to shell out to grep.
|
|
425
425
|
|
|
426
426
|
**Analysis:**
|
|
427
|
-
- `projscan_analyze`
|
|
428
|
-
- `projscan_doctor`
|
|
429
|
-
- `projscan_hotspots`
|
|
430
|
-
- `projscan_file`
|
|
431
|
-
- `projscan_explain`
|
|
432
|
-
- `projscan_structure`
|
|
433
|
-
- `projscan_coverage`
|
|
427
|
+
- `projscan_analyze` - full project report
|
|
428
|
+
- `projscan_doctor` - health score + issues
|
|
429
|
+
- `projscan_hotspots` - risk-ranked files (churn × complexity × issues × ownership × coverage)
|
|
430
|
+
- `projscan_file` - per-file risk + ownership + related issues
|
|
431
|
+
- `projscan_explain` - per-file purpose, imports, exports, smells
|
|
432
|
+
- `projscan_structure` - directory tree
|
|
433
|
+
- `projscan_coverage` - scariest untested files (coverage × hotspots)
|
|
434
434
|
|
|
435
435
|
**Dependencies:**
|
|
436
|
-
- `projscan_dependencies`
|
|
437
|
-
- `projscan_outdated`
|
|
438
|
-
- `projscan_audit`
|
|
439
|
-
- `projscan_upgrade`
|
|
436
|
+
- `projscan_dependencies` - declared deps, risks
|
|
437
|
+
- `projscan_outdated` - declared-vs-installed drift (offline)
|
|
438
|
+
- `projscan_audit` - normalized `npm audit`
|
|
439
|
+
- `projscan_upgrade` - upgrade preview (CHANGELOG + importers, offline)
|
|
440
440
|
|
|
441
441
|
### Context-window budgeting
|
|
442
442
|
|
|
443
|
-
**Every MCP tool accepts an optional `max_tokens` argument.** Set it and projscan serializes the result, and
|
|
443
|
+
**Every MCP tool accepts an optional `max_tokens` argument.** Set it and projscan serializes the result, and - if over budget - truncates the largest array field record-by-record until it fits. Responses include a `_budget` sidecar when truncated so your agent knows it got a partial view.
|
|
444
444
|
|
|
445
445
|
```json
|
|
446
446
|
{ "name": "projscan_hotspots", "arguments": { "limit": 100, "max_tokens": 800 } }
|
|
@@ -448,7 +448,7 @@ claude mcp add projscan -- npx projscan mcp
|
|
|
448
448
|
|
|
449
449
|
### Semantic search (0.9.0+, opt-in)
|
|
450
450
|
|
|
451
|
-
projscan ships with BM25-ranked lexical search by default. To unlock **true semantic search**
|
|
451
|
+
projscan ships with BM25-ranked lexical search by default. To unlock **true semantic search** - embeddings over file content so queries like *"which file implements auth"* hit files that don't literally contain the word "auth" - install the optional peer:
|
|
452
452
|
|
|
453
453
|
```bash
|
|
454
454
|
npm install @xenova/transformers
|
|
@@ -461,11 +461,11 @@ Or via the MCP tool:
|
|
|
461
461
|
```
|
|
462
462
|
|
|
463
463
|
Modes on `projscan_search`:
|
|
464
|
-
- `lexical` (default)
|
|
465
|
-
- `semantic`
|
|
466
|
-
- `hybrid`
|
|
464
|
+
- `lexical` (default) - BM25 over content + symbol + path boosts. No peer needed.
|
|
465
|
+
- `semantic` - cosine similarity on `Xenova/all-MiniLM-L6-v2` embeddings. Requires peer.
|
|
466
|
+
- `hybrid` - both, fused via Reciprocal Rank Fusion. Requires peer.
|
|
467
467
|
|
|
468
|
-
Semantic embeddings are cached at `.projscan-cache/embeddings.bin` keyed by `(model, mtime, content hash)`
|
|
468
|
+
Semantic embeddings are cached at `.projscan-cache/embeddings.bin` keyed by `(model, mtime, content hash)` - invalidates automatically on file change. All offline after the first-run model download (~25MB).
|
|
469
469
|
|
|
470
470
|
### Pagination, progress, and streaming (0.8.0+)
|
|
471
471
|
|
|
@@ -475,15 +475,15 @@ Large responses can be walked incrementally:
|
|
|
475
475
|
- **Progress notifications**: set `_meta.progressToken` on the tool-call request. The server emits `notifications/progress` at coarse milestones (scanning → analyzing → ranking → done) so your agent can display progress or cancel.
|
|
476
476
|
- **Response chunking**: set `stream: true` in arguments to split large arrays into multiple `content` blocks (header + N chunks of ~20 records each).
|
|
477
477
|
|
|
478
|
-
All opt-in
|
|
478
|
+
All opt-in - default behavior is unchanged.
|
|
479
479
|
|
|
480
480
|
### Incremental index cache
|
|
481
481
|
|
|
482
482
|
projscan caches parsed ASTs at `.projscan-cache/graph.json` (auto-gitignored). First run populates it; subsequent runs re-parse only files whose `mtime` changed. Agent queries on a warm cache are milliseconds, not seconds.
|
|
483
483
|
|
|
484
484
|
### Prompts (2, parameterized with live project data)
|
|
485
|
-
- `prioritize_refactoring`
|
|
486
|
-
- `investigate_file`
|
|
485
|
+
- `prioritize_refactoring` - ranked plan grounded in current hotspots
|
|
486
|
+
- `investigate_file` - senior-engineer brief for a specific file
|
|
487
487
|
|
|
488
488
|
### Resources (3, readable on demand)
|
|
489
489
|
- `projscan://health` · `projscan://hotspots` · `projscan://structure`
|
|
@@ -6,12 +6,12 @@ import type { FileEntry, Issue } from '../types.js';
|
|
|
6
6
|
*
|
|
7
7
|
* Does NOT flag:
|
|
8
8
|
* - files listed as the package's public entry points (main, exports, types, bin)
|
|
9
|
-
* - default exports (too many false positives
|
|
9
|
+
* - default exports (too many false positives - framework conventions)
|
|
10
10
|
* - test files (they're not supposed to export)
|
|
11
11
|
* - index files (barrels re-export for public use)
|
|
12
12
|
*
|
|
13
13
|
* False-positive guard: if a file is the target of at least one import, we treat
|
|
14
|
-
* all its exports as "possibly used"
|
|
14
|
+
* all its exports as "possibly used" - the regex-based graph can't tell which
|
|
15
15
|
* named export is imported via `import { ... } from './barrel'`. This keeps
|
|
16
16
|
* noise low at the cost of missing some dead exports that live in used files.
|
|
17
17
|
*/
|
|
@@ -3,7 +3,7 @@ import path from 'node:path';
|
|
|
3
3
|
import { buildImportGraph } from '../core/importGraph.js';
|
|
4
4
|
import { extractExports } from '../core/fileInspector.js';
|
|
5
5
|
const SOURCE_EXTENSIONS = new Set(['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs', '.mts', '.cts']);
|
|
6
|
-
// Never flag these
|
|
6
|
+
// Never flag these - they're public API by definition
|
|
7
7
|
const PUBLIC_PATH_PREFIXES = ['src/index', 'index.'];
|
|
8
8
|
/**
|
|
9
9
|
* Flag exports that are never imported anywhere in the project. This catches:
|
|
@@ -12,12 +12,12 @@ const PUBLIC_PATH_PREFIXES = ['src/index', 'index.'];
|
|
|
12
12
|
*
|
|
13
13
|
* Does NOT flag:
|
|
14
14
|
* - files listed as the package's public entry points (main, exports, types, bin)
|
|
15
|
-
* - default exports (too many false positives
|
|
15
|
+
* - default exports (too many false positives - framework conventions)
|
|
16
16
|
* - test files (they're not supposed to export)
|
|
17
17
|
* - index files (barrels re-export for public use)
|
|
18
18
|
*
|
|
19
19
|
* False-positive guard: if a file is the target of at least one import, we treat
|
|
20
|
-
* all its exports as "possibly used"
|
|
20
|
+
* all its exports as "possibly used" - the regex-based graph can't tell which
|
|
21
21
|
* named export is imported via `import { ... } from './barrel'`. This keeps
|
|
22
22
|
* noise low at the cost of missing some dead exports that live in used files.
|
|
23
23
|
*/
|
|
@@ -143,7 +143,7 @@ async function loadPublicEntries(rootPath) {
|
|
|
143
143
|
collectExports(pkg.exports, entries);
|
|
144
144
|
}
|
|
145
145
|
catch {
|
|
146
|
-
// package.json missing/unreadable
|
|
146
|
+
// package.json missing/unreadable - don't guard, every file is a candidate
|
|
147
147
|
}
|
|
148
148
|
return entries;
|
|
149
149
|
}
|
|
@@ -101,7 +101,7 @@ export async function check(rootPath, files) {
|
|
|
101
101
|
continue;
|
|
102
102
|
if (scriptUsedBinaries.has(name))
|
|
103
103
|
continue;
|
|
104
|
-
// skip scoped bin lookups (e.g., "npx some-tool")
|
|
104
|
+
// skip scoped bin lookups (e.g., "npx some-tool") - covered by scriptUsedBinaries
|
|
105
105
|
const isDev = name in devDependencies;
|
|
106
106
|
const line = locations?.lineOfDependency.get(name);
|
|
107
107
|
unused.push({
|
package/dist/cli/index.js
CHANGED
|
@@ -42,7 +42,7 @@ import { reportAnalysisSarif, reportHealthSarif, reportCiSarif, issuesToSarif, }
|
|
|
42
42
|
const program = new Command();
|
|
43
43
|
program
|
|
44
44
|
.name('projscan')
|
|
45
|
-
.description('Instant codebase insights
|
|
45
|
+
.description('Instant codebase insights - doctor, x-ray, and architecture map for any repository')
|
|
46
46
|
.version(pkg.version)
|
|
47
47
|
.option('--format <type>', 'output format: console, json, markdown, sarif', 'console')
|
|
48
48
|
.option('--config <path>', 'path to .projscanrc config file')
|
|
@@ -78,7 +78,7 @@ async function filterIssuesByChangedFiles(issues, rootPath, baseRef) {
|
|
|
78
78
|
const result = await getChangedFiles(rootPath, baseRef);
|
|
79
79
|
if (!result.available) {
|
|
80
80
|
if (getFormat() === 'console' && !program.opts().quiet) {
|
|
81
|
-
console.error(chalk.yellow(` [--changed-only: ${result.reason ?? 'unavailable'}
|
|
81
|
+
console.error(chalk.yellow(` [--changed-only: ${result.reason ?? 'unavailable'} - reporting all issues]`));
|
|
82
82
|
}
|
|
83
83
|
return issues;
|
|
84
84
|
}
|
|
@@ -411,7 +411,7 @@ program
|
|
|
411
411
|
// ── Command: file ─────────────────────────────────────────
|
|
412
412
|
program
|
|
413
413
|
.command('file <file>')
|
|
414
|
-
.description('Drill into a file
|
|
414
|
+
.description('Drill into a file - purpose, risk, ownership, related issues')
|
|
415
415
|
.action(async (filePath) => {
|
|
416
416
|
setupLogLevel();
|
|
417
417
|
maybeCompactBanner();
|
|
@@ -447,7 +447,7 @@ program
|
|
|
447
447
|
// ── Command: explain ──────────────────────────────────────
|
|
448
448
|
program
|
|
449
449
|
.command('explain <file>')
|
|
450
|
-
.description('Explain a file
|
|
450
|
+
.description('Explain a file - its purpose, dependencies, and exports')
|
|
451
451
|
.action(async (filePath) => {
|
|
452
452
|
setupLogLevel();
|
|
453
453
|
maybeCompactBanner();
|
|
@@ -630,7 +630,7 @@ program
|
|
|
630
630
|
// ── Command: outdated ─────────────────────────────────────
|
|
631
631
|
program
|
|
632
632
|
.command('outdated')
|
|
633
|
-
.description('Detect outdated dependencies (offline
|
|
633
|
+
.description('Detect outdated dependencies (offline - compares declared vs installed)')
|
|
634
634
|
.action(async () => {
|
|
635
635
|
setupLogLevel();
|
|
636
636
|
maybeCompactBanner();
|
|
@@ -702,7 +702,7 @@ program
|
|
|
702
702
|
// ── Command: upgrade ──────────────────────────────────────
|
|
703
703
|
program
|
|
704
704
|
.command('upgrade <package>')
|
|
705
|
-
.description('Preview the impact of upgrading a package (offline
|
|
705
|
+
.description('Preview the impact of upgrading a package (offline - reads local CHANGELOG + importers)')
|
|
706
706
|
.action(async (pkgName) => {
|
|
707
707
|
setupLogLevel();
|
|
708
708
|
maybeCompactBanner();
|
|
@@ -736,7 +736,7 @@ program
|
|
|
736
736
|
// ── Command: search ───────────────────────────────────────
|
|
737
737
|
program
|
|
738
738
|
.command('search <query...>')
|
|
739
|
-
.description('Ranked search
|
|
739
|
+
.description('Ranked search - BM25 by default, semantic or hybrid when @xenova/transformers peer is installed')
|
|
740
740
|
.option('--scope <scope>', 'auto | content | symbols | files', 'auto')
|
|
741
741
|
.option('--mode <mode>', 'lexical | semantic | hybrid (content/auto scope only)', 'lexical')
|
|
742
742
|
.option('--semantic', 'shortcut for --mode semantic')
|
|
@@ -881,7 +881,7 @@ program
|
|
|
881
881
|
}
|
|
882
882
|
if (format === 'markdown') {
|
|
883
883
|
const r = results;
|
|
884
|
-
console.log(`# Search
|
|
884
|
+
console.log(`# Search - \`${r.query}\` (${r.scope})\n`);
|
|
885
885
|
if (r.matches.length === 0) {
|
|
886
886
|
console.log('_No matches._');
|
|
887
887
|
return;
|
|
@@ -890,7 +890,7 @@ program
|
|
|
890
890
|
if ('symbol' in m)
|
|
891
891
|
console.log(`- \`${m.symbol}\` (${m.kind}) → \`${m.file}:${m.line}\``);
|
|
892
892
|
else if ('score' in m)
|
|
893
|
-
console.log(`- \`${m.file}:${m.line}\`
|
|
893
|
+
console.log(`- \`${m.file}:${m.line}\` - score ${m.score} - ${m.excerpt ?? ''}`);
|
|
894
894
|
else
|
|
895
895
|
console.log(`- \`${m.file}\``);
|
|
896
896
|
}
|
|
@@ -898,7 +898,7 @@ program
|
|
|
898
898
|
}
|
|
899
899
|
// Console
|
|
900
900
|
const r = results;
|
|
901
|
-
console.log(`\n ${chalk.bold(`Search
|
|
901
|
+
console.log(`\n ${chalk.bold(`Search - "${query}"`)} ${chalk.dim(`[${r.scope}]`)}`);
|
|
902
902
|
if (r.queryTokens)
|
|
903
903
|
console.log(chalk.dim(` tokens: ${r.queryTokens.join(', ')}`));
|
|
904
904
|
console.log(chalk.dim(' ─'.repeat(20)));
|
|
@@ -932,7 +932,7 @@ program
|
|
|
932
932
|
// ── Command: coverage ─────────────────────────────────────
|
|
933
933
|
program
|
|
934
934
|
.command('coverage')
|
|
935
|
-
.description('Join test coverage with hotspots
|
|
935
|
+
.description('Join test coverage with hotspots - surface the scariest untested files')
|
|
936
936
|
.option('--limit <n>', 'limit number of entries shown', '30')
|
|
937
937
|
.action(async (cmdOpts) => {
|
|
938
938
|
setupLogLevel();
|
package/dist/core/ast.d.ts
CHANGED
|
@@ -28,7 +28,7 @@ export declare function isParseable(filePath: string): boolean;
|
|
|
28
28
|
* Uses @babel/parser with generous options so we accept real-world code:
|
|
29
29
|
* TypeScript, JSX, decorators, top-level await, class properties, etc.
|
|
30
30
|
*
|
|
31
|
-
* Failures return ok:false with a reason
|
|
31
|
+
* Failures return ok:false with a reason - callers decide whether to fall
|
|
32
32
|
* back to regex or skip the file. Never throws.
|
|
33
33
|
*/
|
|
34
34
|
export declare function parseSource(filePath: string, content: string): AstResult;
|
package/dist/core/ast.js
CHANGED
|
@@ -28,7 +28,7 @@ export function isParseable(filePath) {
|
|
|
28
28
|
* Uses @babel/parser with generous options so we accept real-world code:
|
|
29
29
|
* TypeScript, JSX, decorators, top-level await, class properties, etc.
|
|
30
30
|
*
|
|
31
|
-
* Failures return ok:false with a reason
|
|
31
|
+
* Failures return ok:false with a reason - callers decide whether to fall
|
|
32
32
|
* back to regex or skip the file. Never throws.
|
|
33
33
|
*/
|
|
34
34
|
export function parseSource(filePath, content) {
|
|
@@ -67,7 +67,7 @@ export function parseSource(filePath, content) {
|
|
|
67
67
|
visitTopLevel(node, imports, exports);
|
|
68
68
|
}
|
|
69
69
|
// Second pass: extract dynamic imports + call sites. Walk the whole tree
|
|
70
|
-
// (cheap
|
|
70
|
+
// (cheap - we already have the AST in memory).
|
|
71
71
|
walk(ast.program, (n) => {
|
|
72
72
|
if (n.type === 'CallExpression') {
|
|
73
73
|
const callee = n.callee;
|
|
@@ -6,7 +6,7 @@ export interface AuditOptions {
|
|
|
6
6
|
/**
|
|
7
7
|
* Run `npm audit --json` and normalize the output.
|
|
8
8
|
*
|
|
9
|
-
* npm's audit JSON format has changed between npm 6/7/8/9+
|
|
9
|
+
* npm's audit JSON format has changed between npm 6/7/8/9+ - we handle the
|
|
10
10
|
* modern format (npm 7+) first and fall back to a friendly error otherwise.
|
|
11
11
|
* Yarn/pnpm projects: we don't try to translate; we report "not available"
|
|
12
12
|
* with a hint.
|
package/dist/core/auditRunner.js
CHANGED
|
@@ -13,7 +13,7 @@ const EMPTY_SUMMARY = {
|
|
|
13
13
|
/**
|
|
14
14
|
* Run `npm audit --json` and normalize the output.
|
|
15
15
|
*
|
|
16
|
-
* npm's audit JSON format has changed between npm 6/7/8/9+
|
|
16
|
+
* npm's audit JSON format has changed between npm 6/7/8/9+ - we handle the
|
|
17
17
|
* modern format (npm 7+) first and fall back to a friendly error otherwise.
|
|
18
18
|
* Yarn/pnpm projects: we don't try to translate; we report "not available"
|
|
19
19
|
* with a hint.
|
|
@@ -28,12 +28,12 @@ export async function runAudit(rootPath, options = {}) {
|
|
|
28
28
|
const hasPnpmLock = await fileExists(path.join(rootPath, 'pnpm-lock.yaml'));
|
|
29
29
|
if (!hasNpmLock) {
|
|
30
30
|
if (hasYarnLock) {
|
|
31
|
-
return unavailable('yarn.lock detected
|
|
31
|
+
return unavailable('yarn.lock detected - run `yarn npm audit` instead');
|
|
32
32
|
}
|
|
33
33
|
if (hasPnpmLock) {
|
|
34
|
-
return unavailable('pnpm-lock.yaml detected
|
|
34
|
+
return unavailable('pnpm-lock.yaml detected - run `pnpm audit` instead');
|
|
35
35
|
}
|
|
36
|
-
return unavailable('No package-lock.json
|
|
36
|
+
return unavailable('No package-lock.json - run `npm install` first, then retry');
|
|
37
37
|
}
|
|
38
38
|
const timeoutMs = options.timeoutMs ?? 60_000;
|
|
39
39
|
let stdout;
|
|
@@ -46,7 +46,7 @@ export async function runAudit(rootPath, options = {}) {
|
|
|
46
46
|
stdout = result.stdout;
|
|
47
47
|
}
|
|
48
48
|
catch (err) {
|
|
49
|
-
// `npm audit` exits non-zero when vulnerabilities exist
|
|
49
|
+
// `npm audit` exits non-zero when vulnerabilities exist - this is normal.
|
|
50
50
|
// The stdout still contains the JSON payload.
|
|
51
51
|
const e = err;
|
|
52
52
|
if (typeof e.stdout === 'string' && e.stdout) {
|
|
@@ -211,7 +211,7 @@ export function auditFindingsToIssues(report) {
|
|
|
211
211
|
id: `audit-${f.name}`,
|
|
212
212
|
title: f.title,
|
|
213
213
|
description: f.url !== undefined
|
|
214
|
-
? `${f.title}
|
|
214
|
+
? `${f.title} - ${f.url}${f.range ? ` (range: ${f.range})` : ''}`
|
|
215
215
|
: `${f.title}${f.range ? ` (range: ${f.range})` : ''}`,
|
|
216
216
|
severity: severityMap[f.severity],
|
|
217
217
|
category: 'security',
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { CoverageJoinedReport, CoverageReport, HotspotReport } from '../types.js';
|
|
2
2
|
/**
|
|
3
3
|
* Join a hotspot report with a coverage report and rank entries by
|
|
4
|
-
* "risk × uncovered fraction"
|
|
4
|
+
* "risk × uncovered fraction" - the files that most deserve tests.
|
|
5
5
|
*/
|
|
6
6
|
export declare function joinCoverageWithHotspots(hotspots: HotspotReport, coverage: CoverageReport): CoverageJoinedReport;
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
/**
|
|
2
2
|
* Join a hotspot report with a coverage report and rank entries by
|
|
3
|
-
* "risk × uncovered fraction"
|
|
3
|
+
* "risk × uncovered fraction" - the files that most deserve tests.
|
|
4
4
|
*/
|
|
5
5
|
export function joinCoverageWithHotspots(hotspots, coverage) {
|
|
6
6
|
if (!hotspots.available) {
|
|
@@ -64,7 +64,7 @@ function parseByFormat(raw, source, rootPath) {
|
|
|
64
64
|
return parseCoverageSummary(raw, rootPath);
|
|
65
65
|
}
|
|
66
66
|
/**
|
|
67
|
-
* LCOV
|
|
67
|
+
* LCOV - record-oriented plain text:
|
|
68
68
|
* SF:/abs/path/to/file.ts
|
|
69
69
|
* LF:100 (lines found)
|
|
70
70
|
* LH:85 (lines hit)
|
|
@@ -104,7 +104,7 @@ function parseLcov(raw, rootPath) {
|
|
|
104
104
|
return files;
|
|
105
105
|
}
|
|
106
106
|
/**
|
|
107
|
-
* coverage-final.json
|
|
107
|
+
* coverage-final.json - Istanbul per-file detail:
|
|
108
108
|
* { "/abs/path/file.ts": { "path": "...", "statementMap": {...}, "s": { "0": 1, "1": 0 }, ... } }
|
|
109
109
|
* We approximate line coverage from statement counts (statements is the closest
|
|
110
110
|
* thing to "line" when line-level data isn't separately broken out).
|
|
@@ -132,7 +132,7 @@ function parseCoverageFinal(raw, rootPath) {
|
|
|
132
132
|
return files;
|
|
133
133
|
}
|
|
134
134
|
/**
|
|
135
|
-
* coverage-summary.json
|
|
135
|
+
* coverage-summary.json - Istanbul per-file summary:
|
|
136
136
|
* { "total": {...}, "/abs/path/file.ts": { "lines": { "total": 100, "covered": 85, "pct": 85.0 }, ... } }
|
|
137
137
|
*/
|
|
138
138
|
function parseCoverageSummary(raw, rootPath) {
|