@optave/codegraph 3.1.5 → 3.2.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 +3 -2
- package/package.json +7 -7
- package/src/ast-analysis/engine.js +252 -258
- package/src/ast-analysis/shared.js +0 -12
- package/src/ast-analysis/visitors/cfg-visitor.js +635 -649
- package/src/ast-analysis/visitors/complexity-visitor.js +135 -139
- package/src/ast-analysis/visitors/dataflow-visitor.js +230 -224
- package/src/cli/commands/ast.js +2 -1
- package/src/cli/commands/audit.js +2 -1
- package/src/cli/commands/batch.js +2 -1
- package/src/cli/commands/brief.js +12 -0
- package/src/cli/commands/cfg.js +2 -1
- package/src/cli/commands/check.js +20 -23
- package/src/cli/commands/children.js +6 -1
- package/src/cli/commands/complexity.js +2 -1
- package/src/cli/commands/context.js +6 -1
- package/src/cli/commands/dataflow.js +2 -1
- package/src/cli/commands/deps.js +8 -3
- package/src/cli/commands/flow.js +2 -1
- package/src/cli/commands/fn-impact.js +6 -1
- package/src/cli/commands/owners.js +4 -2
- package/src/cli/commands/query.js +6 -1
- package/src/cli/commands/roles.js +2 -1
- package/src/cli/commands/search.js +8 -2
- package/src/cli/commands/sequence.js +2 -1
- package/src/cli/commands/triage.js +38 -27
- package/src/db/connection.js +18 -12
- package/src/db/migrations.js +41 -64
- package/src/db/query-builder.js +60 -4
- package/src/db/repository/in-memory-repository.js +27 -16
- package/src/db/repository/nodes.js +8 -10
- package/src/domain/analysis/brief.js +155 -0
- package/src/domain/analysis/context.js +174 -190
- package/src/domain/analysis/dependencies.js +200 -146
- package/src/domain/analysis/exports.js +3 -2
- package/src/domain/analysis/impact.js +267 -152
- package/src/domain/analysis/module-map.js +247 -221
- package/src/domain/analysis/roles.js +8 -5
- package/src/domain/analysis/symbol-lookup.js +7 -5
- package/src/domain/graph/builder/helpers.js +1 -1
- package/src/domain/graph/builder/incremental.js +116 -90
- package/src/domain/graph/builder/pipeline.js +106 -80
- package/src/domain/graph/builder/stages/build-edges.js +318 -239
- package/src/domain/graph/builder/stages/detect-changes.js +198 -177
- package/src/domain/graph/builder/stages/insert-nodes.js +147 -139
- package/src/domain/graph/watcher.js +2 -2
- package/src/domain/parser.js +20 -11
- package/src/domain/queries.js +1 -0
- package/src/domain/search/search/filters.js +9 -5
- package/src/domain/search/search/keyword.js +12 -5
- package/src/domain/search/search/prepare.js +13 -5
- package/src/extractors/csharp.js +224 -207
- package/src/extractors/go.js +176 -172
- package/src/extractors/hcl.js +94 -78
- package/src/extractors/java.js +213 -207
- package/src/extractors/javascript.js +274 -304
- package/src/extractors/php.js +234 -221
- package/src/extractors/python.js +252 -250
- package/src/extractors/ruby.js +192 -185
- package/src/extractors/rust.js +182 -167
- package/src/features/ast.js +5 -3
- package/src/features/audit.js +4 -2
- package/src/features/boundaries.js +98 -83
- package/src/features/cfg.js +134 -143
- package/src/features/communities.js +68 -53
- package/src/features/complexity.js +143 -132
- package/src/features/dataflow.js +146 -149
- package/src/features/export.js +3 -3
- package/src/features/graph-enrichment.js +2 -2
- package/src/features/manifesto.js +9 -6
- package/src/features/owners.js +4 -3
- package/src/features/sequence.js +152 -141
- package/src/features/shared/find-nodes.js +31 -0
- package/src/features/structure.js +130 -99
- package/src/features/triage.js +83 -68
- package/src/graph/classifiers/risk.js +3 -2
- package/src/graph/classifiers/roles.js +6 -3
- package/src/index.js +1 -0
- package/src/mcp/server.js +65 -56
- package/src/mcp/tool-registry.js +13 -0
- package/src/mcp/tools/brief.js +8 -0
- package/src/mcp/tools/index.js +2 -0
- package/src/presentation/brief.js +51 -0
- package/src/presentation/queries-cli/exports.js +21 -14
- package/src/presentation/queries-cli/impact.js +55 -39
- package/src/presentation/queries-cli/inspect.js +184 -189
- package/src/presentation/queries-cli/overview.js +57 -58
- package/src/presentation/queries-cli/path.js +36 -29
- package/src/presentation/table.js +0 -8
- package/src/shared/generators.js +7 -3
- package/src/shared/kinds.js +1 -1
|
@@ -1,7 +1,24 @@
|
|
|
1
|
+
import { collectFile } from '../../db/query-builder.js';
|
|
1
2
|
import { EVERY_SYMBOL_KIND } from '../../domain/queries.js';
|
|
2
3
|
import { ConfigError } from '../../shared/errors.js';
|
|
3
4
|
import { config } from '../shared/options.js';
|
|
4
5
|
|
|
6
|
+
function validateKind(kind) {
|
|
7
|
+
if (kind && !EVERY_SYMBOL_KIND.includes(kind)) {
|
|
8
|
+
throw new ConfigError(`Invalid kind "${kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
|
|
9
|
+
}
|
|
10
|
+
}
|
|
11
|
+
|
|
12
|
+
async function runManifesto(opts, qOpts) {
|
|
13
|
+
validateKind(opts.kind);
|
|
14
|
+
const { manifesto } = await import('../../presentation/manifesto.js');
|
|
15
|
+
manifesto(opts.db, {
|
|
16
|
+
file: opts.file,
|
|
17
|
+
kind: opts.kind,
|
|
18
|
+
...qOpts,
|
|
19
|
+
});
|
|
20
|
+
}
|
|
21
|
+
|
|
5
22
|
export const command = {
|
|
6
23
|
name: 'check [ref]',
|
|
7
24
|
description:
|
|
@@ -15,7 +32,7 @@ export const command = {
|
|
|
15
32
|
['--signatures', 'Assert no function declaration lines were modified'],
|
|
16
33
|
['--boundaries', 'Assert no cross-owner boundary violations'],
|
|
17
34
|
['--depth <n>', 'Max BFS depth for blast radius (default: 3)'],
|
|
18
|
-
['-f, --file <path>', 'Scope to file (partial match, manifesto mode)'],
|
|
35
|
+
['-f, --file <path>', 'Scope to file (partial match, repeatable, manifesto mode)', collectFile],
|
|
19
36
|
['-k, --kind <kind>', 'Filter by symbol kind (manifesto mode)'],
|
|
20
37
|
['-T, --no-tests', 'Exclude test/spec files from results'],
|
|
21
38
|
['--include-tests', 'Include test/spec files (overrides excludeTests config)'],
|
|
@@ -29,17 +46,7 @@ export const command = {
|
|
|
29
46
|
const qOpts = ctx.resolveQueryOpts(opts);
|
|
30
47
|
|
|
31
48
|
if (!isDiffMode && !opts.rules) {
|
|
32
|
-
|
|
33
|
-
throw new ConfigError(
|
|
34
|
-
`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`,
|
|
35
|
-
);
|
|
36
|
-
}
|
|
37
|
-
const { manifesto } = await import('../../presentation/manifesto.js');
|
|
38
|
-
manifesto(opts.db, {
|
|
39
|
-
file: opts.file,
|
|
40
|
-
kind: opts.kind,
|
|
41
|
-
...qOpts,
|
|
42
|
-
});
|
|
49
|
+
await runManifesto(opts, qOpts);
|
|
43
50
|
return;
|
|
44
51
|
}
|
|
45
52
|
|
|
@@ -58,17 +65,7 @@ export const command = {
|
|
|
58
65
|
});
|
|
59
66
|
|
|
60
67
|
if (opts.rules) {
|
|
61
|
-
|
|
62
|
-
throw new ConfigError(
|
|
63
|
-
`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`,
|
|
64
|
-
);
|
|
65
|
-
}
|
|
66
|
-
const { manifesto } = await import('../../presentation/manifesto.js');
|
|
67
|
-
manifesto(opts.db, {
|
|
68
|
-
file: opts.file,
|
|
69
|
-
kind: opts.kind,
|
|
70
|
-
...qOpts,
|
|
71
|
-
});
|
|
68
|
+
await runManifesto(opts, qOpts);
|
|
72
69
|
}
|
|
73
70
|
},
|
|
74
71
|
};
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { collectFile } from '../../db/query-builder.js';
|
|
1
2
|
import { EVERY_SYMBOL_KIND } from '../../domain/queries.js';
|
|
2
3
|
import { children } from '../../presentation/queries-cli.js';
|
|
3
4
|
|
|
@@ -6,7 +7,11 @@ export const command = {
|
|
|
6
7
|
description: 'List parameters, properties, and constants of a symbol',
|
|
7
8
|
options: [
|
|
8
9
|
['-d, --db <path>', 'Path to graph.db'],
|
|
9
|
-
[
|
|
10
|
+
[
|
|
11
|
+
'-f, --file <path>',
|
|
12
|
+
'Scope search to symbols in this file (partial match, repeatable)',
|
|
13
|
+
collectFile,
|
|
14
|
+
],
|
|
10
15
|
['-k, --kind <kind>', 'Filter to a specific symbol kind'],
|
|
11
16
|
['-T, --no-tests', 'Exclude test/spec files from results'],
|
|
12
17
|
['-j, --json', 'Output as JSON'],
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { collectFile } from '../../db/query-builder.js';
|
|
1
2
|
import { EVERY_SYMBOL_KIND } from '../../domain/queries.js';
|
|
2
3
|
|
|
3
4
|
export const command = {
|
|
@@ -13,7 +14,7 @@ export const command = {
|
|
|
13
14
|
],
|
|
14
15
|
['--above-threshold', 'Only functions exceeding warn thresholds'],
|
|
15
16
|
['--health', 'Show health metrics (Halstead, MI) columns'],
|
|
16
|
-
['-f, --file <path>', 'Scope to file (partial match)'],
|
|
17
|
+
['-f, --file <path>', 'Scope to file (partial match, repeatable)', collectFile],
|
|
17
18
|
['-k, --kind <kind>', 'Filter by symbol kind'],
|
|
18
19
|
['-T, --no-tests', 'Exclude test/spec files from results'],
|
|
19
20
|
['--include-tests', 'Include test/spec files (overrides excludeTests config)'],
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { collectFile } from '../../db/query-builder.js';
|
|
1
2
|
import { EVERY_SYMBOL_KIND } from '../../domain/queries.js';
|
|
2
3
|
import { context } from '../../presentation/queries-cli.js';
|
|
3
4
|
|
|
@@ -7,7 +8,11 @@ export const command = {
|
|
|
7
8
|
queryOpts: true,
|
|
8
9
|
options: [
|
|
9
10
|
['--depth <n>', 'Include callee source up to N levels deep', '0'],
|
|
10
|
-
[
|
|
11
|
+
[
|
|
12
|
+
'-f, --file <path>',
|
|
13
|
+
'Scope search to functions in this file (partial match, repeatable)',
|
|
14
|
+
collectFile,
|
|
15
|
+
],
|
|
11
16
|
['-k, --kind <kind>', 'Filter to a specific symbol kind'],
|
|
12
17
|
['--no-source', 'Metadata only (skip source extraction)'],
|
|
13
18
|
['--with-test-source', 'Include test source code'],
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { collectFile } from '../../db/query-builder.js';
|
|
1
2
|
import { EVERY_SYMBOL_KIND } from '../../domain/queries.js';
|
|
2
3
|
|
|
3
4
|
export const command = {
|
|
@@ -5,7 +6,7 @@ export const command = {
|
|
|
5
6
|
description: 'Show data flow for a function: parameters, return consumers, mutations',
|
|
6
7
|
queryOpts: true,
|
|
7
8
|
options: [
|
|
8
|
-
['-f, --file <path>', 'Scope to file (partial match)'],
|
|
9
|
+
['-f, --file <path>', 'Scope to file (partial match, repeatable)', collectFile],
|
|
9
10
|
['-k, --kind <kind>', 'Filter by symbol kind'],
|
|
10
11
|
['--impact', 'Show data-dependent blast radius'],
|
|
11
12
|
['--depth <n>', 'Max traversal depth', '5'],
|
package/src/cli/commands/deps.js
CHANGED
|
@@ -1,12 +1,17 @@
|
|
|
1
|
+
import { brief } from '../../presentation/brief.js';
|
|
1
2
|
import { fileDeps } from '../../presentation/queries-cli.js';
|
|
2
3
|
|
|
3
4
|
export const command = {
|
|
4
5
|
name: 'deps <file>',
|
|
5
6
|
description: 'Show what this file imports and what imports it',
|
|
6
7
|
queryOpts: true,
|
|
8
|
+
options: [['--brief', 'Compact output with symbol roles, caller counts, and risk tier']],
|
|
7
9
|
execute([file], opts, ctx) {
|
|
8
|
-
|
|
9
|
-
|
|
10
|
-
|
|
10
|
+
const qOpts = ctx.resolveQueryOpts(opts);
|
|
11
|
+
if (opts.brief) {
|
|
12
|
+
brief(file, opts.db, qOpts);
|
|
13
|
+
} else {
|
|
14
|
+
fileDeps(file, opts.db, qOpts);
|
|
15
|
+
}
|
|
11
16
|
},
|
|
12
17
|
};
|
package/src/cli/commands/flow.js
CHANGED
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { collectFile } from '../../db/query-builder.js';
|
|
1
2
|
import { EVERY_SYMBOL_KIND } from '../../domain/queries.js';
|
|
2
3
|
|
|
3
4
|
export const command = {
|
|
@@ -8,7 +9,7 @@ export const command = {
|
|
|
8
9
|
options: [
|
|
9
10
|
['--list', 'List all entry points grouped by type'],
|
|
10
11
|
['--depth <n>', 'Max forward traversal depth', '10'],
|
|
11
|
-
['-f, --file <path>', 'Scope to a specific file (partial match)'],
|
|
12
|
+
['-f, --file <path>', 'Scope to a specific file (partial match, repeatable)', collectFile],
|
|
12
13
|
['-k, --kind <kind>', 'Filter by symbol kind'],
|
|
13
14
|
],
|
|
14
15
|
validate([name], opts) {
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { collectFile } from '../../db/query-builder.js';
|
|
1
2
|
import { EVERY_SYMBOL_KIND } from '../../domain/queries.js';
|
|
2
3
|
import { fnImpact } from '../../presentation/queries-cli.js';
|
|
3
4
|
|
|
@@ -7,7 +8,11 @@ export const command = {
|
|
|
7
8
|
queryOpts: true,
|
|
8
9
|
options: [
|
|
9
10
|
['--depth <n>', 'Max transitive depth', '5'],
|
|
10
|
-
[
|
|
11
|
+
[
|
|
12
|
+
'-f, --file <path>',
|
|
13
|
+
'Scope search to functions in this file (partial match, repeatable)',
|
|
14
|
+
collectFile,
|
|
15
|
+
],
|
|
11
16
|
['-k, --kind <kind>', 'Filter to a specific symbol kind'],
|
|
12
17
|
],
|
|
13
18
|
validate([_name], opts) {
|
|
@@ -1,3 +1,5 @@
|
|
|
1
|
+
import { collectFile } from '../../db/query-builder.js';
|
|
2
|
+
|
|
1
3
|
export const command = {
|
|
2
4
|
name: 'owners [target]',
|
|
3
5
|
description: 'Show CODEOWNERS mapping for files and functions',
|
|
@@ -5,7 +7,7 @@ export const command = {
|
|
|
5
7
|
['-d, --db <path>', 'Path to graph.db'],
|
|
6
8
|
['--owner <owner>', 'Filter to a specific owner'],
|
|
7
9
|
['--boundary', 'Show cross-owner boundary edges'],
|
|
8
|
-
['-f, --file <path>', 'Scope to a specific file'],
|
|
10
|
+
['-f, --file <path>', 'Scope to a specific file (repeatable)', collectFile],
|
|
9
11
|
['-k, --kind <kind>', 'Filter by symbol kind'],
|
|
10
12
|
['-T, --no-tests', 'Exclude test/spec files'],
|
|
11
13
|
['--include-tests', 'Include test/spec files (overrides excludeTests config)'],
|
|
@@ -16,7 +18,7 @@ export const command = {
|
|
|
16
18
|
owners(opts.db, {
|
|
17
19
|
owner: opts.owner,
|
|
18
20
|
boundary: opts.boundary,
|
|
19
|
-
file: opts.file
|
|
21
|
+
file: opts.file && opts.file.length > 0 ? opts.file : target,
|
|
20
22
|
kind: opts.kind,
|
|
21
23
|
noTests: ctx.resolveNoTests(opts),
|
|
22
24
|
json: opts.json,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { collectFile } from '../../db/query-builder.js';
|
|
1
2
|
import { EVERY_SYMBOL_KIND } from '../../domain/queries.js';
|
|
2
3
|
import { fnDeps, symbolPath } from '../../presentation/queries-cli.js';
|
|
3
4
|
|
|
@@ -7,7 +8,11 @@ export const command = {
|
|
|
7
8
|
queryOpts: true,
|
|
8
9
|
options: [
|
|
9
10
|
['--depth <n>', 'Transitive caller depth', '3'],
|
|
10
|
-
[
|
|
11
|
+
[
|
|
12
|
+
'-f, --file <path>',
|
|
13
|
+
'Scope search to functions in this file (partial match, repeatable)',
|
|
14
|
+
collectFile,
|
|
15
|
+
],
|
|
11
16
|
['-k, --kind <kind>', 'Filter to a specific symbol kind'],
|
|
12
17
|
['--path <to>', 'Path mode: find shortest path to <to>'],
|
|
13
18
|
['--kinds <kinds>', 'Path mode: comma-separated edge kinds to follow (default: calls)'],
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { collectFile } from '../../db/query-builder.js';
|
|
1
2
|
import { VALID_ROLES } from '../../domain/queries.js';
|
|
2
3
|
import { roles } from '../../presentation/queries-cli.js';
|
|
3
4
|
|
|
@@ -7,7 +8,7 @@ export const command = {
|
|
|
7
8
|
options: [
|
|
8
9
|
['-d, --db <path>', 'Path to graph.db'],
|
|
9
10
|
['--role <role>', `Filter by role (${VALID_ROLES.join(', ')})`],
|
|
10
|
-
['-f, --file <path>', 'Scope to a specific file (partial match)'],
|
|
11
|
+
['-f, --file <path>', 'Scope to a specific file (partial match, repeatable)', collectFile],
|
|
11
12
|
['-T, --no-tests', 'Exclude test/spec files'],
|
|
12
13
|
['--include-tests', 'Include test/spec files (overrides excludeTests config)'],
|
|
13
14
|
['-j, --json', 'Output as JSON'],
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { collectFile } from '../../db/query-builder.js';
|
|
1
2
|
import { search } from '../../domain/search/index.js';
|
|
2
3
|
|
|
3
4
|
export const command = {
|
|
@@ -11,7 +12,7 @@ export const command = {
|
|
|
11
12
|
['--include-tests', 'Include test/spec files (overrides excludeTests config)'],
|
|
12
13
|
['--min-score <score>', 'Minimum similarity threshold', '0.2'],
|
|
13
14
|
['-k, --kind <kind>', 'Filter by kind: function, method, class'],
|
|
14
|
-
['--file <pattern>', 'Filter by file path pattern'],
|
|
15
|
+
['--file <pattern>', 'Filter by file path pattern (repeatable)', collectFile],
|
|
15
16
|
['--rrf-k <number>', 'RRF k parameter for multi-query ranking', '60'],
|
|
16
17
|
['--mode <mode>', 'Search mode: hybrid, semantic, keyword (default: hybrid)'],
|
|
17
18
|
['-j, --json', 'Output as JSON'],
|
|
@@ -25,6 +26,11 @@ export const command = {
|
|
|
25
26
|
}
|
|
26
27
|
},
|
|
27
28
|
async execute([query], opts, ctx) {
|
|
29
|
+
// --file collects into an array; pass single element unwrapped for single
|
|
30
|
+
// value, or pass the raw array for multi-file scoping.
|
|
31
|
+
const fileArr = opts.file || [];
|
|
32
|
+
const filePattern =
|
|
33
|
+
fileArr.length === 1 ? fileArr[0] : fileArr.length > 1 ? fileArr : undefined;
|
|
28
34
|
await search(query, opts.db, {
|
|
29
35
|
limit: parseInt(opts.limit, 10),
|
|
30
36
|
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
@@ -32,7 +38,7 @@ export const command = {
|
|
|
32
38
|
minScore: parseFloat(opts.minScore),
|
|
33
39
|
model: opts.model,
|
|
34
40
|
kind: opts.kind,
|
|
35
|
-
filePattern
|
|
41
|
+
filePattern,
|
|
36
42
|
rrfK: parseInt(opts.rrfK, 10),
|
|
37
43
|
mode: opts.mode,
|
|
38
44
|
json: opts.json,
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { collectFile } from '../../db/query-builder.js';
|
|
1
2
|
import { EVERY_SYMBOL_KIND } from '../../domain/queries.js';
|
|
2
3
|
|
|
3
4
|
export const command = {
|
|
@@ -7,7 +8,7 @@ export const command = {
|
|
|
7
8
|
options: [
|
|
8
9
|
['--depth <n>', 'Max forward traversal depth', '10'],
|
|
9
10
|
['--dataflow', 'Annotate with parameter names and return arrows from dataflow table'],
|
|
10
|
-
['-f, --file <path>', 'Scope to a specific file (partial match)'],
|
|
11
|
+
['-f, --file <path>', 'Scope to a specific file (partial match, repeatable)', collectFile],
|
|
11
12
|
['-k, --kind <kind>', 'Filter by symbol kind'],
|
|
12
13
|
],
|
|
13
14
|
validate([_name], opts) {
|
|
@@ -1,6 +1,40 @@
|
|
|
1
|
+
import { collectFile } from '../../db/query-builder.js';
|
|
1
2
|
import { EVERY_SYMBOL_KIND, VALID_ROLES } from '../../domain/queries.js';
|
|
2
3
|
import { ConfigError } from '../../shared/errors.js';
|
|
3
4
|
|
|
5
|
+
function validateFilters(opts) {
|
|
6
|
+
if (opts.kind && !EVERY_SYMBOL_KIND.includes(opts.kind)) {
|
|
7
|
+
throw new ConfigError(`Invalid kind "${opts.kind}". Valid: ${EVERY_SYMBOL_KIND.join(', ')}`);
|
|
8
|
+
}
|
|
9
|
+
if (opts.role && !VALID_ROLES.includes(opts.role)) {
|
|
10
|
+
throw new ConfigError(`Invalid role "${opts.role}". Valid: ${VALID_ROLES.join(', ')}`);
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
function parseWeights(raw) {
|
|
15
|
+
if (!raw) return undefined;
|
|
16
|
+
try {
|
|
17
|
+
return JSON.parse(raw);
|
|
18
|
+
} catch (err) {
|
|
19
|
+
throw new ConfigError('Invalid --weights JSON', { cause: err });
|
|
20
|
+
}
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
async function runHotspots(opts, ctx) {
|
|
24
|
+
const { hotspotsData, formatHotspots } = await import('../../presentation/structure.js');
|
|
25
|
+
const metric = opts.sort === 'risk' ? 'fan-in' : opts.sort;
|
|
26
|
+
const data = hotspotsData(opts.db, {
|
|
27
|
+
metric,
|
|
28
|
+
level: opts.level,
|
|
29
|
+
limit: parseInt(opts.limit, 10),
|
|
30
|
+
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
31
|
+
noTests: ctx.resolveNoTests(opts),
|
|
32
|
+
});
|
|
33
|
+
if (!ctx.outputResult(data, 'hotspots', opts)) {
|
|
34
|
+
console.log(formatHotspots(data));
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
4
38
|
export const command = {
|
|
5
39
|
name: 'triage',
|
|
6
40
|
description:
|
|
@@ -20,7 +54,7 @@ export const command = {
|
|
|
20
54
|
],
|
|
21
55
|
['--min-score <score>', 'Only show symbols with risk score >= threshold'],
|
|
22
56
|
['--role <role>', 'Filter by role (entry, core, utility, adapter, leaf, dead)'],
|
|
23
|
-
['-f, --file <path>', 'Scope to a specific file (partial match)'],
|
|
57
|
+
['-f, --file <path>', 'Scope to a specific file (partial match, repeatable)', collectFile],
|
|
24
58
|
['-k, --kind <kind>', 'Filter by symbol kind (function, method, class)'],
|
|
25
59
|
['-T, --no-tests', 'Exclude test/spec files from results'],
|
|
26
60
|
['--include-tests', 'Include test/spec files (overrides excludeTests config)'],
|
|
@@ -31,35 +65,12 @@ export const command = {
|
|
|
31
65
|
],
|
|
32
66
|
async execute(_args, opts, ctx) {
|
|
33
67
|
if (opts.level === 'file' || opts.level === 'directory') {
|
|
34
|
-
|
|
35
|
-
const metric = opts.sort === 'risk' ? 'fan-in' : opts.sort;
|
|
36
|
-
const data = hotspotsData(opts.db, {
|
|
37
|
-
metric,
|
|
38
|
-
level: opts.level,
|
|
39
|
-
limit: parseInt(opts.limit, 10),
|
|
40
|
-
offset: opts.offset ? parseInt(opts.offset, 10) : undefined,
|
|
41
|
-
noTests: ctx.resolveNoTests(opts),
|
|
42
|
-
});
|
|
43
|
-
if (!ctx.outputResult(data, 'hotspots', opts)) {
|
|
44
|
-
console.log(formatHotspots(data));
|
|
45
|
-
}
|
|
68
|
+
await runHotspots(opts, ctx);
|
|
46
69
|
return;
|
|
47
70
|
}
|
|
48
71
|
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
}
|
|
52
|
-
if (opts.role && !VALID_ROLES.includes(opts.role)) {
|
|
53
|
-
throw new ConfigError(`Invalid role "${opts.role}". Valid: ${VALID_ROLES.join(', ')}`);
|
|
54
|
-
}
|
|
55
|
-
let weights;
|
|
56
|
-
if (opts.weights) {
|
|
57
|
-
try {
|
|
58
|
-
weights = JSON.parse(opts.weights);
|
|
59
|
-
} catch (err) {
|
|
60
|
-
throw new ConfigError('Invalid --weights JSON', { cause: err });
|
|
61
|
-
}
|
|
62
|
-
}
|
|
72
|
+
validateFilters(opts);
|
|
73
|
+
const weights = parseWeights(opts.weights);
|
|
63
74
|
const { triage } = await import('../../presentation/triage.js');
|
|
64
75
|
triage(opts.db, {
|
|
65
76
|
limit: parseInt(opts.limit, 10),
|
package/src/db/connection.js
CHANGED
|
@@ -37,10 +37,12 @@ export function findRepoRoot(fromDir) {
|
|
|
37
37
|
// matches the realpathSync'd dir in findDbPath.
|
|
38
38
|
try {
|
|
39
39
|
root = fs.realpathSync(raw);
|
|
40
|
-
} catch {
|
|
40
|
+
} catch (e) {
|
|
41
|
+
debug(`realpathSync failed for git root "${raw}", using resolve: ${e.message}`);
|
|
41
42
|
root = path.resolve(raw);
|
|
42
43
|
}
|
|
43
|
-
} catch {
|
|
44
|
+
} catch (e) {
|
|
45
|
+
debug(`git rev-parse failed for "${dir}": ${e.message}`);
|
|
44
46
|
root = null;
|
|
45
47
|
}
|
|
46
48
|
if (!fromDir) {
|
|
@@ -60,7 +62,8 @@ function isProcessAlive(pid) {
|
|
|
60
62
|
try {
|
|
61
63
|
process.kill(pid, 0);
|
|
62
64
|
return true;
|
|
63
|
-
} catch {
|
|
65
|
+
} catch (e) {
|
|
66
|
+
debug(`PID ${pid} not alive: ${e.code || e.message}`);
|
|
64
67
|
return false;
|
|
65
68
|
}
|
|
66
69
|
}
|
|
@@ -75,13 +78,13 @@ function acquireAdvisoryLock(dbPath) {
|
|
|
75
78
|
warn(`Another process (PID ${pid}) may be using this database. Proceeding with caution.`);
|
|
76
79
|
}
|
|
77
80
|
}
|
|
78
|
-
} catch {
|
|
79
|
-
|
|
81
|
+
} catch (e) {
|
|
82
|
+
debug(`Advisory lock read failed: ${e.message}`);
|
|
80
83
|
}
|
|
81
84
|
try {
|
|
82
85
|
fs.writeFileSync(lockPath, String(process.pid), 'utf-8');
|
|
83
|
-
} catch {
|
|
84
|
-
|
|
86
|
+
} catch (e) {
|
|
87
|
+
debug(`Advisory lock write failed: ${e.message}`);
|
|
85
88
|
}
|
|
86
89
|
}
|
|
87
90
|
|
|
@@ -91,8 +94,8 @@ function releaseAdvisoryLock(lockPath) {
|
|
|
91
94
|
if (Number(content) === process.pid) {
|
|
92
95
|
fs.unlinkSync(lockPath);
|
|
93
96
|
}
|
|
94
|
-
} catch {
|
|
95
|
-
|
|
97
|
+
} catch (e) {
|
|
98
|
+
debug(`Advisory lock release failed for ${lockPath}: ${e.message}`);
|
|
96
99
|
}
|
|
97
100
|
}
|
|
98
101
|
|
|
@@ -107,7 +110,8 @@ function isSameDirectory(a, b) {
|
|
|
107
110
|
const sa = fs.statSync(a);
|
|
108
111
|
const sb = fs.statSync(b);
|
|
109
112
|
return sa.dev === sb.dev && sa.ino === sb.ino;
|
|
110
|
-
} catch {
|
|
113
|
+
} catch (e) {
|
|
114
|
+
debug(`isSameDirectory stat failed: ${e.message}`);
|
|
111
115
|
return false;
|
|
112
116
|
}
|
|
113
117
|
}
|
|
@@ -139,7 +143,8 @@ export function findDbPath(customPath) {
|
|
|
139
143
|
if (rawCeiling) {
|
|
140
144
|
try {
|
|
141
145
|
ceiling = fs.realpathSync(rawCeiling);
|
|
142
|
-
} catch {
|
|
146
|
+
} catch (e) {
|
|
147
|
+
debug(`realpathSync failed for ceiling "${rawCeiling}": ${e.message}`);
|
|
143
148
|
ceiling = rawCeiling;
|
|
144
149
|
}
|
|
145
150
|
} else {
|
|
@@ -149,7 +154,8 @@ export function findDbPath(customPath) {
|
|
|
149
154
|
let dir;
|
|
150
155
|
try {
|
|
151
156
|
dir = fs.realpathSync(process.cwd());
|
|
152
|
-
} catch {
|
|
157
|
+
} catch (e) {
|
|
158
|
+
debug(`realpathSync failed for cwd: ${e.message}`);
|
|
153
159
|
dir = process.cwd();
|
|
154
160
|
}
|
|
155
161
|
while (true) {
|
package/src/db/migrations.js
CHANGED
|
@@ -242,11 +242,23 @@ export const MIGRATIONS = [
|
|
|
242
242
|
},
|
|
243
243
|
];
|
|
244
244
|
|
|
245
|
+
function hasColumn(db, table, column) {
|
|
246
|
+
const cols = db.pragma(`table_info(${table})`);
|
|
247
|
+
return cols.some((c) => c.name === column);
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
function hasTable(db, table) {
|
|
251
|
+
const row = db.prepare("SELECT 1 FROM sqlite_master WHERE type='table' AND name=?").get(table);
|
|
252
|
+
return !!row;
|
|
253
|
+
}
|
|
254
|
+
|
|
245
255
|
export function getBuildMeta(db, key) {
|
|
256
|
+
if (!hasTable(db, 'build_meta')) return null;
|
|
246
257
|
try {
|
|
247
258
|
const row = db.prepare('SELECT value FROM build_meta WHERE key = ?').get(key);
|
|
248
259
|
return row ? row.value : null;
|
|
249
|
-
} catch {
|
|
260
|
+
} catch (e) {
|
|
261
|
+
debug(`getBuildMeta failed for key "${key}": ${e.message}`);
|
|
250
262
|
return null;
|
|
251
263
|
}
|
|
252
264
|
}
|
|
@@ -280,74 +292,39 @@ export function initSchema(db) {
|
|
|
280
292
|
}
|
|
281
293
|
}
|
|
282
294
|
|
|
283
|
-
|
|
284
|
-
|
|
285
|
-
|
|
286
|
-
|
|
287
|
-
|
|
288
|
-
|
|
289
|
-
|
|
290
|
-
|
|
291
|
-
/* already exists */
|
|
292
|
-
}
|
|
293
|
-
try {
|
|
294
|
-
db.exec('ALTER TABLE edges ADD COLUMN dynamic INTEGER DEFAULT 0');
|
|
295
|
-
} catch {
|
|
296
|
-
/* already exists */
|
|
297
|
-
}
|
|
298
|
-
try {
|
|
299
|
-
db.exec('ALTER TABLE nodes ADD COLUMN role TEXT');
|
|
300
|
-
} catch {
|
|
301
|
-
/* already exists */
|
|
302
|
-
}
|
|
303
|
-
try {
|
|
295
|
+
// Legacy column compat — add columns that may be missing from pre-migration DBs
|
|
296
|
+
if (hasTable(db, 'nodes')) {
|
|
297
|
+
if (!hasColumn(db, 'nodes', 'end_line')) {
|
|
298
|
+
db.exec('ALTER TABLE nodes ADD COLUMN end_line INTEGER');
|
|
299
|
+
}
|
|
300
|
+
if (!hasColumn(db, 'nodes', 'role')) {
|
|
301
|
+
db.exec('ALTER TABLE nodes ADD COLUMN role TEXT');
|
|
302
|
+
}
|
|
304
303
|
db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_role ON nodes(role)');
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
try {
|
|
309
|
-
db.exec('ALTER TABLE nodes ADD COLUMN parent_id INTEGER REFERENCES nodes(id)');
|
|
310
|
-
} catch {
|
|
311
|
-
/* already exists */
|
|
312
|
-
}
|
|
313
|
-
try {
|
|
304
|
+
if (!hasColumn(db, 'nodes', 'parent_id')) {
|
|
305
|
+
db.exec('ALTER TABLE nodes ADD COLUMN parent_id INTEGER REFERENCES nodes(id)');
|
|
306
|
+
}
|
|
314
307
|
db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_parent ON nodes(parent_id)');
|
|
315
|
-
} catch {
|
|
316
|
-
/* already exists */
|
|
317
|
-
}
|
|
318
|
-
try {
|
|
319
308
|
db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_kind_parent ON nodes(kind, parent_id)');
|
|
320
|
-
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
db.exec('ALTER TABLE nodes ADD COLUMN scope TEXT');
|
|
330
|
-
} catch {
|
|
331
|
-
/* already exists */
|
|
332
|
-
}
|
|
333
|
-
try {
|
|
334
|
-
db.exec('ALTER TABLE nodes ADD COLUMN visibility TEXT');
|
|
335
|
-
} catch {
|
|
336
|
-
/* already exists */
|
|
337
|
-
}
|
|
338
|
-
try {
|
|
309
|
+
if (!hasColumn(db, 'nodes', 'qualified_name')) {
|
|
310
|
+
db.exec('ALTER TABLE nodes ADD COLUMN qualified_name TEXT');
|
|
311
|
+
}
|
|
312
|
+
if (!hasColumn(db, 'nodes', 'scope')) {
|
|
313
|
+
db.exec('ALTER TABLE nodes ADD COLUMN scope TEXT');
|
|
314
|
+
}
|
|
315
|
+
if (!hasColumn(db, 'nodes', 'visibility')) {
|
|
316
|
+
db.exec('ALTER TABLE nodes ADD COLUMN visibility TEXT');
|
|
317
|
+
}
|
|
339
318
|
db.exec('UPDATE nodes SET qualified_name = name WHERE qualified_name IS NULL');
|
|
340
|
-
} catch {
|
|
341
|
-
/* nodes table may not exist yet */
|
|
342
|
-
}
|
|
343
|
-
try {
|
|
344
319
|
db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_qualified_name ON nodes(qualified_name)');
|
|
345
|
-
} catch {
|
|
346
|
-
/* already exists */
|
|
347
|
-
}
|
|
348
|
-
try {
|
|
349
320
|
db.exec('CREATE INDEX IF NOT EXISTS idx_nodes_scope ON nodes(scope)');
|
|
350
|
-
}
|
|
351
|
-
|
|
321
|
+
}
|
|
322
|
+
if (hasTable(db, 'edges')) {
|
|
323
|
+
if (!hasColumn(db, 'edges', 'confidence')) {
|
|
324
|
+
db.exec('ALTER TABLE edges ADD COLUMN confidence REAL DEFAULT 1.0');
|
|
325
|
+
}
|
|
326
|
+
if (!hasColumn(db, 'edges', 'dynamic')) {
|
|
327
|
+
db.exec('ALTER TABLE edges ADD COLUMN dynamic INTEGER DEFAULT 0');
|
|
328
|
+
}
|
|
352
329
|
}
|
|
353
330
|
}
|