projscan 0.9.0 → 0.9.1
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.js +1 -1
- package/dist/fixes/prettierFix.js +1 -1
- package/dist/fixes/testFix.js +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
|
@@ -2,9 +2,9 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
const DEPRECATED_PACKAGES = {
|
|
4
4
|
moment: 'Consider using date-fns or dayjs instead',
|
|
5
|
-
request: 'Deprecated
|
|
5
|
+
request: 'Deprecated - use node-fetch, undici, or axios instead',
|
|
6
6
|
'node-uuid': 'Renamed to uuid',
|
|
7
|
-
nomnom: 'Deprecated
|
|
7
|
+
nomnom: 'Deprecated - use commander or yargs instead',
|
|
8
8
|
'coffee-script': 'CoffeeScript is no longer maintained',
|
|
9
9
|
};
|
|
10
10
|
const HEAVY_PACKAGES = {
|
|
@@ -42,14 +42,14 @@ export async function analyzeDependencies(rootPath) {
|
|
|
42
42
|
if (totalDeps > 100) {
|
|
43
43
|
risks.push({
|
|
44
44
|
name: 'excessive-dependencies',
|
|
45
|
-
reason: `${totalDeps} production dependencies
|
|
45
|
+
reason: `${totalDeps} production dependencies - consider auditing for unused packages`,
|
|
46
46
|
severity: 'high',
|
|
47
47
|
});
|
|
48
48
|
}
|
|
49
49
|
else if (totalDeps > 50) {
|
|
50
50
|
risks.push({
|
|
51
51
|
name: 'many-dependencies',
|
|
52
|
-
reason: `${totalDeps} production dependencies
|
|
52
|
+
reason: `${totalDeps} production dependencies - review for opportunities to reduce`,
|
|
53
53
|
severity: 'medium',
|
|
54
54
|
});
|
|
55
55
|
}
|
|
@@ -58,7 +58,7 @@ export async function analyzeDependencies(rootPath) {
|
|
|
58
58
|
if (version === '*' || version.startsWith('>=')) {
|
|
59
59
|
risks.push({
|
|
60
60
|
name,
|
|
61
|
-
reason: `Wildcard version range "${version}"
|
|
61
|
+
reason: `Wildcard version range "${version}" - pin to a specific version for reproducible builds`,
|
|
62
62
|
severity: 'high',
|
|
63
63
|
});
|
|
64
64
|
}
|
|
@@ -68,7 +68,7 @@ export async function analyzeDependencies(rootPath) {
|
|
|
68
68
|
if (!hasLockfile && totalDeps > 0) {
|
|
69
69
|
risks.push({
|
|
70
70
|
name: 'no-lockfile',
|
|
71
|
-
reason: 'No lockfile found
|
|
71
|
+
reason: 'No lockfile found - run npm install to generate package-lock.json',
|
|
72
72
|
severity: 'medium',
|
|
73
73
|
});
|
|
74
74
|
}
|
package/dist/core/embeddings.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
/**
|
|
2
|
-
* Thin wrapper around `@xenova/transformers`
|
|
2
|
+
* Thin wrapper around `@xenova/transformers` - imported dynamically so we
|
|
3
3
|
* fail softly when the peer dep is absent.
|
|
4
4
|
*
|
|
5
5
|
* Design goals:
|
|
@@ -32,7 +32,7 @@ async function tryLoadTransformers() {
|
|
|
32
32
|
cachedModule = null;
|
|
33
33
|
return null;
|
|
34
34
|
}
|
|
35
|
-
// Unexpected load error
|
|
35
|
+
// Unexpected load error - treat as unavailable, log to stderr for diagnosis.
|
|
36
36
|
process.stderr.write(`[projscan] embeddings unavailable: ${err instanceof Error ? err.message : String(err)}\n`);
|
|
37
37
|
cachedModule = null;
|
|
38
38
|
return null;
|
|
@@ -86,13 +86,13 @@ export function extractImports(content) {
|
|
|
86
86
|
});
|
|
87
87
|
}
|
|
88
88
|
};
|
|
89
|
-
// ES import
|
|
89
|
+
// ES import - optional `type` keyword for type-only imports.
|
|
90
90
|
const esImportRegex = /import\s+(?:type\s+)?(?:(?:\{[^}]*\}|[\w*]+(?:\s*,\s*\{[^}]*\})?|\*\s+as\s+\w+)\s+from\s+)?['"]([^'"]+)['"]/gm;
|
|
91
91
|
let match;
|
|
92
92
|
while ((match = esImportRegex.exec(content)) !== null) {
|
|
93
93
|
addSource(match[1]);
|
|
94
94
|
}
|
|
95
|
-
// ES re-export
|
|
95
|
+
// ES re-export - `export ... from '...'` counts as an import from the
|
|
96
96
|
// importer's point of view for graph-building purposes.
|
|
97
97
|
const esReexportRegex = /export\s+(?:type\s+)?(?:\{[^}]*\}|\*(?:\s+as\s+\w+)?)\s+from\s+['"]([^'"]+)['"]/gm;
|
|
98
98
|
while ((match = esReexportRegex.exec(content)) !== null) {
|
|
@@ -195,17 +195,17 @@ export function inferPurpose(filePath, exports) {
|
|
|
195
195
|
export function detectFileIssues(content, lineCount) {
|
|
196
196
|
const issues = [];
|
|
197
197
|
if (lineCount > 500)
|
|
198
|
-
issues.push(`Large file (${lineCount} lines)
|
|
198
|
+
issues.push(`Large file (${lineCount} lines) - consider splitting`);
|
|
199
199
|
if (lineCount > 1000)
|
|
200
|
-
issues.push('Very large file
|
|
200
|
+
issues.push('Very large file - strongly consider refactoring');
|
|
201
201
|
if (/console\.(log|warn|error|debug)\s*\(/.test(content)) {
|
|
202
|
-
issues.push('Contains console.log statements
|
|
202
|
+
issues.push('Contains console.log statements - consider using a proper logger');
|
|
203
203
|
}
|
|
204
204
|
if (/TODO|FIXME|HACK|XXX/i.test(content)) {
|
|
205
205
|
issues.push('Contains TODO/FIXME comments');
|
|
206
206
|
}
|
|
207
207
|
if (/:\s*any\b/.test(content) && /\.tsx?$/.test(content)) {
|
|
208
|
-
issues.push('Uses "any" type
|
|
208
|
+
issues.push('Uses "any" type - consider using proper types');
|
|
209
209
|
}
|
|
210
210
|
return issues;
|
|
211
211
|
}
|
|
@@ -18,7 +18,7 @@ export async function analyzeHotspots(rootPath, files, issues, options = {}) {
|
|
|
18
18
|
if (!isRepo) {
|
|
19
19
|
return {
|
|
20
20
|
available: false,
|
|
21
|
-
reason: 'Not a git repository
|
|
21
|
+
reason: 'Not a git repository - hotspot analysis requires git history',
|
|
22
22
|
window: { since: null, commitsScanned: 0 },
|
|
23
23
|
hotspots: [],
|
|
24
24
|
totalFilesRanked: 0,
|
|
@@ -263,7 +263,7 @@ function indexIssuesByFile(issues, files) {
|
|
|
263
263
|
index.set(file, list);
|
|
264
264
|
};
|
|
265
265
|
for (const issue of issues) {
|
|
266
|
-
// Prefer explicit locations when the analyzer supplied them
|
|
266
|
+
// Prefer explicit locations when the analyzer supplied them - this is
|
|
267
267
|
// exact and avoids the substring-false-positive problem where "src/a.ts"
|
|
268
268
|
// would match issues that only mention "src/ab.ts".
|
|
269
269
|
if (issue.locations && issue.locations.length > 0) {
|
|
@@ -10,7 +10,7 @@ export interface ImportGraph {
|
|
|
10
10
|
}
|
|
11
11
|
/**
|
|
12
12
|
* Walk source files and build an import graph. Now backed by AST-based
|
|
13
|
-
* codeGraph
|
|
13
|
+
* codeGraph - this function is retained for public API compatibility.
|
|
14
14
|
*/
|
|
15
15
|
export declare function buildImportGraph(rootPath: string, files: FileEntry[]): Promise<ImportGraph>;
|
|
16
16
|
/** Convert an import specifier to a bare package name. */
|
package/dist/core/importGraph.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { buildCodeGraph, toPackageName as graphToPackageName } from './codeGraph.js';
|
|
2
2
|
/**
|
|
3
3
|
* Walk source files and build an import graph. Now backed by AST-based
|
|
4
|
-
* codeGraph
|
|
4
|
+
* codeGraph - this function is retained for public API compatibility.
|
|
5
5
|
*/
|
|
6
6
|
export async function buildImportGraph(rootPath, files) {
|
|
7
7
|
const code = await buildCodeGraph(rootPath, files);
|
|
@@ -1,11 +1,11 @@
|
|
|
1
1
|
import type { CodeGraph } from './codeGraph.js';
|
|
2
2
|
/**
|
|
3
3
|
* Load a previously cached code graph, if present and valid. Returns undefined
|
|
4
|
-
* when there's no cache or the cache is incompatible
|
|
4
|
+
* when there's no cache or the cache is incompatible - caller should rebuild.
|
|
5
5
|
*/
|
|
6
6
|
export declare function loadCachedGraph(rootPath: string): Promise<CodeGraph | undefined>;
|
|
7
7
|
/**
|
|
8
|
-
* Persist the graph. Creates .projscan-cache/ if needed. Swallows errors
|
|
8
|
+
* Persist the graph. Creates .projscan-cache/ if needed. Swallows errors -
|
|
9
9
|
* caching is best-effort, never blocks a run.
|
|
10
10
|
*/
|
|
11
11
|
export declare function saveCachedGraph(rootPath: string, graph: CodeGraph): Promise<void>;
|
package/dist/core/indexCache.js
CHANGED
|
@@ -5,7 +5,7 @@ const CACHE_FILE = 'graph.json';
|
|
|
5
5
|
const CACHE_VERSION = 1;
|
|
6
6
|
/**
|
|
7
7
|
* Load a previously cached code graph, if present and valid. Returns undefined
|
|
8
|
-
* when there's no cache or the cache is incompatible
|
|
8
|
+
* when there's no cache or the cache is incompatible - caller should rebuild.
|
|
9
9
|
*/
|
|
10
10
|
export async function loadCachedGraph(rootPath) {
|
|
11
11
|
const cachePath = path.join(rootPath, CACHE_DIR, CACHE_FILE);
|
|
@@ -38,7 +38,7 @@ export async function loadCachedGraph(rootPath) {
|
|
|
38
38
|
parseReason: entry.parseReason,
|
|
39
39
|
});
|
|
40
40
|
}
|
|
41
|
-
// Derived indexes are rebuilt on load
|
|
41
|
+
// Derived indexes are rebuilt on load - cheap compared to re-parsing.
|
|
42
42
|
// Return a partial graph the caller will rehydrate via buildCodeGraph.
|
|
43
43
|
return {
|
|
44
44
|
files,
|
|
@@ -49,7 +49,7 @@ export async function loadCachedGraph(rootPath) {
|
|
|
49
49
|
};
|
|
50
50
|
}
|
|
51
51
|
/**
|
|
52
|
-
* Persist the graph. Creates .projscan-cache/ if needed. Swallows errors
|
|
52
|
+
* Persist the graph. Creates .projscan-cache/ if needed. Swallows errors -
|
|
53
53
|
* caching is best-effort, never blocks a run.
|
|
54
54
|
*/
|
|
55
55
|
export async function saveCachedGraph(rootPath, graph) {
|
|
@@ -82,7 +82,7 @@ export async function saveCachedGraph(rootPath, graph) {
|
|
|
82
82
|
await fs.writeFile(gitignorePath, '*\n', 'utf-8');
|
|
83
83
|
}
|
|
84
84
|
catch {
|
|
85
|
-
// ignore
|
|
85
|
+
// ignore - cache is best-effort
|
|
86
86
|
}
|
|
87
87
|
}
|
|
88
88
|
export async function invalidateCache(rootPath) {
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import type { OutdatedReport } from '../types.js';
|
|
2
2
|
/**
|
|
3
|
-
* Offline outdated check
|
|
3
|
+
* Offline outdated check - compares the version declared in package.json
|
|
4
4
|
* to the version installed under node_modules/<pkg>/package.json.
|
|
5
5
|
*
|
|
6
6
|
* Does not hit the npm registry. `latest` is filled in only when a node_modules
|
|
@@ -2,7 +2,7 @@ import fs from 'node:fs/promises';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { drift as semverDrift } from '../utils/semver.js';
|
|
4
4
|
/**
|
|
5
|
-
* Offline outdated check
|
|
5
|
+
* Offline outdated check - compares the version declared in package.json
|
|
6
6
|
* to the version installed under node_modules/<pkg>/package.json.
|
|
7
7
|
*
|
|
8
8
|
* Does not hit the npm registry. `latest` is filled in only when a node_modules
|
package/dist/core/searchIndex.js
CHANGED
|
@@ -5,7 +5,7 @@ import path from 'node:path';
|
|
|
5
5
|
*
|
|
6
6
|
* We index three fields per file with different weights:
|
|
7
7
|
* - content (body tokens, BM25 baseline)
|
|
8
|
-
* - symbols (export names
|
|
8
|
+
* - symbols (export names - most informative for code search)
|
|
9
9
|
* - path (file path tokens)
|
|
10
10
|
*
|
|
11
11
|
* Scoring:
|
|
@@ -191,7 +191,7 @@ export function tokenize(input) {
|
|
|
191
191
|
.replace(/([A-Z]+)([A-Z][a-z])/g, '$1 $2');
|
|
192
192
|
const parts = camelSplit.split(/[_\s]+/).filter(Boolean);
|
|
193
193
|
for (const part of parts) {
|
|
194
|
-
// Split embedded digits from letters
|
|
194
|
+
// Split embedded digits from letters - e.g. "v1api" → "v", "1", "api"
|
|
195
195
|
const subparts = part.split(/(\d+)/).filter(Boolean);
|
|
196
196
|
for (const sp of subparts) {
|
|
197
197
|
const lower = sp.toLowerCase();
|
|
@@ -22,7 +22,7 @@ export interface SemanticHit {
|
|
|
22
22
|
}
|
|
23
23
|
/**
|
|
24
24
|
* Build (or refresh) a semantic index. Reuses cached embeddings for files
|
|
25
|
-
* whose mtime AND content hash match
|
|
25
|
+
* whose mtime AND content hash match - both guards are necessary because
|
|
26
26
|
* git checkouts can preserve mtime while swapping content.
|
|
27
27
|
*
|
|
28
28
|
* Returns null if the peer dep isn't available.
|
|
@@ -27,7 +27,7 @@ const INDEXABLE_EXTS = new Set([
|
|
|
27
27
|
]);
|
|
28
28
|
/**
|
|
29
29
|
* Build (or refresh) a semantic index. Reuses cached embeddings for files
|
|
30
|
-
* whose mtime AND content hash match
|
|
30
|
+
* whose mtime AND content hash match - both guards are necessary because
|
|
31
31
|
* git checkouts can preserve mtime while swapping content.
|
|
32
32
|
*
|
|
33
33
|
* Returns null if the peer dep isn't available.
|
|
@@ -120,7 +120,7 @@ export async function buildSemanticIndex(rootPath, files, options = {}) {
|
|
|
120
120
|
entries,
|
|
121
121
|
};
|
|
122
122
|
await saveCache(rootPath, index).catch(() => {
|
|
123
|
-
// best-effort
|
|
123
|
+
// best-effort - don't fail the search if cache write fails
|
|
124
124
|
});
|
|
125
125
|
return index;
|
|
126
126
|
}
|
|
@@ -31,7 +31,7 @@ export async function previewUpgrade(rootPath, pkgName, files) {
|
|
|
31
31
|
if (!installed) {
|
|
32
32
|
return {
|
|
33
33
|
available: false,
|
|
34
|
-
reason: `Package "${pkgName}" not installed
|
|
34
|
+
reason: `Package "${pkgName}" not installed - run npm install and retry`,
|
|
35
35
|
name: pkgName,
|
|
36
36
|
declared: declaredVersions,
|
|
37
37
|
installed: null,
|
|
@@ -34,7 +34,7 @@ export const prettierFix = {
|
|
|
34
34
|
}
|
|
35
35
|
catch (err) {
|
|
36
36
|
if (err instanceof Error && 'code' in err && err.code === 'ENOENT') {
|
|
37
|
-
return; // No package.json
|
|
37
|
+
return; // No package.json - nothing to update
|
|
38
38
|
}
|
|
39
39
|
throw err; // Re-throw JSON parse errors or unexpected failures
|
|
40
40
|
}
|
package/dist/fixes/testFix.js
CHANGED
|
@@ -30,7 +30,7 @@ export const testFix = {
|
|
|
30
30
|
}
|
|
31
31
|
catch (err) {
|
|
32
32
|
if (err instanceof Error && 'code' in err && err.code === 'ENOENT') {
|
|
33
|
-
return; // No package.json
|
|
33
|
+
return; // No package.json - nothing to update
|
|
34
34
|
}
|
|
35
35
|
throw err; // Re-throw JSON parse errors or unexpected failures
|
|
36
36
|
}
|
package/dist/mcp/chunker.d.ts
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* emit chunk blocks each containing a slice of the array.
|
|
9
9
|
* - Otherwise, emit a single block.
|
|
10
10
|
*
|
|
11
|
-
* Chunk size defaults to 20 records per block
|
|
11
|
+
* Chunk size defaults to 20 records per block - small enough to be a
|
|
12
12
|
* meaningful streaming unit, big enough to avoid pathological block counts.
|
|
13
13
|
*/
|
|
14
14
|
export interface ContentBlock {
|
package/dist/mcp/chunker.js
CHANGED
|
@@ -8,7 +8,7 @@
|
|
|
8
8
|
* emit chunk blocks each containing a slice of the array.
|
|
9
9
|
* - Otherwise, emit a single block.
|
|
10
10
|
*
|
|
11
|
-
* Chunk size defaults to 20 records per block
|
|
11
|
+
* Chunk size defaults to 20 records per block - small enough to be a
|
|
12
12
|
* meaningful streaming unit, big enough to avoid pathological block counts.
|
|
13
13
|
*/
|
|
14
14
|
const DEFAULT_CHUNK_SIZE = 20;
|
package/dist/mcp/pagination.d.ts
CHANGED
|
@@ -19,7 +19,7 @@ export interface Page<T> {
|
|
|
19
19
|
}
|
|
20
20
|
/**
|
|
21
21
|
* Slice an array into a page. `checksum` should be a cheap identifier of
|
|
22
|
-
* the result-set shape (e.g., `items.length`)
|
|
22
|
+
* the result-set shape (e.g., `items.length`) - if it mismatches a cursor's
|
|
23
23
|
* captured checksum we treat the page as fresh (offset=0) rather than risk
|
|
24
24
|
* returning stale offsets.
|
|
25
25
|
*/
|
|
@@ -31,7 +31,7 @@ interface DecodedCursor {
|
|
|
31
31
|
export declare function encodeCursor(cursor: DecodedCursor): string;
|
|
32
32
|
export declare function decodeCursor(cursor?: string): DecodedCursor | null;
|
|
33
33
|
/**
|
|
34
|
-
* Compute a lightweight checksum for a list. Deliberately weak
|
|
34
|
+
* Compute a lightweight checksum for a list. Deliberately weak - we want
|
|
35
35
|
* cursor invalidation on shape changes (length) but not on micro-changes
|
|
36
36
|
* within items (scores that shift slightly between runs). Agents already
|
|
37
37
|
* handle eventual consistency.
|
package/dist/mcp/pagination.js
CHANGED
|
@@ -12,7 +12,7 @@ const DEFAULT_PAGE_SIZE = 50;
|
|
|
12
12
|
const MAX_PAGE_SIZE = 500;
|
|
13
13
|
/**
|
|
14
14
|
* Slice an array into a page. `checksum` should be a cheap identifier of
|
|
15
|
-
* the result-set shape (e.g., `items.length`)
|
|
15
|
+
* the result-set shape (e.g., `items.length`) - if it mismatches a cursor's
|
|
16
16
|
* captured checksum we treat the page as fresh (offset=0) rather than risk
|
|
17
17
|
* returning stale offsets.
|
|
18
18
|
*/
|
|
@@ -49,7 +49,7 @@ export function decodeCursor(cursor) {
|
|
|
49
49
|
}
|
|
50
50
|
}
|
|
51
51
|
/**
|
|
52
|
-
* Compute a lightweight checksum for a list. Deliberately weak
|
|
52
|
+
* Compute a lightweight checksum for a list. Deliberately weak - we want
|
|
53
53
|
* cursor invalidation on shape changes (length) but not on micro-changes
|
|
54
54
|
* within items (scores that shift slightly between runs). Agents already
|
|
55
55
|
* handle eventual consistency.
|
package/dist/mcp/progress.d.ts
CHANGED
|
@@ -4,7 +4,7 @@
|
|
|
4
4
|
* Per MCP spec, a client that wants progress sets `_meta.progressToken` on
|
|
5
5
|
* the tool-call request. We capture it at dispatch time and expose a
|
|
6
6
|
* `notify(progress, total?, message?)` callback to the tool handler via an
|
|
7
|
-
* AsyncLocalStorage context
|
|
7
|
+
* AsyncLocalStorage context - which means concurrent tool calls get their
|
|
8
8
|
* own isolated emitters (the naive module-level-variable approach had tools
|
|
9
9
|
* clobbering each other's progress streams under pipelined requests).
|
|
10
10
|
*
|
package/dist/mcp/prompts.js
CHANGED
|
@@ -53,10 +53,10 @@ async function prioritizeRefactoringPrompt(args, rootPath) {
|
|
|
53
53
|
const ownership = h.busFactorOne && h.primaryAuthor
|
|
54
54
|
? ` [BUS FACTOR 1: ${h.primaryAuthor}]`
|
|
55
55
|
: '';
|
|
56
|
-
return `${i + 1}. ${h.relativePath}
|
|
56
|
+
return `${i + 1}. ${h.relativePath} - risk ${h.riskScore.toFixed(1)} (${reasons})${ownership}`;
|
|
57
57
|
})
|
|
58
58
|
.join('\n')
|
|
59
|
-
: '(no hotspots available
|
|
59
|
+
: '(no hotspots available - project may not be a git repository)';
|
|
60
60
|
const topIssues = issues
|
|
61
61
|
.slice(0, 15)
|
|
62
62
|
.map((issue) => `- [${issue.severity}] ${issue.title}`)
|
|
@@ -104,7 +104,7 @@ async function investigateFilePrompt(args, rootPath) {
|
|
|
104
104
|
'Explain in order:',
|
|
105
105
|
'1. What this file does and how it fits in the codebase.',
|
|
106
106
|
'2. What is risky about it right now (cite evidence from the report).',
|
|
107
|
-
'3. Concrete next actions
|
|
107
|
+
'3. Concrete next actions - questions to ask, tests to add, or refactors to attempt.',
|
|
108
108
|
'4. Who to involve (based on ownership, if available).',
|
|
109
109
|
].join('\n');
|
|
110
110
|
return {
|
package/dist/mcp/server.js
CHANGED
|
@@ -32,7 +32,7 @@ export function createMcpServer(rootPath, options = {}) {
|
|
|
32
32
|
let initialized = false;
|
|
33
33
|
async function dispatch(request) {
|
|
34
34
|
const id = request.id ?? null;
|
|
35
|
-
// Notifications (no id)
|
|
35
|
+
// Notifications (no id) - no response expected.
|
|
36
36
|
const isNotification = request.id === undefined || request.id === null;
|
|
37
37
|
try {
|
|
38
38
|
switch (request.method) {
|
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Rough token estimator and record-aware truncator for MCP tool output.
|
|
3
3
|
*
|
|
4
4
|
* Uses the widely-used "~4 chars per token" heuristic. Good enough for
|
|
5
|
-
* prioritization
|
|
5
|
+
* prioritization - absolute accuracy is not required.
|
|
6
6
|
*/
|
|
7
7
|
export declare const CHARS_PER_TOKEN = 4;
|
|
8
8
|
export declare function estimateTokens(value: string): number;
|
package/dist/mcp/tokenBudget.js
CHANGED
|
@@ -2,7 +2,7 @@
|
|
|
2
2
|
* Rough token estimator and record-aware truncator for MCP tool output.
|
|
3
3
|
*
|
|
4
4
|
* Uses the widely-used "~4 chars per token" heuristic. Good enough for
|
|
5
|
-
* prioritization
|
|
5
|
+
* prioritization - absolute accuracy is not required.
|
|
6
6
|
*/
|
|
7
7
|
export const CHARS_PER_TOKEN = 4;
|
|
8
8
|
export function estimateTokens(value) {
|
|
@@ -55,7 +55,7 @@ function safeStringify(value) {
|
|
|
55
55
|
}
|
|
56
56
|
}
|
|
57
57
|
/**
|
|
58
|
-
* Find top-level array field names
|
|
58
|
+
* Find top-level array field names - our convention is that MCP results
|
|
59
59
|
* expose a primary array (hotspots, entries, findings, files) worth
|
|
60
60
|
* trimming before scalar fields.
|
|
61
61
|
*/
|
package/dist/mcp/tools.js
CHANGED
|
@@ -197,7 +197,7 @@ const tools = [
|
|
|
197
197
|
},
|
|
198
198
|
{
|
|
199
199
|
name: 'projscan_outdated',
|
|
200
|
-
description: 'Compare declared vs installed versions of every package. Reports drift (patch/minor/major). Offline
|
|
200
|
+
description: 'Compare declared vs installed versions of every package. Reports drift (patch/minor/major). Offline - does not hit the npm registry. Supports cursor pagination.',
|
|
201
201
|
inputSchema: {
|
|
202
202
|
type: 'object',
|
|
203
203
|
properties: {
|
|
@@ -271,7 +271,7 @@ const tools = [
|
|
|
271
271
|
},
|
|
272
272
|
{
|
|
273
273
|
name: 'projscan_coverage',
|
|
274
|
-
description: 'Join test coverage with hotspot risk. Returns files ranked by "risk × uncovered fraction"
|
|
274
|
+
description: 'Join test coverage with hotspot risk. Returns files ranked by "risk × uncovered fraction" - the scariest untested files. Requires a coverage file at coverage/lcov.info, coverage/coverage-final.json, or coverage/coverage-summary.json.',
|
|
275
275
|
inputSchema: {
|
|
276
276
|
type: 'object',
|
|
277
277
|
properties: {
|
|
@@ -311,7 +311,7 @@ const tools = [
|
|
|
311
311
|
},
|
|
312
312
|
{
|
|
313
313
|
name: 'projscan_graph',
|
|
314
|
-
description: 'Query the AST-based code graph directly. Returns imports, exports, importers, or symbol definitions for a file or symbol. Agents should prefer this over analyze/doctor/explain for targeted structural questions
|
|
314
|
+
description: 'Query the AST-based code graph directly. Returns imports, exports, importers, or symbol definitions for a file or symbol. Agents should prefer this over analyze/doctor/explain for targeted structural questions - it is much cheaper and more accurate.',
|
|
315
315
|
inputSchema: {
|
|
316
316
|
type: 'object',
|
|
317
317
|
properties: {
|
|
@@ -421,7 +421,7 @@ const tools = [
|
|
|
421
421
|
const cached = await loadCachedGraph(rootPath);
|
|
422
422
|
const graph = await buildCodeGraph(rootPath, scan.files, cached);
|
|
423
423
|
await saveCachedGraph(rootPath, graph);
|
|
424
|
-
// Files scope
|
|
424
|
+
// Files scope - simple substring scan; ranking adds no value
|
|
425
425
|
if (scope === 'files') {
|
|
426
426
|
const q = query.toLowerCase();
|
|
427
427
|
const all = scan.files
|
|
@@ -430,7 +430,7 @@ const tools = [
|
|
|
430
430
|
const page = paginate(all, readPageParams(args), listChecksum(all));
|
|
431
431
|
return { scope, query, matches: page.items, total: page.total, nextCursor: page.nextCursor };
|
|
432
432
|
}
|
|
433
|
-
// Symbols scope
|
|
433
|
+
// Symbols scope - walk the graph's export table; rank exact/prefix/substring
|
|
434
434
|
if (scope === 'symbols') {
|
|
435
435
|
const q = query.toLowerCase();
|
|
436
436
|
const rawMatches = [];
|
|
@@ -453,7 +453,7 @@ const tools = [
|
|
|
453
453
|
const page = paginate(cleaned, readPageParams(args), listChecksum(cleaned));
|
|
454
454
|
return { scope, query, matches: page.items, total: page.total, nextCursor: page.nextCursor };
|
|
455
455
|
}
|
|
456
|
-
// Content or auto scope
|
|
456
|
+
// Content or auto scope - lexical BM25 by default, optionally semantic or hybrid
|
|
457
457
|
const mode = String(args.mode ?? 'lexical');
|
|
458
458
|
const index = await buildSearchIndex(rootPath, scan.files, graph);
|
|
459
459
|
const lexicalHits = searchIndex(index, query, { limit });
|
|
@@ -471,7 +471,7 @@ const tools = [
|
|
|
471
471
|
nextCursor: page.nextCursor,
|
|
472
472
|
};
|
|
473
473
|
}
|
|
474
|
-
// Semantic or hybrid
|
|
474
|
+
// Semantic or hybrid - both require the peer
|
|
475
475
|
const hasSemantic = await isSemanticAvailable();
|
|
476
476
|
if (!hasSemantic) {
|
|
477
477
|
return {
|
|
@@ -518,7 +518,7 @@ const tools = [
|
|
|
518
518
|
nextCursor: page.nextCursor,
|
|
519
519
|
};
|
|
520
520
|
}
|
|
521
|
-
// Hybrid
|
|
521
|
+
// Hybrid - reciprocal rank fusion
|
|
522
522
|
const fused = reciprocalRankFusion([lexicalHits, semHits]).slice(0, limit);
|
|
523
523
|
const enriched = await attachExcerpts(rootPath, fused.map((f) => ({
|
|
524
524
|
file: f.file,
|
|
@@ -127,7 +127,7 @@ export function reportCi(issues, threshold) {
|
|
|
127
127
|
const pass = score >= threshold;
|
|
128
128
|
const status = pass ? chalk.green('PASS') : chalk.red('FAIL');
|
|
129
129
|
const gradeColor = grade === 'A' || grade === 'B' ? chalk.green : grade === 'C' ? chalk.yellow : chalk.red;
|
|
130
|
-
console.log(`projscan: ${gradeColor(chalk.bold(`${grade} (${score}/100)`))}
|
|
130
|
+
console.log(`projscan: ${gradeColor(chalk.bold(`${grade} (${score}/100)`))} - ${errors} error${errors !== 1 ? 's' : ''}, ${warnings} warning${warnings !== 1 ? 's' : ''}, ${infos} info - ${status} (threshold: ${threshold})`);
|
|
131
131
|
if (!pass) {
|
|
132
132
|
for (const issue of issues) {
|
|
133
133
|
console.log(` ${severityIcon(issue.severity)} ${issue.title}`);
|
|
@@ -137,20 +137,20 @@ export function reportCi(issues, threshold) {
|
|
|
137
137
|
// ── Report: diff ──────────────────────────────────────────
|
|
138
138
|
export function reportDiff(diff) {
|
|
139
139
|
console.log(header('Health Diff'));
|
|
140
|
-
const arrow = diff.scoreDelta > 0 ? chalk.green('↑') : diff.scoreDelta < 0 ? chalk.red('↓') : chalk.dim('
|
|
140
|
+
const arrow = diff.scoreDelta > 0 ? chalk.green('↑') : diff.scoreDelta < 0 ? chalk.red('↓') : chalk.dim('-');
|
|
141
141
|
const delta = diff.scoreDelta > 0 ? `+${diff.scoreDelta}` : String(diff.scoreDelta);
|
|
142
142
|
console.log(`\n Score: ${diff.before.score} → ${diff.after.score} (${delta}) ${arrow}`);
|
|
143
143
|
console.log(` Grade: ${diff.before.grade} → ${diff.after.grade}`);
|
|
144
144
|
if (diff.resolvedIssues.length > 0) {
|
|
145
145
|
console.log(`\n ${chalk.green('✓')} Resolved (${diff.resolvedIssues.length}):`);
|
|
146
146
|
for (const title of diff.resolvedIssues) {
|
|
147
|
-
console.log(` ${chalk.green('
|
|
147
|
+
console.log(` ${chalk.green('-')} ${title}`);
|
|
148
148
|
}
|
|
149
149
|
}
|
|
150
150
|
if (diff.newIssues.length > 0) {
|
|
151
151
|
console.log(`\n ${chalk.red('✗')} New (${diff.newIssues.length}):`);
|
|
152
152
|
for (const title of diff.newIssues) {
|
|
153
|
-
console.log(` ${chalk.red('
|
|
153
|
+
console.log(` ${chalk.red('-')} ${title}`);
|
|
154
154
|
}
|
|
155
155
|
}
|
|
156
156
|
if (diff.resolvedIssues.length === 0 && diff.newIssues.length === 0) {
|
|
@@ -182,7 +182,7 @@ export function reportDiff(diff) {
|
|
|
182
182
|
if (hd.resolved.length > 0) {
|
|
183
183
|
console.log(`\n ${chalk.green('✓')} No longer tracked (${hd.resolved.length}):`);
|
|
184
184
|
for (const delta of hd.resolved.slice(0, 5)) {
|
|
185
|
-
console.log(` ${chalk.green('
|
|
185
|
+
console.log(` ${chalk.green('-')} ${delta.relativePath}`);
|
|
186
186
|
}
|
|
187
187
|
}
|
|
188
188
|
}
|
|
@@ -209,7 +209,7 @@ export function reportFixResults(results) {
|
|
|
209
209
|
console.log(` ${chalk.green('✔')} ${result.fix.title}`);
|
|
210
210
|
}
|
|
211
211
|
else {
|
|
212
|
-
console.log(` ${chalk.red('✗')} ${result.fix.title}
|
|
212
|
+
console.log(` ${chalk.red('✗')} ${result.fix.title} - ${chalk.dim(result.error ?? 'unknown error')}`);
|
|
213
213
|
}
|
|
214
214
|
}
|
|
215
215
|
console.log('');
|
|
@@ -356,7 +356,7 @@ export function reportFileInspection(insp) {
|
|
|
356
356
|
console.log(` ${chalk.bold('Last change:')} ${h.daysSinceLastChange} days ago`);
|
|
357
357
|
}
|
|
358
358
|
if (h.busFactorOne) {
|
|
359
|
-
console.log(` ${chalk.red('⚠')} Bus factor 1
|
|
359
|
+
console.log(` ${chalk.red('⚠')} Bus factor 1 - only one author has touched this.`);
|
|
360
360
|
}
|
|
361
361
|
if (h.reasons.length > 0) {
|
|
362
362
|
console.log(` ${chalk.dim(h.reasons.join(', '))}`);
|
|
@@ -493,10 +493,10 @@ export function reportUpgrade(preview) {
|
|
|
493
493
|
console.log(chalk.yellow(`\n ${preview.reason ?? 'Upgrade preview unavailable'}\n`));
|
|
494
494
|
return;
|
|
495
495
|
}
|
|
496
|
-
console.log(header(`Upgrade Preview
|
|
496
|
+
console.log(header(`Upgrade Preview - ${preview.name}`));
|
|
497
497
|
const drift = DRIFT_COLORS[preview.drift] ?? chalk.dim;
|
|
498
|
-
console.log(` Declared: ${chalk.dim(preview.declared ?? '
|
|
499
|
-
console.log(` Installed: ${chalk.bold(preview.installed ?? '
|
|
498
|
+
console.log(` Declared: ${chalk.dim(preview.declared ?? '-')}`);
|
|
499
|
+
console.log(` Installed: ${chalk.bold(preview.installed ?? '-')}`);
|
|
500
500
|
console.log(` Drift: ${drift(preview.drift.toUpperCase())}`);
|
|
501
501
|
console.log('');
|
|
502
502
|
if (preview.breakingMarkers.length > 0) {
|
|
@@ -539,7 +539,7 @@ export function reportCoverage(report) {
|
|
|
539
539
|
console.log(chalk.yellow(`\n ${report.reason ?? 'Coverage report unavailable'}\n`));
|
|
540
540
|
return;
|
|
541
541
|
}
|
|
542
|
-
console.log(header('Coverage × Hotspots
|
|
542
|
+
console.log(header('Coverage × Hotspots - "Scariest Untested Files"'));
|
|
543
543
|
const src = report.coverageSourceFile ? ` (${report.coverageSourceFile})` : '';
|
|
544
544
|
console.log(chalk.dim(` Source: ${report.coverageSource}${src}`));
|
|
545
545
|
console.log('');
|