@optave/codegraph 3.1.4 → 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 +29 -72
- package/package.json +10 -8
- package/src/ast-analysis/engine.js +260 -246
- package/src/ast-analysis/shared.js +2 -14
- 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 +4 -7
- package/src/cli/commands/audit.js +11 -11
- package/src/cli/commands/batch.js +6 -5
- package/src/cli/commands/branch-compare.js +1 -1
- package/src/cli/commands/brief.js +12 -0
- package/src/cli/commands/build.js +1 -1
- package/src/cli/commands/cfg.js +5 -8
- package/src/cli/commands/check.js +28 -36
- package/src/cli/commands/children.js +9 -7
- package/src/cli/commands/co-change.js +5 -3
- package/src/cli/commands/communities.js +2 -6
- package/src/cli/commands/complexity.js +5 -3
- package/src/cli/commands/context.js +9 -8
- package/src/cli/commands/cycles.js +12 -8
- package/src/cli/commands/dataflow.js +5 -8
- package/src/cli/commands/deps.js +9 -8
- package/src/cli/commands/diff-impact.js +2 -6
- package/src/cli/commands/embed.js +1 -1
- package/src/cli/commands/export.js +34 -31
- package/src/cli/commands/exports.js +2 -6
- package/src/cli/commands/flow.js +5 -8
- package/src/cli/commands/fn-impact.js +9 -8
- package/src/cli/commands/impact.js +2 -6
- package/src/cli/commands/info.js +2 -2
- package/src/cli/commands/map.js +1 -1
- package/src/cli/commands/mcp.js +1 -1
- package/src/cli/commands/models.js +1 -1
- package/src/cli/commands/owners.js +5 -3
- package/src/cli/commands/path.js +2 -2
- package/src/cli/commands/plot.js +40 -31
- package/src/cli/commands/query.js +9 -8
- package/src/cli/commands/registry.js +2 -2
- package/src/cli/commands/roles.js +5 -8
- package/src/cli/commands/search.js +9 -3
- package/src/cli/commands/sequence.js +5 -8
- package/src/cli/commands/snapshot.js +6 -1
- package/src/cli/commands/stats.js +1 -1
- package/src/cli/commands/structure.js +5 -4
- package/src/cli/commands/triage.js +41 -30
- package/src/cli/commands/watch.js +1 -1
- package/src/cli/commands/where.js +2 -6
- package/src/cli/index.js +11 -5
- package/src/cli/shared/open-graph.js +13 -0
- package/src/cli/shared/options.js +22 -2
- package/src/cli.js +1 -1
- package/src/db/connection.js +140 -11
- package/src/{db.js → db/index.js} +12 -5
- package/src/db/migrations.js +42 -65
- package/src/db/query-builder.js +72 -9
- package/src/db/repository/base.js +1 -1
- package/src/db/repository/graph-read.js +3 -3
- package/src/db/repository/in-memory-repository.js +30 -28
- package/src/db/repository/nodes.js +10 -17
- package/src/domain/analysis/brief.js +155 -0
- package/src/domain/analysis/context.js +392 -0
- package/src/domain/analysis/dependencies.js +395 -0
- package/src/{analysis → domain/analysis}/exports.js +11 -6
- package/src/domain/analysis/impact.js +581 -0
- package/src/domain/analysis/module-map.js +348 -0
- package/src/{analysis → domain/analysis}/roles.js +12 -9
- package/src/{analysis → domain/analysis}/symbol-lookup.js +19 -11
- package/src/{builder → domain/graph/builder}/helpers.js +4 -4
- package/src/{builder → domain/graph/builder}/incremental.js +119 -93
- package/src/domain/graph/builder/pipeline.js +156 -0
- package/src/domain/graph/builder/stages/build-edges.js +376 -0
- package/src/{builder → domain/graph/builder}/stages/build-structure.js +4 -4
- package/src/{builder → domain/graph/builder}/stages/collect-files.js +2 -2
- package/src/{builder → domain/graph/builder}/stages/detect-changes.js +204 -183
- package/src/{builder → domain/graph/builder}/stages/finalize.js +4 -4
- package/src/domain/graph/builder/stages/insert-nodes.js +203 -0
- package/src/{builder → domain/graph/builder}/stages/parse-files.js +2 -2
- package/src/{builder → domain/graph/builder}/stages/resolve-imports.js +1 -1
- package/src/{builder → domain/graph/builder}/stages/run-analyses.js +2 -2
- package/src/{change-journal.js → domain/graph/change-journal.js} +1 -1
- package/src/{cycles.js → domain/graph/cycles.js} +4 -4
- package/src/{journal.js → domain/graph/journal.js} +1 -1
- package/src/{resolve.js → domain/graph/resolve.js} +2 -2
- package/src/{watcher.js → domain/graph/watcher.js} +7 -7
- package/src/{parser.js → domain/parser.js} +24 -15
- package/src/{queries.js → domain/queries.js} +17 -16
- package/src/{embeddings → domain/search}/generator.js +3 -3
- package/src/{embeddings → domain/search}/models.js +2 -2
- package/src/{embeddings → domain/search}/search/cli-formatter.js +1 -1
- package/src/{embeddings → domain/search}/search/filters.js +9 -5
- package/src/{embeddings → domain/search}/search/hybrid.js +1 -1
- package/src/{embeddings → domain/search}/search/keyword.js +13 -6
- package/src/{embeddings → domain/search}/search/prepare.js +15 -7
- package/src/{embeddings → domain/search}/search/semantic.js +1 -1
- package/src/{embeddings → domain/search}/strategies/structured.js +1 -1
- 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 +275 -305
- 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/{ast.js → features/ast.js} +13 -11
- package/src/{audit.js → features/audit.js} +20 -46
- package/src/{batch.js → features/batch.js} +5 -5
- package/src/{boundaries.js → features/boundaries.js} +100 -85
- package/src/{branch-compare.js → features/branch-compare.js} +3 -3
- package/src/{cfg.js → features/cfg.js} +141 -150
- package/src/{check.js → features/check.js} +13 -30
- package/src/{cochange.js → features/cochange.js} +5 -5
- package/src/{communities.js → features/communities.js} +72 -57
- package/src/{complexity.js → features/complexity.js} +154 -143
- package/src/{dataflow.js → features/dataflow.js} +155 -158
- package/src/{export.js → features/export.js} +6 -6
- package/src/{flow.js → features/flow.js} +4 -4
- package/src/{viewer.js → features/graph-enrichment.js} +8 -8
- package/src/{manifesto.js → features/manifesto.js} +15 -12
- package/src/{owners.js → features/owners.js} +6 -5
- package/src/features/sequence.js +300 -0
- package/src/features/shared/find-nodes.js +31 -0
- package/src/{snapshot.js → features/snapshot.js} +3 -3
- package/src/{structure.js → features/structure.js} +139 -108
- package/src/features/triage.js +141 -0
- package/src/graph/builders/dependency.js +33 -14
- package/src/graph/classifiers/risk.js +3 -2
- package/src/graph/classifiers/roles.js +6 -3
- package/src/index.cjs +16 -0
- package/src/index.js +40 -39
- package/src/{native.js → infrastructure/native.js} +1 -1
- package/src/mcp/middleware.js +1 -1
- package/src/mcp/server.js +68 -59
- package/src/mcp/tool-registry.js +15 -2
- package/src/mcp/tools/ast-query.js +1 -1
- package/src/mcp/tools/audit.js +1 -1
- package/src/mcp/tools/batch-query.js +1 -1
- package/src/mcp/tools/branch-compare.js +3 -1
- package/src/mcp/tools/brief.js +8 -0
- package/src/mcp/tools/cfg.js +1 -1
- package/src/mcp/tools/check.js +3 -3
- package/src/mcp/tools/co-changes.js +1 -1
- package/src/mcp/tools/code-owners.js +1 -1
- package/src/mcp/tools/communities.js +1 -1
- package/src/mcp/tools/complexity.js +1 -1
- package/src/mcp/tools/dataflow.js +2 -2
- package/src/mcp/tools/execution-flow.js +2 -2
- package/src/mcp/tools/export-graph.js +2 -2
- package/src/mcp/tools/find-cycles.js +2 -2
- package/src/mcp/tools/index.js +2 -0
- package/src/mcp/tools/list-repos.js +1 -1
- package/src/mcp/tools/sequence.js +1 -1
- package/src/mcp/tools/structure.js +1 -1
- package/src/mcp/tools/triage.js +2 -2
- package/src/{commands → presentation}/audit.js +2 -2
- package/src/{commands → presentation}/batch.js +1 -1
- package/src/{commands → presentation}/branch-compare.js +2 -2
- package/src/presentation/brief.js +51 -0
- package/src/{commands → presentation}/cfg.js +1 -1
- package/src/{commands → presentation}/check.js +2 -2
- package/src/{commands → presentation}/communities.js +1 -1
- package/src/{commands → presentation}/complexity.js +1 -1
- package/src/{commands → presentation}/dataflow.js +1 -1
- package/src/{commands → presentation}/flow.js +2 -2
- package/src/{commands → presentation}/manifesto.js +1 -1
- package/src/{commands → presentation}/owners.js +1 -1
- package/src/presentation/queries-cli/exports.js +53 -0
- package/src/presentation/queries-cli/impact.js +214 -0
- package/src/presentation/queries-cli/index.js +5 -0
- package/src/presentation/queries-cli/inspect.js +329 -0
- package/src/presentation/queries-cli/overview.js +196 -0
- package/src/presentation/queries-cli/path.js +65 -0
- package/src/presentation/queries-cli.js +27 -0
- package/src/{commands → presentation}/query.js +1 -1
- package/src/presentation/result-formatter.js +126 -3
- package/src/{commands → presentation}/sequence.js +2 -2
- package/src/{commands → presentation}/structure.js +1 -1
- package/src/presentation/table.js +0 -8
- package/src/{commands → presentation}/triage.js +1 -1
- package/src/{constants.js → shared/constants.js} +1 -1
- package/src/shared/file-utils.js +2 -2
- package/src/shared/generators.js +9 -5
- package/src/shared/hierarchy.js +1 -1
- package/src/{kinds.js → shared/kinds.js} +1 -1
- package/src/analysis/context.js +0 -408
- package/src/analysis/dependencies.js +0 -341
- package/src/analysis/impact.js +0 -463
- package/src/analysis/module-map.js +0 -322
- package/src/builder/pipeline.js +0 -130
- package/src/builder/stages/build-edges.js +0 -297
- package/src/builder/stages/insert-nodes.js +0 -195
- package/src/mcp.js +0 -2
- package/src/queries-cli.js +0 -866
- package/src/sequence.js +0 -289
- package/src/triage.js +0 -126
- /package/src/{builder → domain/graph/builder}/context.js +0 -0
- /package/src/{builder.js → domain/graph/builder.js} +0 -0
- /package/src/{embeddings → domain/search}/index.js +0 -0
- /package/src/{embeddings → domain/search}/stores/fts5.js +0 -0
- /package/src/{embeddings → domain/search}/stores/sqlite-blob.js +0 -0
- /package/src/{embeddings → domain/search}/strategies/source.js +0 -0
- /package/src/{embeddings → domain/search}/strategies/text-utils.js +0 -0
- /package/src/{config.js → infrastructure/config.js} +0 -0
- /package/src/{logger.js → infrastructure/logger.js} +0 -0
- /package/src/{registry.js → infrastructure/registry.js} +0 -0
- /package/src/{update-check.js → infrastructure/update-check.js} +0 -0
- /package/src/{commands → presentation}/cochange.js +0 -0
- /package/src/{errors.js → shared/errors.js} +0 -0
- /package/src/{paginate.js → shared/paginate.js} +0 -0
|
@@ -1,11 +1,128 @@
|
|
|
1
|
-
import { printNdjson } from '../paginate.js';
|
|
1
|
+
import { printNdjson } from '../shared/paginate.js';
|
|
2
|
+
import { formatTable, truncEnd } from './table.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
4
|
-
*
|
|
5
|
+
* Flatten a nested object into dot-notation keys.
|
|
6
|
+
* Arrays are JSON-stringified; nested objects are recursed.
|
|
7
|
+
*
|
|
8
|
+
* Note: this assumes input objects do not contain literal dot-notation keys
|
|
9
|
+
* (e.g. `{ "a.b": 1 }`). If they do, flattened keys will silently collide
|
|
10
|
+
* with nested paths (e.g. `{ a: { b: 2 } }` also produces `"a.b"`).
|
|
11
|
+
*/
|
|
12
|
+
function flattenObject(obj, prefix = '') {
|
|
13
|
+
const result = {};
|
|
14
|
+
for (const [key, value] of Object.entries(obj)) {
|
|
15
|
+
const fullKey = prefix ? `${prefix}.${key}` : key;
|
|
16
|
+
if (
|
|
17
|
+
value !== null &&
|
|
18
|
+
typeof value === 'object' &&
|
|
19
|
+
!Array.isArray(value) &&
|
|
20
|
+
Object.getPrototypeOf(value) === Object.prototype
|
|
21
|
+
) {
|
|
22
|
+
Object.assign(result, flattenObject(value, fullKey));
|
|
23
|
+
} else if (Array.isArray(value)) {
|
|
24
|
+
result[fullKey] = JSON.stringify(value);
|
|
25
|
+
} else {
|
|
26
|
+
result[fullKey] = value;
|
|
27
|
+
}
|
|
28
|
+
}
|
|
29
|
+
return result;
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Flatten items array and derive column names.
|
|
34
|
+
* Shared by printCsv and printAutoTable.
|
|
35
|
+
* @returns {{ flatItems: object[], columns: string[] } | null}
|
|
36
|
+
*/
|
|
37
|
+
function prepareFlatItems(data, field) {
|
|
38
|
+
const items = field ? data[field] : data;
|
|
39
|
+
if (!Array.isArray(items)) return null;
|
|
40
|
+
|
|
41
|
+
const flatItems = items.map((item) =>
|
|
42
|
+
typeof item === 'object' && item !== null && !Array.isArray(item)
|
|
43
|
+
? flattenObject(item)
|
|
44
|
+
: { value: item },
|
|
45
|
+
);
|
|
46
|
+
const columns = (() => {
|
|
47
|
+
const keys = new Set();
|
|
48
|
+
for (const item of flatItems) for (const key of Object.keys(item)) keys.add(key);
|
|
49
|
+
return [...keys];
|
|
50
|
+
})();
|
|
51
|
+
if (columns.length === 0) columns.push('value');
|
|
52
|
+
|
|
53
|
+
return { flatItems, columns };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
/** Escape a value for CSV output (LF line endings). */
|
|
57
|
+
function escapeCsv(val) {
|
|
58
|
+
const str = val == null ? '' : String(val);
|
|
59
|
+
if (str.includes(',') || str.includes('"') || str.includes('\n') || str.includes('\r')) {
|
|
60
|
+
return `"${str.replace(/"/g, '""')}"`;
|
|
61
|
+
}
|
|
62
|
+
return str;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
/**
|
|
66
|
+
* Print data as CSV to stdout.
|
|
67
|
+
* @param {object} data - Result object from a *Data() function
|
|
68
|
+
* @param {string} field - Array field name (e.g. 'results')
|
|
69
|
+
*/
|
|
70
|
+
function printCsv(data, field) {
|
|
71
|
+
const prepared = prepareFlatItems(data, field);
|
|
72
|
+
if (!prepared) return false;
|
|
73
|
+
const { flatItems, columns } = prepared;
|
|
74
|
+
|
|
75
|
+
console.log(columns.map(escapeCsv).join(','));
|
|
76
|
+
for (const row of flatItems) {
|
|
77
|
+
console.log(columns.map((col) => escapeCsv(row[col])).join(','));
|
|
78
|
+
}
|
|
79
|
+
return true;
|
|
80
|
+
}
|
|
81
|
+
|
|
82
|
+
const MAX_COL_WIDTH = 40;
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Print data as an aligned table to stdout.
|
|
86
|
+
* @param {object} data - Result object from a *Data() function
|
|
87
|
+
* @param {string} field - Array field name (e.g. 'results')
|
|
88
|
+
*/
|
|
89
|
+
function printAutoTable(data, field) {
|
|
90
|
+
const prepared = prepareFlatItems(data, field);
|
|
91
|
+
if (!prepared) return false;
|
|
92
|
+
const { flatItems, columns } = prepared;
|
|
93
|
+
|
|
94
|
+
const colDefs = columns.map((col) => {
|
|
95
|
+
const maxLen = flatItems.reduce(
|
|
96
|
+
(max, item) => Math.max(max, String(item[col] ?? '').length),
|
|
97
|
+
col.length,
|
|
98
|
+
);
|
|
99
|
+
const isNumeric =
|
|
100
|
+
flatItems.length > 0 &&
|
|
101
|
+
flatItems.every((item) => {
|
|
102
|
+
const v = item[col];
|
|
103
|
+
return v == null || v === '' || (typeof v !== 'boolean' && Number.isFinite(Number(v)));
|
|
104
|
+
});
|
|
105
|
+
return {
|
|
106
|
+
header: col,
|
|
107
|
+
width: Math.min(maxLen, MAX_COL_WIDTH),
|
|
108
|
+
align: isNumeric ? 'right' : 'left',
|
|
109
|
+
};
|
|
110
|
+
});
|
|
111
|
+
|
|
112
|
+
const rows = flatItems.map((item) =>
|
|
113
|
+
columns.map((col) => truncEnd(String(item[col] ?? ''), MAX_COL_WIDTH)),
|
|
114
|
+
);
|
|
115
|
+
|
|
116
|
+
console.log(formatTable({ columns: colDefs, rows }));
|
|
117
|
+
return true;
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Shared JSON / NDJSON / table / CSV output dispatch for CLI wrappers.
|
|
5
122
|
*
|
|
6
123
|
* @param {object} data - Result object from a *Data() function
|
|
7
124
|
* @param {string} field - Array field name for NDJSON streaming (e.g. 'results')
|
|
8
|
-
* @param {object} opts - CLI options ({ json?, ndjson? })
|
|
125
|
+
* @param {object} opts - CLI options ({ json?, ndjson?, table?, csv? })
|
|
9
126
|
* @returns {boolean} true if output was handled (caller should return early)
|
|
10
127
|
*/
|
|
11
128
|
export function outputResult(data, field, opts) {
|
|
@@ -17,5 +134,11 @@ export function outputResult(data, field, opts) {
|
|
|
17
134
|
console.log(JSON.stringify(data, null, 2));
|
|
18
135
|
return true;
|
|
19
136
|
}
|
|
137
|
+
if (opts.csv) {
|
|
138
|
+
return printCsv(data, field) !== false;
|
|
139
|
+
}
|
|
140
|
+
if (opts.table) {
|
|
141
|
+
return printAutoTable(data, field) !== false;
|
|
142
|
+
}
|
|
20
143
|
return false;
|
|
21
144
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
|
+
import { kindIcon } from '../domain/queries.js';
|
|
2
|
+
import { sequenceData, sequenceToMermaid } from '../features/sequence.js';
|
|
1
3
|
import { outputResult } from '../infrastructure/result-formatter.js';
|
|
2
|
-
import { kindIcon } from '../queries.js';
|
|
3
|
-
import { sequenceData, sequenceToMermaid } from '../sequence.js';
|
|
4
4
|
|
|
5
5
|
/**
|
|
6
6
|
* CLI entry point — format sequence data as mermaid, JSON, or ndjson.
|
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import path from 'node:path';
|
|
2
|
-
import { hotspotsData, moduleBoundariesData, structureData } from '../structure.js';
|
|
2
|
+
import { hotspotsData, moduleBoundariesData, structureData } from '../features/structure.js';
|
|
3
3
|
|
|
4
4
|
export { hotspotsData, moduleBoundariesData, structureData };
|
|
5
5
|
|
|
@@ -37,11 +37,3 @@ export function truncEnd(str, maxLen) {
|
|
|
37
37
|
if (str.length <= maxLen) return str;
|
|
38
38
|
return `${str.slice(0, maxLen - 1)}\u2026`;
|
|
39
39
|
}
|
|
40
|
-
|
|
41
|
-
/**
|
|
42
|
-
* Truncate a string from the start, prepending '\u2026' if truncated.
|
|
43
|
-
*/
|
|
44
|
-
export function truncStart(str, maxLen) {
|
|
45
|
-
if (str.length <= maxLen) return str;
|
|
46
|
-
return `\u2026${str.slice(-(maxLen - 1))}`;
|
|
47
|
-
}
|
package/src/shared/file-utils.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'node:fs';
|
|
2
2
|
import path from 'node:path';
|
|
3
|
-
import {
|
|
4
|
-
import {
|
|
3
|
+
import { LANGUAGE_REGISTRY } from '../domain/parser.js';
|
|
4
|
+
import { debug } from '../infrastructure/logger.js';
|
|
5
5
|
|
|
6
6
|
/**
|
|
7
7
|
* Resolve a file path relative to repoRoot, rejecting traversal outside the repo.
|
package/src/shared/generators.js
CHANGED
|
@@ -1,6 +1,7 @@
|
|
|
1
|
-
import { iterateFunctionNodes, openReadonlyOrFail } from '../db.js';
|
|
1
|
+
import { iterateFunctionNodes, openReadonlyOrFail } from '../db/index.js';
|
|
2
|
+
import { buildFileConditionSQL } from '../db/query-builder.js';
|
|
2
3
|
import { isTestFile } from '../infrastructure/test-filter.js';
|
|
3
|
-
import { ALL_SYMBOL_KINDS } from '
|
|
4
|
+
import { ALL_SYMBOL_KINDS } from './kinds.js';
|
|
4
5
|
|
|
5
6
|
/**
|
|
6
7
|
* Generator: stream functions one-by-one using .iterate() for memory efficiency.
|
|
@@ -52,9 +53,12 @@ export function* iterRoles(customDbPath, opts = {}) {
|
|
|
52
53
|
conditions.push('role = ?');
|
|
53
54
|
params.push(opts.role);
|
|
54
55
|
}
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
56
|
+
{
|
|
57
|
+
const fc = buildFileConditionSQL(opts.file, 'file');
|
|
58
|
+
if (fc.sql) {
|
|
59
|
+
conditions.push(fc.sql.replace(/^ AND /, ''));
|
|
60
|
+
params.push(...fc.params);
|
|
61
|
+
}
|
|
58
62
|
}
|
|
59
63
|
|
|
60
64
|
const stmt = db.prepare(
|
package/src/shared/hierarchy.js
CHANGED
|
@@ -47,4 +47,4 @@ export const STRUCTURAL_EDGE_KINDS = ['parameter_of', 'receiver'];
|
|
|
47
47
|
// Full set for MCP enum and validation
|
|
48
48
|
export const EVERY_EDGE_KIND = [...CORE_EDGE_KINDS, ...STRUCTURAL_EDGE_KINDS];
|
|
49
49
|
|
|
50
|
-
export const VALID_ROLES = ['entry', 'core', 'utility', 'adapter', 'dead', 'leaf'];
|
|
50
|
+
export const VALID_ROLES = ['entry', 'core', 'utility', 'adapter', 'dead', 'test-only', 'leaf'];
|
package/src/analysis/context.js
DELETED
|
@@ -1,408 +0,0 @@
|
|
|
1
|
-
import path from 'node:path';
|
|
2
|
-
import {
|
|
3
|
-
findCallees,
|
|
4
|
-
findCallers,
|
|
5
|
-
findCrossFileCallTargets,
|
|
6
|
-
findDbPath,
|
|
7
|
-
findFileNodes,
|
|
8
|
-
findImportSources,
|
|
9
|
-
findImportTargets,
|
|
10
|
-
findIntraFileCallEdges,
|
|
11
|
-
findNodeChildren,
|
|
12
|
-
findNodesByFile,
|
|
13
|
-
getComplexityForNode,
|
|
14
|
-
openReadonlyOrFail,
|
|
15
|
-
} from '../db.js';
|
|
16
|
-
import { isTestFile } from '../infrastructure/test-filter.js';
|
|
17
|
-
import { paginateResult } from '../paginate.js';
|
|
18
|
-
import {
|
|
19
|
-
createFileLinesReader,
|
|
20
|
-
extractSignature,
|
|
21
|
-
extractSummary,
|
|
22
|
-
isFileLikeTarget,
|
|
23
|
-
readSourceRange,
|
|
24
|
-
} from '../shared/file-utils.js';
|
|
25
|
-
import { resolveMethodViaHierarchy } from '../shared/hierarchy.js';
|
|
26
|
-
import { normalizeSymbol } from '../shared/normalize.js';
|
|
27
|
-
import { findMatchingNodes } from './symbol-lookup.js';
|
|
28
|
-
|
|
29
|
-
function explainFileImpl(db, target, getFileLines) {
|
|
30
|
-
const fileNodes = findFileNodes(db, `%${target}%`);
|
|
31
|
-
if (fileNodes.length === 0) return [];
|
|
32
|
-
|
|
33
|
-
return fileNodes.map((fn) => {
|
|
34
|
-
const symbols = findNodesByFile(db, fn.file);
|
|
35
|
-
|
|
36
|
-
// IDs of symbols that have incoming calls from other files (public)
|
|
37
|
-
const publicIds = findCrossFileCallTargets(db, fn.file);
|
|
38
|
-
|
|
39
|
-
const fileLines = getFileLines(fn.file);
|
|
40
|
-
const mapSymbol = (s) => ({
|
|
41
|
-
name: s.name,
|
|
42
|
-
kind: s.kind,
|
|
43
|
-
line: s.line,
|
|
44
|
-
role: s.role || null,
|
|
45
|
-
summary: fileLines ? extractSummary(fileLines, s.line) : null,
|
|
46
|
-
signature: fileLines ? extractSignature(fileLines, s.line) : null,
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
const publicApi = symbols.filter((s) => publicIds.has(s.id)).map(mapSymbol);
|
|
50
|
-
const internal = symbols.filter((s) => !publicIds.has(s.id)).map(mapSymbol);
|
|
51
|
-
|
|
52
|
-
// Imports / importedBy
|
|
53
|
-
const imports = findImportTargets(db, fn.id).map((r) => ({ file: r.file }));
|
|
54
|
-
|
|
55
|
-
const importedBy = findImportSources(db, fn.id).map((r) => ({ file: r.file }));
|
|
56
|
-
|
|
57
|
-
// Intra-file data flow
|
|
58
|
-
const intraEdges = findIntraFileCallEdges(db, fn.file);
|
|
59
|
-
|
|
60
|
-
const dataFlowMap = new Map();
|
|
61
|
-
for (const edge of intraEdges) {
|
|
62
|
-
if (!dataFlowMap.has(edge.caller_name)) dataFlowMap.set(edge.caller_name, []);
|
|
63
|
-
dataFlowMap.get(edge.caller_name).push(edge.callee_name);
|
|
64
|
-
}
|
|
65
|
-
const dataFlow = [...dataFlowMap.entries()].map(([caller, callees]) => ({
|
|
66
|
-
caller,
|
|
67
|
-
callees,
|
|
68
|
-
}));
|
|
69
|
-
|
|
70
|
-
// Line count: prefer node_metrics (actual), fall back to MAX(end_line)
|
|
71
|
-
const metric = db
|
|
72
|
-
.prepare(`SELECT nm.line_count FROM node_metrics nm WHERE nm.node_id = ?`)
|
|
73
|
-
.get(fn.id);
|
|
74
|
-
let lineCount = metric?.line_count || null;
|
|
75
|
-
if (!lineCount) {
|
|
76
|
-
const maxLine = db
|
|
77
|
-
.prepare(`SELECT MAX(end_line) as max_end FROM nodes WHERE file = ?`)
|
|
78
|
-
.get(fn.file);
|
|
79
|
-
lineCount = maxLine?.max_end || null;
|
|
80
|
-
}
|
|
81
|
-
|
|
82
|
-
return {
|
|
83
|
-
file: fn.file,
|
|
84
|
-
lineCount,
|
|
85
|
-
symbolCount: symbols.length,
|
|
86
|
-
publicApi,
|
|
87
|
-
internal,
|
|
88
|
-
imports,
|
|
89
|
-
importedBy,
|
|
90
|
-
dataFlow,
|
|
91
|
-
};
|
|
92
|
-
});
|
|
93
|
-
}
|
|
94
|
-
|
|
95
|
-
function explainFunctionImpl(db, target, noTests, getFileLines) {
|
|
96
|
-
let nodes = db
|
|
97
|
-
.prepare(
|
|
98
|
-
`SELECT * FROM nodes WHERE name LIKE ? AND kind IN ('function','method','class','interface','type','struct','enum','trait','record','module') ORDER BY file, line`,
|
|
99
|
-
)
|
|
100
|
-
.all(`%${target}%`);
|
|
101
|
-
if (noTests) nodes = nodes.filter((n) => !isTestFile(n.file));
|
|
102
|
-
if (nodes.length === 0) return [];
|
|
103
|
-
|
|
104
|
-
const hc = new Map();
|
|
105
|
-
return nodes.slice(0, 10).map((node) => {
|
|
106
|
-
const fileLines = getFileLines(node.file);
|
|
107
|
-
const lineCount = node.end_line ? node.end_line - node.line + 1 : null;
|
|
108
|
-
const summary = fileLines ? extractSummary(fileLines, node.line) : null;
|
|
109
|
-
const signature = fileLines ? extractSignature(fileLines, node.line) : null;
|
|
110
|
-
|
|
111
|
-
const callees = findCallees(db, node.id).map((c) => ({
|
|
112
|
-
name: c.name,
|
|
113
|
-
kind: c.kind,
|
|
114
|
-
file: c.file,
|
|
115
|
-
line: c.line,
|
|
116
|
-
}));
|
|
117
|
-
|
|
118
|
-
let callers = findCallers(db, node.id).map((c) => ({
|
|
119
|
-
name: c.name,
|
|
120
|
-
kind: c.kind,
|
|
121
|
-
file: c.file,
|
|
122
|
-
line: c.line,
|
|
123
|
-
}));
|
|
124
|
-
if (noTests) callers = callers.filter((c) => !isTestFile(c.file));
|
|
125
|
-
|
|
126
|
-
const testCallerRows = findCallers(db, node.id);
|
|
127
|
-
const seenFiles = new Set();
|
|
128
|
-
const relatedTests = testCallerRows
|
|
129
|
-
.filter((r) => isTestFile(r.file) && !seenFiles.has(r.file) && seenFiles.add(r.file))
|
|
130
|
-
.map((r) => ({ file: r.file }));
|
|
131
|
-
|
|
132
|
-
// Complexity metrics
|
|
133
|
-
let complexityMetrics = null;
|
|
134
|
-
try {
|
|
135
|
-
const cRow = getComplexityForNode(db, node.id);
|
|
136
|
-
if (cRow) {
|
|
137
|
-
complexityMetrics = {
|
|
138
|
-
cognitive: cRow.cognitive,
|
|
139
|
-
cyclomatic: cRow.cyclomatic,
|
|
140
|
-
maxNesting: cRow.max_nesting,
|
|
141
|
-
maintainabilityIndex: cRow.maintainability_index || 0,
|
|
142
|
-
halsteadVolume: cRow.halstead_volume || 0,
|
|
143
|
-
};
|
|
144
|
-
}
|
|
145
|
-
} catch {
|
|
146
|
-
/* table may not exist */
|
|
147
|
-
}
|
|
148
|
-
|
|
149
|
-
return {
|
|
150
|
-
...normalizeSymbol(node, db, hc),
|
|
151
|
-
lineCount,
|
|
152
|
-
summary,
|
|
153
|
-
signature,
|
|
154
|
-
complexity: complexityMetrics,
|
|
155
|
-
callees,
|
|
156
|
-
callers,
|
|
157
|
-
relatedTests,
|
|
158
|
-
};
|
|
159
|
-
});
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
// ─── Exported functions ──────────────────────────────────────────────────
|
|
163
|
-
|
|
164
|
-
export function contextData(name, customDbPath, opts = {}) {
|
|
165
|
-
const db = openReadonlyOrFail(customDbPath);
|
|
166
|
-
try {
|
|
167
|
-
const depth = opts.depth || 0;
|
|
168
|
-
const noSource = opts.noSource || false;
|
|
169
|
-
const noTests = opts.noTests || false;
|
|
170
|
-
const includeTests = opts.includeTests || false;
|
|
171
|
-
|
|
172
|
-
const dbPath = findDbPath(customDbPath);
|
|
173
|
-
const repoRoot = path.resolve(path.dirname(dbPath), '..');
|
|
174
|
-
|
|
175
|
-
const nodes = findMatchingNodes(db, name, { noTests, file: opts.file, kind: opts.kind });
|
|
176
|
-
if (nodes.length === 0) {
|
|
177
|
-
return { name, results: [] };
|
|
178
|
-
}
|
|
179
|
-
|
|
180
|
-
// No hardcoded slice — pagination handles bounding via limit/offset
|
|
181
|
-
|
|
182
|
-
const getFileLines = createFileLinesReader(repoRoot);
|
|
183
|
-
|
|
184
|
-
const results = nodes.map((node) => {
|
|
185
|
-
const fileLines = getFileLines(node.file);
|
|
186
|
-
|
|
187
|
-
// Source
|
|
188
|
-
const source = noSource
|
|
189
|
-
? null
|
|
190
|
-
: readSourceRange(repoRoot, node.file, node.line, node.end_line);
|
|
191
|
-
|
|
192
|
-
// Signature
|
|
193
|
-
const signature = fileLines ? extractSignature(fileLines, node.line) : null;
|
|
194
|
-
|
|
195
|
-
// Callees
|
|
196
|
-
const calleeRows = findCallees(db, node.id);
|
|
197
|
-
const filteredCallees = noTests ? calleeRows.filter((c) => !isTestFile(c.file)) : calleeRows;
|
|
198
|
-
|
|
199
|
-
const callees = filteredCallees.map((c) => {
|
|
200
|
-
const cLines = getFileLines(c.file);
|
|
201
|
-
const summary = cLines ? extractSummary(cLines, c.line) : null;
|
|
202
|
-
let calleeSource = null;
|
|
203
|
-
if (depth >= 1) {
|
|
204
|
-
calleeSource = readSourceRange(repoRoot, c.file, c.line, c.end_line);
|
|
205
|
-
}
|
|
206
|
-
return {
|
|
207
|
-
name: c.name,
|
|
208
|
-
kind: c.kind,
|
|
209
|
-
file: c.file,
|
|
210
|
-
line: c.line,
|
|
211
|
-
endLine: c.end_line || null,
|
|
212
|
-
summary,
|
|
213
|
-
source: calleeSource,
|
|
214
|
-
};
|
|
215
|
-
});
|
|
216
|
-
|
|
217
|
-
// Deep callee expansion via BFS (depth > 1, capped at 5)
|
|
218
|
-
if (depth > 1) {
|
|
219
|
-
const visited = new Set(filteredCallees.map((c) => c.id));
|
|
220
|
-
visited.add(node.id);
|
|
221
|
-
let frontier = filteredCallees.map((c) => c.id);
|
|
222
|
-
const maxDepth = Math.min(depth, 5);
|
|
223
|
-
for (let d = 2; d <= maxDepth; d++) {
|
|
224
|
-
const nextFrontier = [];
|
|
225
|
-
for (const fid of frontier) {
|
|
226
|
-
const deeper = findCallees(db, fid);
|
|
227
|
-
for (const c of deeper) {
|
|
228
|
-
if (!visited.has(c.id) && (!noTests || !isTestFile(c.file))) {
|
|
229
|
-
visited.add(c.id);
|
|
230
|
-
nextFrontier.push(c.id);
|
|
231
|
-
const cLines = getFileLines(c.file);
|
|
232
|
-
callees.push({
|
|
233
|
-
name: c.name,
|
|
234
|
-
kind: c.kind,
|
|
235
|
-
file: c.file,
|
|
236
|
-
line: c.line,
|
|
237
|
-
endLine: c.end_line || null,
|
|
238
|
-
summary: cLines ? extractSummary(cLines, c.line) : null,
|
|
239
|
-
source: readSourceRange(repoRoot, c.file, c.line, c.end_line),
|
|
240
|
-
});
|
|
241
|
-
}
|
|
242
|
-
}
|
|
243
|
-
}
|
|
244
|
-
frontier = nextFrontier;
|
|
245
|
-
if (frontier.length === 0) break;
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
|
|
249
|
-
// Callers
|
|
250
|
-
let callerRows = findCallers(db, node.id);
|
|
251
|
-
|
|
252
|
-
// Method hierarchy resolution
|
|
253
|
-
if (node.kind === 'method' && node.name.includes('.')) {
|
|
254
|
-
const methodName = node.name.split('.').pop();
|
|
255
|
-
const relatedMethods = resolveMethodViaHierarchy(db, methodName);
|
|
256
|
-
for (const rm of relatedMethods) {
|
|
257
|
-
if (rm.id === node.id) continue;
|
|
258
|
-
const extraCallers = findCallers(db, rm.id);
|
|
259
|
-
callerRows.push(...extraCallers.map((c) => ({ ...c, viaHierarchy: rm.name })));
|
|
260
|
-
}
|
|
261
|
-
}
|
|
262
|
-
if (noTests) callerRows = callerRows.filter((c) => !isTestFile(c.file));
|
|
263
|
-
|
|
264
|
-
const callers = callerRows.map((c) => ({
|
|
265
|
-
name: c.name,
|
|
266
|
-
kind: c.kind,
|
|
267
|
-
file: c.file,
|
|
268
|
-
line: c.line,
|
|
269
|
-
viaHierarchy: c.viaHierarchy || undefined,
|
|
270
|
-
}));
|
|
271
|
-
|
|
272
|
-
// Related tests: callers that live in test files
|
|
273
|
-
const testCallerRows = findCallers(db, node.id);
|
|
274
|
-
const testCallers = testCallerRows.filter((c) => isTestFile(c.file));
|
|
275
|
-
|
|
276
|
-
const testsByFile = new Map();
|
|
277
|
-
for (const tc of testCallers) {
|
|
278
|
-
if (!testsByFile.has(tc.file)) testsByFile.set(tc.file, []);
|
|
279
|
-
testsByFile.get(tc.file).push(tc);
|
|
280
|
-
}
|
|
281
|
-
|
|
282
|
-
const relatedTests = [];
|
|
283
|
-
for (const [file] of testsByFile) {
|
|
284
|
-
const tLines = getFileLines(file);
|
|
285
|
-
const testNames = [];
|
|
286
|
-
if (tLines) {
|
|
287
|
-
for (const tl of tLines) {
|
|
288
|
-
const tm = tl.match(/(?:it|test|describe)\s*\(\s*['"`]([^'"`]+)['"`]/);
|
|
289
|
-
if (tm) testNames.push(tm[1]);
|
|
290
|
-
}
|
|
291
|
-
}
|
|
292
|
-
const testSource = includeTests && tLines ? tLines.join('\n') : undefined;
|
|
293
|
-
relatedTests.push({
|
|
294
|
-
file,
|
|
295
|
-
testCount: testNames.length,
|
|
296
|
-
testNames,
|
|
297
|
-
source: testSource,
|
|
298
|
-
});
|
|
299
|
-
}
|
|
300
|
-
|
|
301
|
-
// Complexity metrics
|
|
302
|
-
let complexityMetrics = null;
|
|
303
|
-
try {
|
|
304
|
-
const cRow = getComplexityForNode(db, node.id);
|
|
305
|
-
if (cRow) {
|
|
306
|
-
complexityMetrics = {
|
|
307
|
-
cognitive: cRow.cognitive,
|
|
308
|
-
cyclomatic: cRow.cyclomatic,
|
|
309
|
-
maxNesting: cRow.max_nesting,
|
|
310
|
-
maintainabilityIndex: cRow.maintainability_index || 0,
|
|
311
|
-
halsteadVolume: cRow.halstead_volume || 0,
|
|
312
|
-
};
|
|
313
|
-
}
|
|
314
|
-
} catch {
|
|
315
|
-
/* table may not exist */
|
|
316
|
-
}
|
|
317
|
-
|
|
318
|
-
// Children (parameters, properties, constants)
|
|
319
|
-
let nodeChildren = [];
|
|
320
|
-
try {
|
|
321
|
-
nodeChildren = findNodeChildren(db, node.id).map((c) => ({
|
|
322
|
-
name: c.name,
|
|
323
|
-
kind: c.kind,
|
|
324
|
-
line: c.line,
|
|
325
|
-
endLine: c.end_line || null,
|
|
326
|
-
}));
|
|
327
|
-
} catch {
|
|
328
|
-
/* parent_id column may not exist */
|
|
329
|
-
}
|
|
330
|
-
|
|
331
|
-
return {
|
|
332
|
-
name: node.name,
|
|
333
|
-
kind: node.kind,
|
|
334
|
-
file: node.file,
|
|
335
|
-
line: node.line,
|
|
336
|
-
role: node.role || null,
|
|
337
|
-
endLine: node.end_line || null,
|
|
338
|
-
source,
|
|
339
|
-
signature,
|
|
340
|
-
complexity: complexityMetrics,
|
|
341
|
-
children: nodeChildren.length > 0 ? nodeChildren : undefined,
|
|
342
|
-
callees,
|
|
343
|
-
callers,
|
|
344
|
-
relatedTests,
|
|
345
|
-
};
|
|
346
|
-
});
|
|
347
|
-
|
|
348
|
-
const base = { name, results };
|
|
349
|
-
return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
|
|
350
|
-
} finally {
|
|
351
|
-
db.close();
|
|
352
|
-
}
|
|
353
|
-
}
|
|
354
|
-
|
|
355
|
-
export function explainData(target, customDbPath, opts = {}) {
|
|
356
|
-
const db = openReadonlyOrFail(customDbPath);
|
|
357
|
-
try {
|
|
358
|
-
const noTests = opts.noTests || false;
|
|
359
|
-
const depth = opts.depth || 0;
|
|
360
|
-
const kind = isFileLikeTarget(target) ? 'file' : 'function';
|
|
361
|
-
|
|
362
|
-
const dbPath = findDbPath(customDbPath);
|
|
363
|
-
const repoRoot = path.resolve(path.dirname(dbPath), '..');
|
|
364
|
-
|
|
365
|
-
const getFileLines = createFileLinesReader(repoRoot);
|
|
366
|
-
|
|
367
|
-
const results =
|
|
368
|
-
kind === 'file'
|
|
369
|
-
? explainFileImpl(db, target, getFileLines)
|
|
370
|
-
: explainFunctionImpl(db, target, noTests, getFileLines);
|
|
371
|
-
|
|
372
|
-
// Recursive dependency explanation for function targets
|
|
373
|
-
if (kind === 'function' && depth > 0 && results.length > 0) {
|
|
374
|
-
const visited = new Set(results.map((r) => `${r.name}:${r.file}:${r.line}`));
|
|
375
|
-
|
|
376
|
-
function explainCallees(parentResults, currentDepth) {
|
|
377
|
-
if (currentDepth <= 0) return;
|
|
378
|
-
for (const r of parentResults) {
|
|
379
|
-
const newCallees = [];
|
|
380
|
-
for (const callee of r.callees) {
|
|
381
|
-
const key = `${callee.name}:${callee.file}:${callee.line}`;
|
|
382
|
-
if (visited.has(key)) continue;
|
|
383
|
-
visited.add(key);
|
|
384
|
-
const calleeResults = explainFunctionImpl(db, callee.name, noTests, getFileLines);
|
|
385
|
-
const exact = calleeResults.find(
|
|
386
|
-
(cr) => cr.file === callee.file && cr.line === callee.line,
|
|
387
|
-
);
|
|
388
|
-
if (exact) {
|
|
389
|
-
exact._depth = (r._depth || 0) + 1;
|
|
390
|
-
newCallees.push(exact);
|
|
391
|
-
}
|
|
392
|
-
}
|
|
393
|
-
if (newCallees.length > 0) {
|
|
394
|
-
r.depDetails = newCallees;
|
|
395
|
-
explainCallees(newCallees, currentDepth - 1);
|
|
396
|
-
}
|
|
397
|
-
}
|
|
398
|
-
}
|
|
399
|
-
|
|
400
|
-
explainCallees(results, depth);
|
|
401
|
-
}
|
|
402
|
-
|
|
403
|
-
const base = { target, kind, results };
|
|
404
|
-
return paginateResult(base, 'results', { limit: opts.limit, offset: opts.offset });
|
|
405
|
-
} finally {
|
|
406
|
-
db.close();
|
|
407
|
-
}
|
|
408
|
-
}
|