projscan 0.9.2 → 0.11.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 +37 -7
- package/dist/analyzers/deadCodeCheck.d.ts +10 -12
- package/dist/analyzers/deadCodeCheck.js +41 -69
- package/dist/analyzers/deadCodeCheck.js.map +1 -1
- package/dist/analyzers/pythonDependencyRiskCheck.d.ts +2 -0
- package/dist/analyzers/pythonDependencyRiskCheck.js +114 -0
- package/dist/analyzers/pythonDependencyRiskCheck.js.map +1 -0
- package/dist/analyzers/pythonLinterCheck.d.ts +2 -0
- package/dist/analyzers/pythonLinterCheck.js +119 -0
- package/dist/analyzers/pythonLinterCheck.js.map +1 -0
- package/dist/analyzers/pythonTestCheck.d.ts +2 -0
- package/dist/analyzers/pythonTestCheck.js +97 -0
- package/dist/analyzers/pythonTestCheck.js.map +1 -0
- package/dist/analyzers/pythonUnusedDependencyCheck.d.ts +2 -0
- package/dist/analyzers/pythonUnusedDependencyCheck.js +76 -0
- package/dist/analyzers/pythonUnusedDependencyCheck.js.map +1 -0
- package/dist/cli/index.js +294 -10
- package/dist/cli/index.js.map +1 -1
- package/dist/core/ast.d.ts +2 -0
- package/dist/core/ast.js +35 -2
- package/dist/core/ast.js.map +1 -1
- package/dist/core/codeGraph.d.ts +8 -7
- package/dist/core/codeGraph.js +50 -72
- package/dist/core/codeGraph.js.map +1 -1
- package/dist/core/couplingAnalyzer.d.ts +18 -0
- package/dist/core/couplingAnalyzer.js +174 -0
- package/dist/core/couplingAnalyzer.js.map +1 -0
- package/dist/core/fileInspector.d.ts +3 -0
- package/dist/core/fileInspector.js +78 -3
- package/dist/core/fileInspector.js.map +1 -1
- package/dist/core/hotspotAnalyzer.d.ts +13 -0
- package/dist/core/hotspotAnalyzer.js +29 -6
- package/dist/core/hotspotAnalyzer.js.map +1 -1
- package/dist/core/indexCache.js +8 -1
- package/dist/core/indexCache.js.map +1 -1
- package/dist/core/issueEngine.js +10 -0
- package/dist/core/issueEngine.js.map +1 -1
- package/dist/core/languages/LanguageAdapter.d.ts +36 -0
- package/dist/core/languages/LanguageAdapter.js +2 -0
- package/dist/core/languages/LanguageAdapter.js.map +1 -0
- package/dist/core/languages/goAdapter.d.ts +2 -0
- package/dist/core/languages/goAdapter.js +136 -0
- package/dist/core/languages/goAdapter.js.map +1 -0
- package/dist/core/languages/goCyclomatic.d.ts +21 -0
- package/dist/core/languages/goCyclomatic.js +55 -0
- package/dist/core/languages/goCyclomatic.js.map +1 -0
- package/dist/core/languages/goExports.d.ts +26 -0
- package/dist/core/languages/goExports.js +89 -0
- package/dist/core/languages/goExports.js.map +1 -0
- package/dist/core/languages/goImports.d.ts +26 -0
- package/dist/core/languages/goImports.js +64 -0
- package/dist/core/languages/goImports.js.map +1 -0
- package/dist/core/languages/goManifests.d.ts +19 -0
- package/dist/core/languages/goManifests.js +56 -0
- package/dist/core/languages/goManifests.js.map +1 -0
- package/dist/core/languages/javascriptAdapter.d.ts +2 -0
- package/dist/core/languages/javascriptAdapter.js +68 -0
- package/dist/core/languages/javascriptAdapter.js.map +1 -0
- package/dist/core/languages/pythonAdapter.d.ts +6 -0
- package/dist/core/languages/pythonAdapter.js +147 -0
- package/dist/core/languages/pythonAdapter.js.map +1 -0
- package/dist/core/languages/pythonCyclomatic.d.ts +18 -0
- package/dist/core/languages/pythonCyclomatic.js +45 -0
- package/dist/core/languages/pythonCyclomatic.js.map +1 -0
- package/dist/core/languages/pythonExports.d.ts +28 -0
- package/dist/core/languages/pythonExports.js +169 -0
- package/dist/core/languages/pythonExports.js.map +1 -0
- package/dist/core/languages/pythonImports.d.ts +22 -0
- package/dist/core/languages/pythonImports.js +104 -0
- package/dist/core/languages/pythonImports.js.map +1 -0
- package/dist/core/languages/pythonManifests.d.ts +34 -0
- package/dist/core/languages/pythonManifests.js +344 -0
- package/dist/core/languages/pythonManifests.js.map +1 -0
- package/dist/core/languages/registry.d.ts +5 -0
- package/dist/core/languages/registry.js +31 -0
- package/dist/core/languages/registry.js.map +1 -0
- package/dist/core/languages/treeSitterLoader.d.ts +14 -0
- package/dist/core/languages/treeSitterLoader.js +76 -0
- package/dist/core/languages/treeSitterLoader.js.map +1 -0
- package/dist/core/monorepo.d.ts +20 -0
- package/dist/core/monorepo.js +270 -0
- package/dist/core/monorepo.js.map +1 -0
- package/dist/core/prDiff.d.ts +43 -0
- package/dist/core/prDiff.js +298 -0
- package/dist/core/prDiff.js.map +1 -0
- package/dist/core/searchIndex.js +8 -0
- package/dist/core/searchIndex.js.map +1 -1
- package/dist/core/telemetry.d.ts +90 -0
- package/dist/core/telemetry.js +199 -0
- package/dist/core/telemetry.js.map +1 -0
- package/dist/grammars/tree-sitter-go.wasm +0 -0
- package/dist/grammars/tree-sitter-python.wasm +0 -0
- package/dist/grammars/web-tree-sitter.wasm +0 -0
- package/dist/mcp/server.js +22 -0
- package/dist/mcp/server.js.map +1 -1
- package/dist/mcp/tools.js +317 -20
- package/dist/mcp/tools.js.map +1 -1
- package/dist/reporters/consoleReporter.d.ts +4 -1
- package/dist/reporters/consoleReporter.js +113 -0
- package/dist/reporters/consoleReporter.js.map +1 -1
- package/dist/reporters/jsonReporter.d.ts +4 -1
- package/dist/reporters/jsonReporter.js +9 -0
- package/dist/reporters/jsonReporter.js.map +1 -1
- package/dist/reporters/markdownReporter.d.ts +4 -1
- package/dist/reporters/markdownReporter.js +103 -3
- package/dist/reporters/markdownReporter.js.map +1 -1
- package/dist/types.d.ts +115 -0
- package/dist/utils/cache.d.ts +3 -0
- package/dist/utils/cache.js +51 -0
- package/dist/utils/cache.js.map +1 -0
- package/dist/utils/config.js +10 -0
- package/dist/utils/config.js.map +1 -1
- package/dist/utils/fileWalker.js +14 -0
- package/dist/utils/fileWalker.js.map +1 -1
- package/package.json +11 -5
|
@@ -0,0 +1,97 @@
|
|
|
1
|
+
import fs from 'node:fs/promises';
|
|
2
|
+
import path from 'node:path';
|
|
3
|
+
const TEST_FRAMEWORKS = ['pytest', 'unittest', 'nose', 'nose2', 'ward'];
|
|
4
|
+
function isPythonTestFile(rel) {
|
|
5
|
+
const base = path.basename(rel);
|
|
6
|
+
if (/^test_.+\.py$/.test(base))
|
|
7
|
+
return true;
|
|
8
|
+
if (/^.+_test\.py$/.test(base))
|
|
9
|
+
return true;
|
|
10
|
+
if (rel.startsWith('tests/') || rel.includes('/tests/'))
|
|
11
|
+
return base.endsWith('.py');
|
|
12
|
+
return false;
|
|
13
|
+
}
|
|
14
|
+
async function tryRead(absolutePath) {
|
|
15
|
+
try {
|
|
16
|
+
return await fs.readFile(absolutePath, 'utf-8');
|
|
17
|
+
}
|
|
18
|
+
catch {
|
|
19
|
+
return null;
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
export async function check(rootPath, files) {
|
|
23
|
+
const hasPython = files.some((f) => f.extension === '.py' || f.extension === '.pyw');
|
|
24
|
+
if (!hasPython)
|
|
25
|
+
return [];
|
|
26
|
+
const pyproject = await tryRead(path.join(rootPath, 'pyproject.toml'));
|
|
27
|
+
const setupCfg = await tryRead(path.join(rootPath, 'setup.cfg'));
|
|
28
|
+
const pytestIni = await tryRead(path.join(rootPath, 'pytest.ini'));
|
|
29
|
+
const toxIni = await tryRead(path.join(rootPath, 'tox.ini'));
|
|
30
|
+
// Collect all requirements*.txt contents into one searchable blob.
|
|
31
|
+
const reqRels = files
|
|
32
|
+
.filter((f) => (!f.directory || f.directory === '.') &&
|
|
33
|
+
/^requirements(-.*)?\.txt$/i.test(path.basename(f.relativePath)))
|
|
34
|
+
.map((f) => f.relativePath);
|
|
35
|
+
let requirementsBlob = '';
|
|
36
|
+
for (const rel of reqRels) {
|
|
37
|
+
const content = await tryRead(path.join(rootPath, rel));
|
|
38
|
+
if (content)
|
|
39
|
+
requirementsBlob += content + '\n';
|
|
40
|
+
}
|
|
41
|
+
let hasFramework = false;
|
|
42
|
+
const manifestHaystack = [pyproject ?? '', setupCfg ?? '', requirementsBlob].join('\n');
|
|
43
|
+
for (const fw of TEST_FRAMEWORKS) {
|
|
44
|
+
// Use a word-boundary-ish match so "pytest" doesn't spuriously match "pytest-cov"
|
|
45
|
+
// for purposes of "is pytest the framework?" - but we also accept pytest-*, so
|
|
46
|
+
// a simple case-insensitive containment is fine in practice.
|
|
47
|
+
const re = new RegExp(`(^|[\\s"'\`\\[\\],={}><~^!;])${fw}(\\b|[^a-zA-Z0-9_.-])`, 'im');
|
|
48
|
+
if (re.test(manifestHaystack)) {
|
|
49
|
+
hasFramework = true;
|
|
50
|
+
break;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
if (!hasFramework && pytestIni !== null)
|
|
54
|
+
hasFramework = true;
|
|
55
|
+
if (!hasFramework && toxIni !== null && /\[pytest\]|\[tool:pytest\]|pytest/i.test(toxIni)) {
|
|
56
|
+
hasFramework = true;
|
|
57
|
+
}
|
|
58
|
+
if (!hasFramework && pyproject !== null && /\[tool\.pytest\.ini_options\]/.test(pyproject)) {
|
|
59
|
+
hasFramework = true;
|
|
60
|
+
}
|
|
61
|
+
// Last-resort detection: `import unittest` (or `from unittest ...`) inside a
|
|
62
|
+
// pytest-conventional test file. Some projects rely on stdlib unittest and
|
|
63
|
+
// never declare a framework in manifests.
|
|
64
|
+
const testFiles = files.filter((f) => isPythonTestFile(f.relativePath));
|
|
65
|
+
if (!hasFramework) {
|
|
66
|
+
for (const f of testFiles) {
|
|
67
|
+
const content = await tryRead(f.absolutePath);
|
|
68
|
+
if (content && /^\s*(import\s+unittest|from\s+unittest\s+import)/m.test(content)) {
|
|
69
|
+
hasFramework = true;
|
|
70
|
+
break;
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
}
|
|
74
|
+
const issues = [];
|
|
75
|
+
if (!hasFramework) {
|
|
76
|
+
issues.push({
|
|
77
|
+
id: 'missing-python-test-framework',
|
|
78
|
+
title: 'No Python test framework detected',
|
|
79
|
+
description: 'No pytest/unittest configuration or dependency found. Testing is essential for code quality and reliability.',
|
|
80
|
+
severity: 'warning',
|
|
81
|
+
category: 'testing',
|
|
82
|
+
fixAvailable: false,
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
else if (testFiles.length === 0) {
|
|
86
|
+
issues.push({
|
|
87
|
+
id: 'no-python-test-files',
|
|
88
|
+
title: 'No Python test files found',
|
|
89
|
+
description: 'A Python test framework is configured but no test files were found (expected test_*.py, *_test.py, or under tests/).',
|
|
90
|
+
severity: 'info',
|
|
91
|
+
category: 'testing',
|
|
92
|
+
fixAvailable: false,
|
|
93
|
+
});
|
|
94
|
+
}
|
|
95
|
+
return issues;
|
|
96
|
+
}
|
|
97
|
+
//# sourceMappingURL=pythonTestCheck.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pythonTestCheck.js","sourceRoot":"","sources":["../../src/analyzers/pythonTestCheck.ts"],"names":[],"mappings":"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"}
|
|
@@ -0,0 +1,76 @@
|
|
|
1
|
+
import { detectPythonProject } from '../core/languages/pythonManifests.js';
|
|
2
|
+
import { buildCodeGraph } from '../core/codeGraph.js';
|
|
3
|
+
// Packages whose presence in manifests is implicit (build-time, test-time,
|
|
4
|
+
// or tooling) and which typically won't appear in `import` statements.
|
|
5
|
+
const IMPLICIT_USE = new Set([
|
|
6
|
+
'pytest',
|
|
7
|
+
'pytest-cov',
|
|
8
|
+
'pytest-mock',
|
|
9
|
+
'pytest-asyncio',
|
|
10
|
+
'pytest-xdist',
|
|
11
|
+
'ruff',
|
|
12
|
+
'black',
|
|
13
|
+
'mypy',
|
|
14
|
+
'coverage',
|
|
15
|
+
'wheel',
|
|
16
|
+
'build',
|
|
17
|
+
'setuptools',
|
|
18
|
+
'setuptools-scm',
|
|
19
|
+
'pip',
|
|
20
|
+
'pip-tools',
|
|
21
|
+
'twine',
|
|
22
|
+
'flake8',
|
|
23
|
+
'pylint',
|
|
24
|
+
'pyflakes',
|
|
25
|
+
'isort',
|
|
26
|
+
'bandit',
|
|
27
|
+
'tox',
|
|
28
|
+
'pre-commit',
|
|
29
|
+
'hatch',
|
|
30
|
+
'hatchling',
|
|
31
|
+
'poetry-core',
|
|
32
|
+
'maturin',
|
|
33
|
+
]);
|
|
34
|
+
/**
|
|
35
|
+
* Normalize a package name for comparison. PyPI names are case-insensitive
|
|
36
|
+
* and `_` / `-` / `.` are interchangeable per PEP 503.
|
|
37
|
+
*/
|
|
38
|
+
function normalize(name) {
|
|
39
|
+
return name.toLowerCase().replace(/[_.]/g, '-');
|
|
40
|
+
}
|
|
41
|
+
export async function check(rootPath, files) {
|
|
42
|
+
const info = await detectPythonProject(rootPath, files);
|
|
43
|
+
if (!info || info.declared.length === 0)
|
|
44
|
+
return [];
|
|
45
|
+
const pythonFiles = files.filter((f) => f.extension === '.py' || f.extension === '.pyw');
|
|
46
|
+
if (pythonFiles.length === 0)
|
|
47
|
+
return [];
|
|
48
|
+
const graph = await buildCodeGraph(rootPath, pythonFiles);
|
|
49
|
+
const usedPackages = new Set();
|
|
50
|
+
for (const pkg of graph.packageImporters.keys()) {
|
|
51
|
+
usedPackages.add(normalize(pkg));
|
|
52
|
+
}
|
|
53
|
+
const issues = [];
|
|
54
|
+
const seen = new Set();
|
|
55
|
+
for (const dep of info.declared) {
|
|
56
|
+
const norm = normalize(dep.name);
|
|
57
|
+
if (seen.has(norm))
|
|
58
|
+
continue;
|
|
59
|
+
if (IMPLICIT_USE.has(norm))
|
|
60
|
+
continue;
|
|
61
|
+
if (usedPackages.has(norm))
|
|
62
|
+
continue;
|
|
63
|
+
seen.add(norm);
|
|
64
|
+
issues.push({
|
|
65
|
+
id: `unused-python-dependency-${dep.name}`,
|
|
66
|
+
title: `Unused Python dependency: ${dep.name}`,
|
|
67
|
+
description: `\`${dep.name}\` is declared in ${dep.source} but no source file imports it. Either remove it or add it to the implicit-use allowlist.`,
|
|
68
|
+
severity: dep.scope === 'dev' ? 'info' : 'warning',
|
|
69
|
+
category: 'dependencies',
|
|
70
|
+
fixAvailable: false,
|
|
71
|
+
locations: [{ file: dep.source, line: dep.line }],
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
return issues;
|
|
75
|
+
}
|
|
76
|
+
//# sourceMappingURL=pythonUnusedDependencyCheck.js.map
|
|
@@ -0,0 +1 @@
|
|
|
1
|
+
{"version":3,"file":"pythonUnusedDependencyCheck.js","sourceRoot":"","sources":["../../src/analyzers/pythonUnusedDependencyCheck.ts"],"names":[],"mappings":"AACA,OAAO,EAAE,mBAAmB,EAAE,MAAM,sCAAsC,CAAC;AAC3E,OAAO,EAAE,cAAc,EAAE,MAAM,sBAAsB,CAAC;AAEtD,2EAA2E;AAC3E,uEAAuE;AACvE,MAAM,YAAY,GAAG,IAAI,GAAG,CAAC;IAC3B,QAAQ;IACR,YAAY;IACZ,aAAa;IACb,gBAAgB;IAChB,cAAc;IACd,MAAM;IACN,OAAO;IACP,MAAM;IACN,UAAU;IACV,OAAO;IACP,OAAO;IACP,YAAY;IACZ,gBAAgB;IAChB,KAAK;IACL,WAAW;IACX,OAAO;IACP,QAAQ;IACR,QAAQ;IACR,UAAU;IACV,OAAO;IACP,QAAQ;IACR,KAAK;IACL,YAAY;IACZ,OAAO;IACP,WAAW;IACX,aAAa;IACb,SAAS;CACV,CAAC,CAAC;AAEH;;;GAGG;AACH,SAAS,SAAS,CAAC,IAAY;IAC7B,OAAO,IAAI,CAAC,WAAW,EAAE,CAAC,OAAO,CAAC,OAAO,EAAE,GAAG,CAAC,CAAC;AAClD,CAAC;AAED,MAAM,CAAC,KAAK,UAAU,KAAK,CAAC,QAAgB,EAAE,KAAkB;IAC9D,MAAM,IAAI,GAAG,MAAM,mBAAmB,CAAC,QAAQ,EAAE,KAAK,CAAC,CAAC;IACxD,IAAI,CAAC,IAAI,IAAI,IAAI,CAAC,QAAQ,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAEnD,MAAM,WAAW,GAAG,KAAK,CAAC,MAAM,CAAC,CAAC,CAAC,EAAE,EAAE,CAAC,CAAC,CAAC,SAAS,KAAK,KAAK,IAAI,CAAC,CAAC,SAAS,KAAK,MAAM,CAAC,CAAC;IACzF,IAAI,WAAW,CAAC,MAAM,KAAK,CAAC;QAAE,OAAO,EAAE,CAAC;IAExC,MAAM,KAAK,GAAG,MAAM,cAAc,CAAC,QAAQ,EAAE,WAAW,CAAC,CAAC;IAC1D,MAAM,YAAY,GAAG,IAAI,GAAG,EAAU,CAAC;IACvC,KAAK,MAAM,GAAG,IAAI,KAAK,CAAC,gBAAgB,CAAC,IAAI,EAAE,EAAE,CAAC;QAChD,YAAY,CAAC,GAAG,CAAC,SAAS,CAAC,GAAG,CAAC,CAAC,CAAC;IACnC,CAAC;IAED,MAAM,MAAM,GAAY,EAAE,CAAC;IAC3B,MAAM,IAAI,GAAG,IAAI,GAAG,EAAU,CAAC;IAE/B,KAAK,MAAM,GAAG,IAAI,IAAI,CAAC,QAAQ,EAAE,CAAC;QAChC,MAAM,IAAI,GAAG,SAAS,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACjC,IAAI,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,SAAS;QAC7B,IAAI,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,SAAS;QACrC,IAAI,YAAY,CAAC,GAAG,CAAC,IAAI,CAAC;YAAE,SAAS;QAErC,IAAI,CAAC,GAAG,CAAC,IAAI,CAAC,CAAC;QACf,MAAM,CAAC,IAAI,CAAC;YACV,EAAE,EAAE,4BAA4B,GAAG,CAAC,IAAI,EAAE;YAC1C,KAAK,EAAE,6BAA6B,GAAG,CAAC,IAAI,EAAE;YAC9C,WAAW,EAAE,KAAK,GAAG,CAAC,IAAI,qBAAqB,GAAG,CAAC,MAAM,2FAA2F;YACpJ,QAAQ,EAAE,GAAG,CAAC,KAAK,KAAK,KAAK,CAAC,CAAC,CAAC,MAAM,CAAC,CAAC,CAAC,SAAS;YAClD,QAAQ,EAAE,cAAc;YACxB,YAAY,EAAE,KAAK;YACnB,SAAS,EAAE,CAAC,EAAE,IAAI,EAAE,GAAG,CAAC,MAAM,EAAE,IAAI,EAAE,GAAG,CAAC,IAAI,EAAE,CAAC;SAClD,CAAC,CAAC;IACL,CAAC;IAED,OAAO,MAAM,CAAC;AAChB,CAAC"}
|
package/dist/cli/index.js
CHANGED
|
@@ -22,6 +22,10 @@ import { parseCoverage, coverageMap } from '../core/coverageParser.js';
|
|
|
22
22
|
import { joinCoverageWithHotspots } from '../core/coverageJoin.js';
|
|
23
23
|
import { buildCodeGraph } from '../core/codeGraph.js';
|
|
24
24
|
import { loadCachedGraph, saveCachedGraph } from '../core/indexCache.js';
|
|
25
|
+
import { computeCoupling, filterCoupling } from '../core/couplingAnalyzer.js';
|
|
26
|
+
import { computePrDiff } from '../core/prDiff.js';
|
|
27
|
+
import { detectWorkspaces, filterFilesByPackage } from '../core/monorepo.js';
|
|
28
|
+
import { describeTelemetryConfig, aggregateTelemetry } from '../core/telemetry.js';
|
|
25
29
|
import { buildSearchIndex, search as searchIndex, attachExcerpts, expandQuery } from '../core/searchIndex.js';
|
|
26
30
|
import { buildSemanticIndex, semanticSearch, reciprocalRankFusion, } from '../core/semanticSearch.js';
|
|
27
31
|
import { isSemanticAvailable } from '../core/embeddings.js';
|
|
@@ -34,9 +38,9 @@ import { saveBaseline, loadBaseline, computeDiff } from '../utils/baseline.js';
|
|
|
34
38
|
import { loadConfig, applyConfigToIssues } from '../utils/config.js';
|
|
35
39
|
import { getChangedFiles } from '../utils/changedFiles.js';
|
|
36
40
|
import { runMcpServer } from '../mcp/server.js';
|
|
37
|
-
import { reportAnalysis, reportHealth, reportCi, reportDiff, reportDetectedIssues, reportExplanation, reportDiagram, reportStructure, reportDependencies, reportHotspots, reportFileInspection, reportOutdated, reportAudit, reportUpgrade, reportCoverage, } from '../reporters/consoleReporter.js';
|
|
38
|
-
import { reportAnalysisJson, reportHealthJson, reportCiJson, reportDiffJson, reportExplanationJson, reportDiagramJson, reportStructureJson, reportDependenciesJson, reportHotspotsJson, reportFileJson, reportOutdatedJson, reportAuditJson, reportUpgradeJson, reportCoverageJson, } from '../reporters/jsonReporter.js';
|
|
39
|
-
import { reportAnalysisMarkdown, reportHealthMarkdown, reportCiMarkdown, reportDiffMarkdown, reportExplanationMarkdown, reportDiagramMarkdown, reportStructureMarkdown, reportDependenciesMarkdown, reportHotspotsMarkdown, reportFileMarkdown, reportOutdatedMarkdown, reportAuditMarkdown, reportUpgradeMarkdown, reportCoverageMarkdown, } from '../reporters/markdownReporter.js';
|
|
41
|
+
import { reportAnalysis, reportHealth, reportCi, reportDiff, reportDetectedIssues, reportExplanation, reportDiagram, reportStructure, reportDependencies, reportHotspots, reportFileInspection, reportOutdated, reportAudit, reportUpgrade, reportCoverage, reportCoupling, reportPrDiff, reportWorkspaces, } from '../reporters/consoleReporter.js';
|
|
42
|
+
import { reportAnalysisJson, reportHealthJson, reportCiJson, reportDiffJson, reportExplanationJson, reportDiagramJson, reportStructureJson, reportDependenciesJson, reportHotspotsJson, reportFileJson, reportOutdatedJson, reportAuditJson, reportUpgradeJson, reportCoverageJson, reportCouplingJson, reportPrDiffJson, reportWorkspacesJson, } from '../reporters/jsonReporter.js';
|
|
43
|
+
import { reportAnalysisMarkdown, reportHealthMarkdown, reportCiMarkdown, reportDiffMarkdown, reportExplanationMarkdown, reportDiagramMarkdown, reportStructureMarkdown, reportDependenciesMarkdown, reportHotspotsMarkdown, reportFileMarkdown, reportOutdatedMarkdown, reportAuditMarkdown, reportUpgradeMarkdown, reportCoverageMarkdown, reportCouplingMarkdown, reportPrDiffMarkdown, reportWorkspacesMarkdown, } from '../reporters/markdownReporter.js';
|
|
40
44
|
import { reportAnalysisSarif, reportHealthSarif, reportCiSarif, issuesToSarif, } from '../reporters/sarifReporter.js';
|
|
41
45
|
// ── CLI Setup ─────────────────────────────────────────────
|
|
42
46
|
const program = new Command();
|
|
@@ -137,12 +141,24 @@ function maybeCompactBanner() {
|
|
|
137
141
|
}
|
|
138
142
|
}
|
|
139
143
|
}
|
|
144
|
+
/** Walk a DirectoryNode to find the node whose `path` matches targetPath. */
|
|
145
|
+
function sliceCliTree(node, targetPath) {
|
|
146
|
+
if (node.path === targetPath)
|
|
147
|
+
return node;
|
|
148
|
+
for (const child of node.children) {
|
|
149
|
+
const hit = sliceCliTree(child, targetPath);
|
|
150
|
+
if (hit)
|
|
151
|
+
return hit;
|
|
152
|
+
}
|
|
153
|
+
return null;
|
|
154
|
+
}
|
|
140
155
|
// ── Command: analyze (default) ────────────────────────────
|
|
141
156
|
program
|
|
142
157
|
.command('analyze', { isDefault: true })
|
|
143
158
|
.description('Analyze repository and show project report')
|
|
144
159
|
.option('--changed-only', 'only report issues on files changed vs base ref')
|
|
145
160
|
.option('--base-ref <ref>', 'git base ref for --changed-only (default: origin/main)')
|
|
161
|
+
.option('--package <name>', 'monorepo: scope issues to a single workspace package')
|
|
146
162
|
.action(async (cmdOpts) => {
|
|
147
163
|
setupLogLevel();
|
|
148
164
|
maybeBanner();
|
|
@@ -168,6 +184,11 @@ program
|
|
|
168
184
|
if (cmdOpts.changedOnly) {
|
|
169
185
|
issues = await filterIssuesByChangedFiles(issues, rootPath, cmdOpts.baseRef ?? config.baseRef);
|
|
170
186
|
}
|
|
187
|
+
if (cmdOpts.package) {
|
|
188
|
+
const ws = await detectWorkspaces(rootPath);
|
|
189
|
+
const allowed = new Set(filterFilesByPackage(ws, cmdOpts.package, scan.files.map((f) => f.relativePath)));
|
|
190
|
+
issues = issues.filter((i) => (i.locations ?? []).some((l) => l.file && allowed.has(l.file)));
|
|
191
|
+
}
|
|
171
192
|
if (spinner)
|
|
172
193
|
spinner.stop();
|
|
173
194
|
const report = {
|
|
@@ -207,6 +228,7 @@ program
|
|
|
207
228
|
.description('Evaluate project health and detect issues')
|
|
208
229
|
.option('--changed-only', 'only report issues on files changed vs base ref')
|
|
209
230
|
.option('--base-ref <ref>', 'git base ref for --changed-only (default: origin/main)')
|
|
231
|
+
.option('--package <name>', 'monorepo: scope issues to a single workspace package')
|
|
210
232
|
.action(async (cmdOpts) => {
|
|
211
233
|
setupLogLevel();
|
|
212
234
|
maybeCompactBanner();
|
|
@@ -221,6 +243,11 @@ program
|
|
|
221
243
|
if (cmdOpts.changedOnly) {
|
|
222
244
|
issues = await filterIssuesByChangedFiles(issues, rootPath, cmdOpts.baseRef ?? config.baseRef);
|
|
223
245
|
}
|
|
246
|
+
if (cmdOpts.package) {
|
|
247
|
+
const ws = await detectWorkspaces(rootPath);
|
|
248
|
+
const allowed = new Set(filterFilesByPackage(ws, cmdOpts.package, scan.files.map((f) => f.relativePath)));
|
|
249
|
+
issues = issues.filter((i) => (i.locations ?? []).some((l) => l.file && allowed.has(l.file)));
|
|
250
|
+
}
|
|
224
251
|
if (spinner)
|
|
225
252
|
spinner.stop();
|
|
226
253
|
switch (format) {
|
|
@@ -516,7 +543,8 @@ program
|
|
|
516
543
|
program
|
|
517
544
|
.command('structure')
|
|
518
545
|
.description('Show project directory structure')
|
|
519
|
-
.
|
|
546
|
+
.option('--package <name>', 'monorepo: scope tree to a single workspace package')
|
|
547
|
+
.action(async (cmdOpts) => {
|
|
520
548
|
setupLogLevel();
|
|
521
549
|
maybeCompactBanner();
|
|
522
550
|
const rootPath = getRootPath();
|
|
@@ -525,17 +553,30 @@ program
|
|
|
525
553
|
const spinner = format === 'console' ? ora('Scanning...').start() : null;
|
|
526
554
|
try {
|
|
527
555
|
const scan = await scanRepository(rootPath, { ignore: config.ignore });
|
|
556
|
+
let tree = scan.directoryTree;
|
|
557
|
+
let title = path.basename(rootPath);
|
|
558
|
+
if (cmdOpts.package) {
|
|
559
|
+
const ws = await detectWorkspaces(rootPath);
|
|
560
|
+
const pkg = ws.packages.find((p) => p.name === cmdOpts.package);
|
|
561
|
+
if (pkg && !pkg.isRoot && pkg.relativePath) {
|
|
562
|
+
const sliced = sliceCliTree(tree, pkg.relativePath);
|
|
563
|
+
if (sliced) {
|
|
564
|
+
tree = sliced;
|
|
565
|
+
title = pkg.name;
|
|
566
|
+
}
|
|
567
|
+
}
|
|
568
|
+
}
|
|
528
569
|
if (spinner)
|
|
529
570
|
spinner.stop();
|
|
530
571
|
switch (format) {
|
|
531
572
|
case 'json':
|
|
532
|
-
reportStructureJson(
|
|
573
|
+
reportStructureJson(tree);
|
|
533
574
|
break;
|
|
534
575
|
case 'markdown':
|
|
535
|
-
reportStructureMarkdown(
|
|
576
|
+
reportStructureMarkdown(tree);
|
|
536
577
|
break;
|
|
537
578
|
default:
|
|
538
|
-
reportStructure(
|
|
579
|
+
reportStructure(tree, title);
|
|
539
580
|
}
|
|
540
581
|
}
|
|
541
582
|
catch (error) {
|
|
@@ -584,9 +625,10 @@ program
|
|
|
584
625
|
// ── Command: hotspots ─────────────────────────────────────
|
|
585
626
|
program
|
|
586
627
|
.command('hotspots')
|
|
587
|
-
.description('Rank files by risk (git churn × complexity × open issues)')
|
|
628
|
+
.description('Rank files by risk (git churn × AST cyclomatic complexity × open issues)')
|
|
588
629
|
.option('--limit <n>', 'number of hotspots to show')
|
|
589
630
|
.option('--since <when>', 'git history window (e.g. "6 months ago", "2024-01-01")')
|
|
631
|
+
.option('--package <name>', 'monorepo: scope to a single workspace package')
|
|
590
632
|
.action(async (cmdOpts) => {
|
|
591
633
|
setupLogLevel();
|
|
592
634
|
maybeCompactBanner();
|
|
@@ -602,11 +644,22 @@ program
|
|
|
602
644
|
const limit = Math.max(1, Math.min(100, typeof limitRaw === 'string' ? parseInt(limitRaw, 10) || 10 : limitRaw));
|
|
603
645
|
const since = cmdOpts.since ?? config.hotspots?.since ?? '12 months ago';
|
|
604
646
|
const coverageReport = await parseCoverage(rootPath);
|
|
647
|
+
// Build the code graph so the risk score uses AST cyclomatic complexity
|
|
648
|
+
// instead of LOC. Cache hit makes this nearly free on repeat runs.
|
|
649
|
+
const cached = await loadCachedGraph(rootPath);
|
|
650
|
+
const graph = await buildCodeGraph(rootPath, scan.files, cached);
|
|
651
|
+
await saveCachedGraph(rootPath, graph);
|
|
605
652
|
const report = await analyzeHotspots(rootPath, scan.files, issues, {
|
|
606
653
|
since,
|
|
607
654
|
limit,
|
|
608
655
|
coverage: coverageReport.available ? coverageMap(coverageReport) : undefined,
|
|
656
|
+
graph,
|
|
609
657
|
});
|
|
658
|
+
if (cmdOpts.package) {
|
|
659
|
+
const ws = await detectWorkspaces(rootPath);
|
|
660
|
+
const allowed = new Set(filterFilesByPackage(ws, cmdOpts.package, report.hotspots.map((h) => h.relativePath)));
|
|
661
|
+
report.hotspots = report.hotspots.filter((h) => allowed.has(h.relativePath));
|
|
662
|
+
}
|
|
610
663
|
if (spinner)
|
|
611
664
|
spinner.stop();
|
|
612
665
|
switch (format) {
|
|
@@ -627,6 +680,209 @@ program
|
|
|
627
680
|
process.exit(1);
|
|
628
681
|
}
|
|
629
682
|
});
|
|
683
|
+
// ── Command: coupling ─────────────────────────────────────
|
|
684
|
+
program
|
|
685
|
+
.command('coupling')
|
|
686
|
+
.description('Per-file fan-in / fan-out / instability and circular-import cycles (AST-derived)')
|
|
687
|
+
.option('--limit <n>', 'number of files to show (default 25)')
|
|
688
|
+
.option('--cycles-only', 'only show files participating in import cycles')
|
|
689
|
+
.option('--high-fan-in', 'sort by fan-in (most-depended-on first)')
|
|
690
|
+
.option('--high-fan-out', 'sort by fan-out (most-coupled first)')
|
|
691
|
+
.option('--file <path>', 'restrict output to a single file')
|
|
692
|
+
.option('--package <name>', 'monorepo: scope to a single workspace package')
|
|
693
|
+
.action(async (cmdOpts) => {
|
|
694
|
+
setupLogLevel();
|
|
695
|
+
maybeCompactBanner();
|
|
696
|
+
const rootPath = getRootPath();
|
|
697
|
+
const format = getFormat();
|
|
698
|
+
const config = await loadProjectConfig();
|
|
699
|
+
const spinner = format === 'console' ? ora('Computing coupling + cycles...').start() : null;
|
|
700
|
+
try {
|
|
701
|
+
const scan = await scanRepository(rootPath, { ignore: config.ignore });
|
|
702
|
+
const cached = await loadCachedGraph(rootPath);
|
|
703
|
+
const graph = await buildCodeGraph(rootPath, scan.files, cached);
|
|
704
|
+
await saveCachedGraph(rootPath, graph);
|
|
705
|
+
const ws = await detectWorkspaces(rootPath);
|
|
706
|
+
const report = computeCoupling(graph, ws);
|
|
707
|
+
const direction = cmdOpts.cyclesOnly
|
|
708
|
+
? 'cycles_only'
|
|
709
|
+
: cmdOpts.highFanIn
|
|
710
|
+
? 'high_fan_in'
|
|
711
|
+
: cmdOpts.highFanOut
|
|
712
|
+
? 'high_fan_out'
|
|
713
|
+
: 'all';
|
|
714
|
+
const limitRaw = cmdOpts.limit ?? 25;
|
|
715
|
+
const limit = Math.max(1, Math.min(500, typeof limitRaw === 'string' ? parseInt(limitRaw, 10) || 25 : limitRaw));
|
|
716
|
+
let files = filterCoupling(report, direction);
|
|
717
|
+
if (cmdOpts.file)
|
|
718
|
+
files = files.filter((f) => f.relativePath === cmdOpts.file);
|
|
719
|
+
if (cmdOpts.package) {
|
|
720
|
+
const ws = await detectWorkspaces(rootPath);
|
|
721
|
+
const allowed = new Set(filterFilesByPackage(ws, cmdOpts.package, files.map((f) => f.relativePath)));
|
|
722
|
+
files = files.filter((f) => allowed.has(f.relativePath));
|
|
723
|
+
}
|
|
724
|
+
files = files.slice(0, limit);
|
|
725
|
+
const filtered = {
|
|
726
|
+
files,
|
|
727
|
+
cycles: report.cycles,
|
|
728
|
+
crossPackageEdges: report.crossPackageEdges,
|
|
729
|
+
totalFiles: report.totalFiles,
|
|
730
|
+
totalCycles: report.totalCycles,
|
|
731
|
+
totalCrossPackageEdges: report.totalCrossPackageEdges,
|
|
732
|
+
};
|
|
733
|
+
if (spinner)
|
|
734
|
+
spinner.stop();
|
|
735
|
+
switch (format) {
|
|
736
|
+
case 'json':
|
|
737
|
+
reportCouplingJson(filtered);
|
|
738
|
+
break;
|
|
739
|
+
case 'markdown':
|
|
740
|
+
reportCouplingMarkdown(filtered);
|
|
741
|
+
break;
|
|
742
|
+
default:
|
|
743
|
+
reportCoupling(filtered);
|
|
744
|
+
}
|
|
745
|
+
}
|
|
746
|
+
catch (error) {
|
|
747
|
+
if (spinner)
|
|
748
|
+
spinner.fail('Coupling analysis failed');
|
|
749
|
+
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
|
|
750
|
+
process.exit(1);
|
|
751
|
+
}
|
|
752
|
+
});
|
|
753
|
+
// ── Command: pr-diff ──────────────────────────────────────
|
|
754
|
+
program
|
|
755
|
+
.command('pr-diff')
|
|
756
|
+
.description('Structural (AST) diff between two refs - what changed in exports, imports, calls, CC, fan-in')
|
|
757
|
+
.option('--base <ref>', 'base ref (default: origin/main, falling back to main/master/HEAD~1)')
|
|
758
|
+
.option('--head <ref>', 'head ref (default: HEAD)')
|
|
759
|
+
.option('--package <name>', 'monorepo: scope diff to a single workspace package')
|
|
760
|
+
.action(async (cmdOpts) => {
|
|
761
|
+
setupLogLevel();
|
|
762
|
+
maybeCompactBanner();
|
|
763
|
+
const rootPath = getRootPath();
|
|
764
|
+
const format = getFormat();
|
|
765
|
+
const spinner = format === 'console' ? ora('Computing structural PR diff...').start() : null;
|
|
766
|
+
try {
|
|
767
|
+
const report = await computePrDiff(rootPath, { base: cmdOpts.base, head: cmdOpts.head });
|
|
768
|
+
if (cmdOpts.package) {
|
|
769
|
+
const ws = await detectWorkspaces(rootPath);
|
|
770
|
+
const collected = [
|
|
771
|
+
...report.filesAdded,
|
|
772
|
+
...report.filesRemoved,
|
|
773
|
+
...report.filesModified.map((f) => f.relativePath),
|
|
774
|
+
];
|
|
775
|
+
const allowed = new Set(filterFilesByPackage(ws, cmdOpts.package, collected));
|
|
776
|
+
report.filesAdded = report.filesAdded.filter((f) => allowed.has(f));
|
|
777
|
+
report.filesRemoved = report.filesRemoved.filter((f) => allowed.has(f));
|
|
778
|
+
report.filesModified = report.filesModified.filter((f) => allowed.has(f.relativePath));
|
|
779
|
+
report.totalFilesChanged =
|
|
780
|
+
report.filesAdded.length + report.filesRemoved.length + report.filesModified.length;
|
|
781
|
+
}
|
|
782
|
+
if (spinner)
|
|
783
|
+
spinner.stop();
|
|
784
|
+
switch (format) {
|
|
785
|
+
case 'json':
|
|
786
|
+
reportPrDiffJson(report);
|
|
787
|
+
break;
|
|
788
|
+
case 'markdown':
|
|
789
|
+
reportPrDiffMarkdown(report);
|
|
790
|
+
break;
|
|
791
|
+
default:
|
|
792
|
+
reportPrDiff(report);
|
|
793
|
+
}
|
|
794
|
+
}
|
|
795
|
+
catch (error) {
|
|
796
|
+
if (spinner)
|
|
797
|
+
spinner.fail('PR diff failed');
|
|
798
|
+
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
|
|
799
|
+
process.exit(1);
|
|
800
|
+
}
|
|
801
|
+
});
|
|
802
|
+
// ── Command: workspaces ───────────────────────────────────
|
|
803
|
+
program
|
|
804
|
+
.command('workspaces')
|
|
805
|
+
.description('List monorepo workspace packages (npm/yarn workspaces, pnpm-workspace.yaml, Nx/Turbo/Lerna fallback)')
|
|
806
|
+
.action(async () => {
|
|
807
|
+
setupLogLevel();
|
|
808
|
+
maybeCompactBanner();
|
|
809
|
+
const rootPath = getRootPath();
|
|
810
|
+
const format = getFormat();
|
|
811
|
+
try {
|
|
812
|
+
const info = await detectWorkspaces(rootPath);
|
|
813
|
+
switch (format) {
|
|
814
|
+
case 'json':
|
|
815
|
+
reportWorkspacesJson(info);
|
|
816
|
+
break;
|
|
817
|
+
case 'markdown':
|
|
818
|
+
reportWorkspacesMarkdown(info);
|
|
819
|
+
break;
|
|
820
|
+
default:
|
|
821
|
+
reportWorkspaces(info);
|
|
822
|
+
}
|
|
823
|
+
}
|
|
824
|
+
catch (error) {
|
|
825
|
+
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
|
|
826
|
+
process.exit(1);
|
|
827
|
+
}
|
|
828
|
+
});
|
|
829
|
+
// ── Command: telemetry ────────────────────────────────────
|
|
830
|
+
program
|
|
831
|
+
.command('telemetry')
|
|
832
|
+
.description('Inspect projscan opt-in telemetry: config state, or per-tool histograms with --aggregate')
|
|
833
|
+
.option('--aggregate', 'read the local sink and print per-tool latency histograms (count, p50/p95/p99, error rate)')
|
|
834
|
+
.action(async (cmdOpts) => {
|
|
835
|
+
setupLogLevel();
|
|
836
|
+
maybeCompactBanner();
|
|
837
|
+
const rootPath = getRootPath();
|
|
838
|
+
const format = getFormat();
|
|
839
|
+
try {
|
|
840
|
+
const { config } = await loadConfig(rootPath);
|
|
841
|
+
const out = cmdOpts.aggregate
|
|
842
|
+
? await aggregateTelemetry(config.telemetry)
|
|
843
|
+
: describeTelemetryConfig(config.telemetry);
|
|
844
|
+
if (format === 'json') {
|
|
845
|
+
console.log(JSON.stringify(out, null, 2));
|
|
846
|
+
return;
|
|
847
|
+
}
|
|
848
|
+
// Console: hand-formatted summary so users don't have to read raw JSON.
|
|
849
|
+
if (cmdOpts.aggregate) {
|
|
850
|
+
const agg = out;
|
|
851
|
+
if (!agg.available) {
|
|
852
|
+
console.log(chalk.yellow(`\n ${agg.reason ?? 'No telemetry available.'}\n`));
|
|
853
|
+
return;
|
|
854
|
+
}
|
|
855
|
+
console.log(chalk.bold('\n Telemetry histograms'));
|
|
856
|
+
console.log(chalk.dim(` sink: ${agg.sink}`));
|
|
857
|
+
console.log(chalk.dim(` ${agg.totalEvents} event(s) · ${agg.windowFrom ?? '?'} → ${agg.windowTo ?? '?'}\n`));
|
|
858
|
+
if (agg.byTool.length === 0) {
|
|
859
|
+
console.log(chalk.dim(' (no events recorded yet)\n'));
|
|
860
|
+
return;
|
|
861
|
+
}
|
|
862
|
+
const colHead = ` ${'count'.padStart(6)} ${'err%'.padStart(5)} ${'p50'.padStart(6)} ${'p95'.padStart(6)} ${'p99'.padStart(6)} tool`;
|
|
863
|
+
console.log(chalk.dim(colHead));
|
|
864
|
+
for (const t of agg.byTool) {
|
|
865
|
+
const errPct = (t.errorRate * 100).toFixed(1) + '%';
|
|
866
|
+
console.log(` ${String(t.count).padStart(6)} ${errPct.padStart(5)} ${(t.p50Ms ?? 0).toString().padStart(6)} ${(t.p95Ms ?? 0).toString().padStart(6)} ${(t.p99Ms ?? 0).toString().padStart(6)} ${chalk.cyan(t.tool)}`);
|
|
867
|
+
}
|
|
868
|
+
console.log('');
|
|
869
|
+
}
|
|
870
|
+
else {
|
|
871
|
+
const cfg = out;
|
|
872
|
+
console.log(chalk.bold('\n Telemetry'));
|
|
873
|
+
console.log(` enabled: ${cfg.enabled ? chalk.green('yes') : chalk.dim('no (default)')}`);
|
|
874
|
+
console.log(` sink: ${cfg.sink}`);
|
|
875
|
+
console.log(` default: ${cfg.defaultSink}`);
|
|
876
|
+
console.log(` PROJSCAN_TELEMETRY env: ${cfg.envOverride ?? chalk.dim('(unset)')}`);
|
|
877
|
+
console.log(chalk.dim('\n Records: tool, durationMs, ok, version, ts. Never source/paths/args.'));
|
|
878
|
+
console.log(chalk.dim(' Re-run with --aggregate to see histograms over the recorded events.\n'));
|
|
879
|
+
}
|
|
880
|
+
}
|
|
881
|
+
catch (error) {
|
|
882
|
+
console.error(chalk.red(error instanceof Error ? error.message : String(error)));
|
|
883
|
+
process.exit(1);
|
|
884
|
+
}
|
|
885
|
+
});
|
|
630
886
|
// ── Command: outdated ─────────────────────────────────────
|
|
631
887
|
program
|
|
632
888
|
.command('outdated')
|
|
@@ -741,6 +997,7 @@ program
|
|
|
741
997
|
.option('--mode <mode>', 'lexical | semantic | hybrid (content/auto scope only)', 'lexical')
|
|
742
998
|
.option('--semantic', 'shortcut for --mode semantic')
|
|
743
999
|
.option('--limit <n>', 'max results', '15')
|
|
1000
|
+
.option('--package <name>', 'monorepo: scope to a single workspace package')
|
|
744
1001
|
.action(async (queryParts, cmdOpts) => {
|
|
745
1002
|
setupLogLevel();
|
|
746
1003
|
maybeCompactBanner();
|
|
@@ -761,6 +1018,22 @@ program
|
|
|
761
1018
|
const cached = await loadCachedGraph(rootPath);
|
|
762
1019
|
const graph = await buildCodeGraph(rootPath, scan.files, cached);
|
|
763
1020
|
await saveCachedGraph(rootPath, graph);
|
|
1021
|
+
// Build a (file -> bool) filter once if --package is set; reused below.
|
|
1022
|
+
let passes = null;
|
|
1023
|
+
if (cmdOpts.package) {
|
|
1024
|
+
const ws = await detectWorkspaces(rootPath);
|
|
1025
|
+
const pkg = ws.packages.find((p) => p.name === cmdOpts.package);
|
|
1026
|
+
if (!pkg) {
|
|
1027
|
+
passes = () => false;
|
|
1028
|
+
}
|
|
1029
|
+
else if (pkg.isRoot) {
|
|
1030
|
+
passes = () => true;
|
|
1031
|
+
}
|
|
1032
|
+
else {
|
|
1033
|
+
const prefix = pkg.relativePath + '/';
|
|
1034
|
+
passes = (f) => f === pkg.relativePath || f.startsWith(prefix);
|
|
1035
|
+
}
|
|
1036
|
+
}
|
|
764
1037
|
if (spinner)
|
|
765
1038
|
spinner.text = 'Searching...';
|
|
766
1039
|
let results;
|
|
@@ -768,6 +1041,8 @@ program
|
|
|
768
1041
|
const q = query.toLowerCase();
|
|
769
1042
|
const matches = [];
|
|
770
1043
|
for (const [file, entry] of graph.files) {
|
|
1044
|
+
if (passes && !passes(file))
|
|
1045
|
+
continue;
|
|
771
1046
|
for (const exp of entry.exports) {
|
|
772
1047
|
if (exp.name.toLowerCase().includes(q)) {
|
|
773
1048
|
matches.push({ symbol: exp.name, kind: exp.kind, file, line: exp.line });
|
|
@@ -785,6 +1060,7 @@ program
|
|
|
785
1060
|
const q = query.toLowerCase();
|
|
786
1061
|
const matches = scan.files
|
|
787
1062
|
.filter((f) => f.relativePath.toLowerCase().includes(q))
|
|
1063
|
+
.filter((f) => !passes || passes(f.relativePath))
|
|
788
1064
|
.slice(0, limit)
|
|
789
1065
|
.map((f) => ({ file: f.relativePath, sizeBytes: f.sizeBytes }));
|
|
790
1066
|
results = { scope, query, matches, total: matches.length };
|
|
@@ -792,7 +1068,8 @@ program
|
|
|
792
1068
|
else {
|
|
793
1069
|
const mode = cmdOpts.semantic ? 'semantic' : String(cmdOpts.mode ?? 'lexical');
|
|
794
1070
|
const index = await buildSearchIndex(rootPath, scan.files, graph);
|
|
795
|
-
const
|
|
1071
|
+
const lexicalHitsAll = searchIndex(index, query, { limit });
|
|
1072
|
+
const lexicalHits = passes ? lexicalHitsAll.filter((h) => passes(h.file)) : lexicalHitsAll;
|
|
796
1073
|
const tokens = expandQuery(query);
|
|
797
1074
|
if (mode === 'lexical') {
|
|
798
1075
|
const withExcerpts = await attachExcerpts(rootPath, lexicalHits, tokens);
|
|
@@ -829,7 +1106,8 @@ program
|
|
|
829
1106
|
}
|
|
830
1107
|
if (spinner)
|
|
831
1108
|
spinner.text = 'Searching...';
|
|
832
|
-
const
|
|
1109
|
+
const semHitsAll = await semanticSearch(semIndex, query, { limit });
|
|
1110
|
+
const semHits = passes ? semHitsAll.filter((h) => passes(h.file)) : semHitsAll;
|
|
833
1111
|
if (mode === 'semantic') {
|
|
834
1112
|
const enriched = await attachExcerpts(rootPath, semHits.map((h) => ({
|
|
835
1113
|
file: h.file,
|
|
@@ -934,6 +1212,7 @@ program
|
|
|
934
1212
|
.command('coverage')
|
|
935
1213
|
.description('Join test coverage with hotspots - surface the scariest untested files')
|
|
936
1214
|
.option('--limit <n>', 'limit number of entries shown', '30')
|
|
1215
|
+
.option('--package <name>', 'monorepo: scope to a single workspace package')
|
|
937
1216
|
.action(async (cmdOpts) => {
|
|
938
1217
|
setupLogLevel();
|
|
939
1218
|
maybeCompactBanner();
|
|
@@ -952,6 +1231,11 @@ program
|
|
|
952
1231
|
coverage: coverage.available ? coverageMap(coverage) : undefined,
|
|
953
1232
|
});
|
|
954
1233
|
const joined = joinCoverageWithHotspots(hotspots, coverage);
|
|
1234
|
+
if (cmdOpts.package && joined.available) {
|
|
1235
|
+
const ws = await detectWorkspaces(rootPath);
|
|
1236
|
+
const allowed = new Set(filterFilesByPackage(ws, cmdOpts.package, joined.entries.map((e) => e.relativePath)));
|
|
1237
|
+
joined.entries = joined.entries.filter((e) => allowed.has(e.relativePath));
|
|
1238
|
+
}
|
|
955
1239
|
if (spinner)
|
|
956
1240
|
spinner.stop();
|
|
957
1241
|
switch (format) {
|