@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
|
@@ -15,23 +15,159 @@ import {
|
|
|
15
15
|
readFileSafe,
|
|
16
16
|
} from '../helpers.js';
|
|
17
17
|
|
|
18
|
+
// ── Phase 1: Insert file nodes, definitions, exports ────────────────────
|
|
19
|
+
|
|
20
|
+
function insertDefinitionsAndExports(db, allSymbols) {
|
|
21
|
+
const phase1Rows = [];
|
|
22
|
+
for (const [relPath, symbols] of allSymbols) {
|
|
23
|
+
phase1Rows.push([relPath, 'file', relPath, 0, null, null, null, null, null]);
|
|
24
|
+
for (const def of symbols.definitions) {
|
|
25
|
+
const dotIdx = def.name.lastIndexOf('.');
|
|
26
|
+
const scope = dotIdx !== -1 ? def.name.slice(0, dotIdx) : null;
|
|
27
|
+
phase1Rows.push([
|
|
28
|
+
def.name,
|
|
29
|
+
def.kind,
|
|
30
|
+
relPath,
|
|
31
|
+
def.line,
|
|
32
|
+
def.endLine || null,
|
|
33
|
+
null,
|
|
34
|
+
def.name,
|
|
35
|
+
scope,
|
|
36
|
+
def.visibility || null,
|
|
37
|
+
]);
|
|
38
|
+
}
|
|
39
|
+
for (const exp of symbols.exports) {
|
|
40
|
+
phase1Rows.push([exp.name, exp.kind, relPath, exp.line, null, null, exp.name, null, null]);
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
batchInsertNodes(db, phase1Rows);
|
|
44
|
+
|
|
45
|
+
// Mark exported symbols
|
|
46
|
+
const markExported = db.prepare(
|
|
47
|
+
'UPDATE nodes SET exported = 1 WHERE name = ? AND kind = ? AND file = ? AND line = ?',
|
|
48
|
+
);
|
|
49
|
+
for (const [relPath, symbols] of allSymbols) {
|
|
50
|
+
for (const exp of symbols.exports) {
|
|
51
|
+
markExported.run(exp.name, exp.kind, relPath, exp.line);
|
|
52
|
+
}
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
// ── Phase 2: Insert children (needs parent IDs) ────────────────────────
|
|
57
|
+
|
|
58
|
+
function insertChildren(db, allSymbols) {
|
|
59
|
+
const childRows = [];
|
|
60
|
+
for (const [relPath, symbols] of allSymbols) {
|
|
61
|
+
const nodeIdMap = new Map();
|
|
62
|
+
for (const row of bulkNodeIdsByFile(db, relPath)) {
|
|
63
|
+
nodeIdMap.set(`${row.name}|${row.kind}|${row.line}`, row.id);
|
|
64
|
+
}
|
|
65
|
+
for (const def of symbols.definitions) {
|
|
66
|
+
if (!def.children?.length) continue;
|
|
67
|
+
const defId = nodeIdMap.get(`${def.name}|${def.kind}|${def.line}`);
|
|
68
|
+
if (!defId) continue;
|
|
69
|
+
for (const child of def.children) {
|
|
70
|
+
const qualifiedName = `${def.name}.${child.name}`;
|
|
71
|
+
childRows.push([
|
|
72
|
+
child.name,
|
|
73
|
+
child.kind,
|
|
74
|
+
relPath,
|
|
75
|
+
child.line,
|
|
76
|
+
child.endLine || null,
|
|
77
|
+
defId,
|
|
78
|
+
qualifiedName,
|
|
79
|
+
def.name,
|
|
80
|
+
child.visibility || null,
|
|
81
|
+
]);
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
batchInsertNodes(db, childRows);
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
// ── Phase 3: Insert containment + parameter_of edges ────────────────────
|
|
89
|
+
|
|
90
|
+
function insertContainmentEdges(db, allSymbols) {
|
|
91
|
+
const edgeRows = [];
|
|
92
|
+
for (const [relPath, symbols] of allSymbols) {
|
|
93
|
+
const nodeIdMap = new Map();
|
|
94
|
+
for (const row of bulkNodeIdsByFile(db, relPath)) {
|
|
95
|
+
nodeIdMap.set(`${row.name}|${row.kind}|${row.line}`, row.id);
|
|
96
|
+
}
|
|
97
|
+
const fileId = nodeIdMap.get(`${relPath}|file|0`);
|
|
98
|
+
for (const def of symbols.definitions) {
|
|
99
|
+
const defId = nodeIdMap.get(`${def.name}|${def.kind}|${def.line}`);
|
|
100
|
+
if (fileId && defId) {
|
|
101
|
+
edgeRows.push([fileId, defId, 'contains', 1.0, 0]);
|
|
102
|
+
}
|
|
103
|
+
if (def.children?.length && defId) {
|
|
104
|
+
for (const child of def.children) {
|
|
105
|
+
const childId = nodeIdMap.get(`${child.name}|${child.kind}|${child.line}`);
|
|
106
|
+
if (childId) {
|
|
107
|
+
edgeRows.push([defId, childId, 'contains', 1.0, 0]);
|
|
108
|
+
if (child.kind === 'parameter') {
|
|
109
|
+
edgeRows.push([childId, defId, 'parameter_of', 1.0, 0]);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
batchInsertEdges(db, edgeRows);
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
// ── Phase 4: Update file hashes ─────────────────────────────────────────
|
|
120
|
+
|
|
121
|
+
function updateFileHashes(_db, allSymbols, precomputedData, metadataUpdates, rootDir, upsertHash) {
|
|
122
|
+
if (!upsertHash) return;
|
|
123
|
+
|
|
124
|
+
for (const [relPath] of allSymbols) {
|
|
125
|
+
const precomputed = precomputedData.get(relPath);
|
|
126
|
+
if (precomputed?._reverseDepOnly) {
|
|
127
|
+
// no-op: file unchanged, hash already correct
|
|
128
|
+
} else if (precomputed?.hash) {
|
|
129
|
+
const stat = precomputed.stat || fileStat(path.join(rootDir, relPath));
|
|
130
|
+
const mtime = stat ? Math.floor(stat.mtimeMs) : 0;
|
|
131
|
+
const size = stat ? stat.size : 0;
|
|
132
|
+
upsertHash.run(relPath, precomputed.hash, mtime, size);
|
|
133
|
+
} else {
|
|
134
|
+
const absPath = path.join(rootDir, relPath);
|
|
135
|
+
let code;
|
|
136
|
+
try {
|
|
137
|
+
code = readFileSafe(absPath);
|
|
138
|
+
} catch {
|
|
139
|
+
code = null;
|
|
140
|
+
}
|
|
141
|
+
if (code !== null) {
|
|
142
|
+
const stat = fileStat(absPath);
|
|
143
|
+
const mtime = stat ? Math.floor(stat.mtimeMs) : 0;
|
|
144
|
+
const size = stat ? stat.size : 0;
|
|
145
|
+
upsertHash.run(relPath, fileHash(code), mtime, size);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Also update metadata-only entries (self-heal mtime/size without re-parse)
|
|
151
|
+
for (const item of metadataUpdates) {
|
|
152
|
+
const mtime = item.stat ? Math.floor(item.stat.mtimeMs) : 0;
|
|
153
|
+
const size = item.stat ? item.stat.size : 0;
|
|
154
|
+
upsertHash.run(item.relPath, item.hash, mtime, size);
|
|
155
|
+
}
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// ── Main entry point ────────────────────────────────────────────────────
|
|
159
|
+
|
|
18
160
|
/**
|
|
19
161
|
* @param {import('../context.js').PipelineContext} ctx
|
|
20
162
|
*/
|
|
21
163
|
export async function insertNodes(ctx) {
|
|
22
164
|
const { db, allSymbols, filesToParse, metadataUpdates, rootDir, removed } = ctx;
|
|
23
165
|
|
|
24
|
-
// Build lookup from incremental data (pre-computed hashes + stats)
|
|
25
166
|
const precomputedData = new Map();
|
|
26
167
|
for (const item of filesToParse) {
|
|
27
|
-
if (item.relPath)
|
|
28
|
-
precomputedData.set(item.relPath, item);
|
|
29
|
-
}
|
|
168
|
+
if (item.relPath) precomputedData.set(item.relPath, item);
|
|
30
169
|
}
|
|
31
170
|
|
|
32
|
-
const bulkGetNodeIds = { all: (file) => bulkNodeIdsByFile(db, file) };
|
|
33
|
-
|
|
34
|
-
// Prepare hash upsert
|
|
35
171
|
let upsertHash;
|
|
36
172
|
try {
|
|
37
173
|
upsertHash = db.prepare(
|
|
@@ -42,143 +178,15 @@ export async function insertNodes(ctx) {
|
|
|
42
178
|
}
|
|
43
179
|
|
|
44
180
|
// Populate fileSymbols before the transaction so it is a pure input
|
|
45
|
-
// to (rather than a side-effect of) the DB write — avoids partial
|
|
46
|
-
// population if the transaction rolls back.
|
|
47
181
|
for (const [relPath, symbols] of allSymbols) {
|
|
48
182
|
ctx.fileSymbols.set(relPath, symbols);
|
|
49
183
|
}
|
|
50
184
|
|
|
51
185
|
const insertAll = db.transaction(() => {
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
phase1Rows.push([relPath, 'file', relPath, 0, null, null, null, null, null]);
|
|
57
|
-
for (const def of symbols.definitions) {
|
|
58
|
-
// Methods already have 'Class.method' as name — use as qualified_name.
|
|
59
|
-
// For methods, scope is the class portion; for top-level defs, scope is null.
|
|
60
|
-
const dotIdx = def.name.lastIndexOf('.');
|
|
61
|
-
const scope = dotIdx !== -1 ? def.name.slice(0, dotIdx) : null;
|
|
62
|
-
phase1Rows.push([
|
|
63
|
-
def.name,
|
|
64
|
-
def.kind,
|
|
65
|
-
relPath,
|
|
66
|
-
def.line,
|
|
67
|
-
def.endLine || null,
|
|
68
|
-
null,
|
|
69
|
-
def.name,
|
|
70
|
-
scope,
|
|
71
|
-
def.visibility || null,
|
|
72
|
-
]);
|
|
73
|
-
}
|
|
74
|
-
for (const exp of symbols.exports) {
|
|
75
|
-
phase1Rows.push([exp.name, exp.kind, relPath, exp.line, null, null, exp.name, null, null]);
|
|
76
|
-
}
|
|
77
|
-
}
|
|
78
|
-
batchInsertNodes(db, phase1Rows);
|
|
79
|
-
|
|
80
|
-
// Phase 1b: Mark exported symbols
|
|
81
|
-
const markExported = db.prepare(
|
|
82
|
-
'UPDATE nodes SET exported = 1 WHERE name = ? AND kind = ? AND file = ? AND line = ?',
|
|
83
|
-
);
|
|
84
|
-
for (const [relPath, symbols] of allSymbols) {
|
|
85
|
-
for (const exp of symbols.exports) {
|
|
86
|
-
markExported.run(exp.name, exp.kind, relPath, exp.line);
|
|
87
|
-
}
|
|
88
|
-
}
|
|
89
|
-
|
|
90
|
-
// Phase 3: Batch insert children (needs parent IDs from Phase 2)
|
|
91
|
-
const childRows = [];
|
|
92
|
-
for (const [relPath, symbols] of allSymbols) {
|
|
93
|
-
const nodeIdMap = new Map();
|
|
94
|
-
for (const row of bulkGetNodeIds.all(relPath)) {
|
|
95
|
-
nodeIdMap.set(`${row.name}|${row.kind}|${row.line}`, row.id);
|
|
96
|
-
}
|
|
97
|
-
for (const def of symbols.definitions) {
|
|
98
|
-
if (!def.children?.length) continue;
|
|
99
|
-
const defId = nodeIdMap.get(`${def.name}|${def.kind}|${def.line}`);
|
|
100
|
-
if (!defId) continue;
|
|
101
|
-
for (const child of def.children) {
|
|
102
|
-
const qualifiedName = `${def.name}.${child.name}`;
|
|
103
|
-
childRows.push([
|
|
104
|
-
child.name,
|
|
105
|
-
child.kind,
|
|
106
|
-
relPath,
|
|
107
|
-
child.line,
|
|
108
|
-
child.endLine || null,
|
|
109
|
-
defId,
|
|
110
|
-
qualifiedName,
|
|
111
|
-
def.name,
|
|
112
|
-
child.visibility || null,
|
|
113
|
-
]);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
}
|
|
117
|
-
batchInsertNodes(db, childRows);
|
|
118
|
-
|
|
119
|
-
// Phase 5: Batch insert contains/parameter_of edges
|
|
120
|
-
const edgeRows = [];
|
|
121
|
-
for (const [relPath, symbols] of allSymbols) {
|
|
122
|
-
const nodeIdMap = new Map();
|
|
123
|
-
for (const row of bulkGetNodeIds.all(relPath)) {
|
|
124
|
-
nodeIdMap.set(`${row.name}|${row.kind}|${row.line}`, row.id);
|
|
125
|
-
}
|
|
126
|
-
const fileId = nodeIdMap.get(`${relPath}|file|0`);
|
|
127
|
-
for (const def of symbols.definitions) {
|
|
128
|
-
const defId = nodeIdMap.get(`${def.name}|${def.kind}|${def.line}`);
|
|
129
|
-
if (fileId && defId) {
|
|
130
|
-
edgeRows.push([fileId, defId, 'contains', 1.0, 0]);
|
|
131
|
-
}
|
|
132
|
-
if (def.children?.length && defId) {
|
|
133
|
-
for (const child of def.children) {
|
|
134
|
-
const childId = nodeIdMap.get(`${child.name}|${child.kind}|${child.line}`);
|
|
135
|
-
if (childId) {
|
|
136
|
-
edgeRows.push([defId, childId, 'contains', 1.0, 0]);
|
|
137
|
-
if (child.kind === 'parameter') {
|
|
138
|
-
edgeRows.push([childId, defId, 'parameter_of', 1.0, 0]);
|
|
139
|
-
}
|
|
140
|
-
}
|
|
141
|
-
}
|
|
142
|
-
}
|
|
143
|
-
}
|
|
144
|
-
|
|
145
|
-
// Update file hash — skip reverse-dep files (unchanged)
|
|
146
|
-
if (upsertHash) {
|
|
147
|
-
const precomputed = precomputedData.get(relPath);
|
|
148
|
-
if (precomputed?._reverseDepOnly) {
|
|
149
|
-
// no-op: file unchanged, hash already correct
|
|
150
|
-
} else if (precomputed?.hash) {
|
|
151
|
-
const stat = precomputed.stat || fileStat(path.join(rootDir, relPath));
|
|
152
|
-
const mtime = stat ? Math.floor(stat.mtimeMs) : 0;
|
|
153
|
-
const size = stat ? stat.size : 0;
|
|
154
|
-
upsertHash.run(relPath, precomputed.hash, mtime, size);
|
|
155
|
-
} else {
|
|
156
|
-
const absPath = path.join(rootDir, relPath);
|
|
157
|
-
let code;
|
|
158
|
-
try {
|
|
159
|
-
code = readFileSafe(absPath);
|
|
160
|
-
} catch {
|
|
161
|
-
code = null;
|
|
162
|
-
}
|
|
163
|
-
if (code !== null) {
|
|
164
|
-
const stat = fileStat(absPath);
|
|
165
|
-
const mtime = stat ? Math.floor(stat.mtimeMs) : 0;
|
|
166
|
-
const size = stat ? stat.size : 0;
|
|
167
|
-
upsertHash.run(relPath, fileHash(code), mtime, size);
|
|
168
|
-
}
|
|
169
|
-
}
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
batchInsertEdges(db, edgeRows);
|
|
173
|
-
|
|
174
|
-
// Also update metadata-only entries (self-heal mtime/size without re-parse)
|
|
175
|
-
if (upsertHash) {
|
|
176
|
-
for (const item of metadataUpdates) {
|
|
177
|
-
const mtime = item.stat ? Math.floor(item.stat.mtimeMs) : 0;
|
|
178
|
-
const size = item.stat ? item.stat.size : 0;
|
|
179
|
-
upsertHash.run(item.relPath, item.hash, mtime, size);
|
|
180
|
-
}
|
|
181
|
-
}
|
|
186
|
+
insertDefinitionsAndExports(db, allSymbols);
|
|
187
|
+
insertChildren(db, allSymbols);
|
|
188
|
+
insertContainmentEdges(db, allSymbols);
|
|
189
|
+
updateFileHashes(db, allSymbols, precomputedData, metadataUpdates, rootDir, upsertHash);
|
|
182
190
|
});
|
|
183
191
|
|
|
184
192
|
const t0 = performance.now();
|
|
@@ -57,10 +57,10 @@ export async function watchProject(rootDir, opts = {}) {
|
|
|
57
57
|
countNodes: db.prepare('SELECT COUNT(*) as c FROM nodes WHERE file = ?'),
|
|
58
58
|
countEdgesForFile: null,
|
|
59
59
|
findNodeInFile: db.prepare(
|
|
60
|
-
"SELECT id, file FROM nodes WHERE name = ? AND kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module') AND file = ?",
|
|
60
|
+
"SELECT id, file FROM nodes WHERE name = ? AND kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module', 'constant') AND file = ?",
|
|
61
61
|
),
|
|
62
62
|
findNodeByName: db.prepare(
|
|
63
|
-
"SELECT id, file FROM nodes WHERE name = ? AND kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module')",
|
|
63
|
+
"SELECT id, file FROM nodes WHERE name = ? AND kind IN ('function', 'method', 'class', 'interface', 'type', 'struct', 'enum', 'trait', 'record', 'module', 'constant')",
|
|
64
64
|
),
|
|
65
65
|
listSymbols: db.prepare("SELECT name, kind, line FROM nodes WHERE file = ? AND kind != 'file'"),
|
|
66
66
|
};
|
package/src/domain/parser.js
CHANGED
|
@@ -2,7 +2,7 @@ import fs from 'node:fs';
|
|
|
2
2
|
import path from 'node:path';
|
|
3
3
|
import { fileURLToPath } from 'node:url';
|
|
4
4
|
import { Language, Parser, Query } from 'web-tree-sitter';
|
|
5
|
-
import { warn } from '../infrastructure/logger.js';
|
|
5
|
+
import { debug, warn } from '../infrastructure/logger.js';
|
|
6
6
|
import { getNative, getNativePackageVersion, loadNative } from '../infrastructure/native.js';
|
|
7
7
|
|
|
8
8
|
// Re-export all extractors for backward compatibility
|
|
@@ -116,29 +116,35 @@ export async function createParsers() {
|
|
|
116
116
|
*/
|
|
117
117
|
export function disposeParsers() {
|
|
118
118
|
if (_cachedParsers) {
|
|
119
|
-
for (const [, parser] of _cachedParsers) {
|
|
119
|
+
for (const [id, parser] of _cachedParsers) {
|
|
120
120
|
if (parser && typeof parser.delete === 'function') {
|
|
121
121
|
try {
|
|
122
122
|
parser.delete();
|
|
123
|
-
} catch {
|
|
123
|
+
} catch (e) {
|
|
124
|
+
debug(`Failed to dispose parser ${id}: ${e.message}`);
|
|
125
|
+
}
|
|
124
126
|
}
|
|
125
127
|
}
|
|
126
128
|
_cachedParsers = null;
|
|
127
129
|
}
|
|
128
|
-
for (const [, query] of _queryCache) {
|
|
130
|
+
for (const [id, query] of _queryCache) {
|
|
129
131
|
if (query && typeof query.delete === 'function') {
|
|
130
132
|
try {
|
|
131
133
|
query.delete();
|
|
132
|
-
} catch {
|
|
134
|
+
} catch (e) {
|
|
135
|
+
debug(`Failed to dispose query ${id}: ${e.message}`);
|
|
136
|
+
}
|
|
133
137
|
}
|
|
134
138
|
}
|
|
135
139
|
_queryCache.clear();
|
|
136
140
|
if (_cachedLanguages) {
|
|
137
|
-
for (const [, lang] of _cachedLanguages) {
|
|
141
|
+
for (const [id, lang] of _cachedLanguages) {
|
|
138
142
|
if (lang && typeof lang.delete === 'function') {
|
|
139
143
|
try {
|
|
140
144
|
lang.delete();
|
|
141
|
-
} catch {
|
|
145
|
+
} catch (e) {
|
|
146
|
+
debug(`Failed to dispose language ${id}: ${e.message}`);
|
|
147
|
+
}
|
|
142
148
|
}
|
|
143
149
|
}
|
|
144
150
|
_cachedLanguages = null;
|
|
@@ -189,14 +195,15 @@ export async function ensureWasmTrees(fileSymbols, rootDir) {
|
|
|
189
195
|
let code;
|
|
190
196
|
try {
|
|
191
197
|
code = fs.readFileSync(absPath, 'utf-8');
|
|
192
|
-
} catch {
|
|
198
|
+
} catch (e) {
|
|
199
|
+
debug(`ensureWasmTrees: cannot read ${relPath}: ${e.message}`);
|
|
193
200
|
continue;
|
|
194
201
|
}
|
|
195
202
|
try {
|
|
196
203
|
symbols._tree = parser.parse(code);
|
|
197
204
|
symbols._langId = entry.id;
|
|
198
|
-
} catch {
|
|
199
|
-
|
|
205
|
+
} catch (e) {
|
|
206
|
+
debug(`ensureWasmTrees: parse failed for ${relPath}: ${e.message}`);
|
|
200
207
|
}
|
|
201
208
|
}
|
|
202
209
|
}
|
|
@@ -483,7 +490,9 @@ export function getActiveEngine(opts = {}) {
|
|
|
483
490
|
if (native) {
|
|
484
491
|
try {
|
|
485
492
|
version = getNativePackageVersion() ?? version;
|
|
486
|
-
} catch {
|
|
493
|
+
} catch (e) {
|
|
494
|
+
debug(`getNativePackageVersion failed: ${e.message}`);
|
|
495
|
+
}
|
|
487
496
|
}
|
|
488
497
|
return { name, version };
|
|
489
498
|
}
|
package/src/domain/queries.js
CHANGED
|
@@ -22,6 +22,7 @@ export {
|
|
|
22
22
|
} from '../shared/kinds.js';
|
|
23
23
|
// ── Shared utilities ─────────────────────────────────────────────────────
|
|
24
24
|
export { kindIcon, normalizeSymbol } from '../shared/normalize.js';
|
|
25
|
+
export { briefData } from './analysis/brief.js';
|
|
25
26
|
export { contextData, explainData } from './analysis/context.js';
|
|
26
27
|
export { fileDepsData, fnDepsData, pathData } from './analysis/dependencies.js';
|
|
27
28
|
export { exportsData } from './analysis/exports.js';
|
|
@@ -29,15 +29,19 @@ const TEST_PATTERN = /\.(test|spec)\.|__test__|__tests__|\.stories\./;
|
|
|
29
29
|
* @param {object} opts
|
|
30
30
|
* @param {string} [opts.filePattern] - Glob pattern (only applied if it contains glob chars)
|
|
31
31
|
* @param {boolean} [opts.noTests] - Exclude test/spec files
|
|
32
|
-
* @param {boolean} [opts.isGlob] - Pre-computed: does filePattern contain glob chars?
|
|
33
32
|
* @returns {Array}
|
|
34
33
|
*/
|
|
35
34
|
export function applyFilters(rows, opts = {}) {
|
|
36
35
|
let filtered = rows;
|
|
37
|
-
const
|
|
38
|
-
|
|
39
|
-
if (
|
|
40
|
-
filtered = filtered.filter((row) =>
|
|
36
|
+
const fp = opts.filePattern;
|
|
37
|
+
const fpArr = Array.isArray(fp) ? fp : fp ? [fp] : [];
|
|
38
|
+
if (fpArr.length > 0) {
|
|
39
|
+
filtered = filtered.filter((row) =>
|
|
40
|
+
fpArr.some((p) => {
|
|
41
|
+
const patternIsGlob = /[*?[\]]/.test(p);
|
|
42
|
+
return patternIsGlob ? globMatch(row.file, p) : row.file.includes(p);
|
|
43
|
+
}),
|
|
44
|
+
);
|
|
41
45
|
}
|
|
42
46
|
if (opts.noTests) {
|
|
43
47
|
filtered = filtered.filter((row) => !TEST_PATTERN.test(row.file));
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { openReadonlyOrFail } from '../../../db/index.js';
|
|
2
|
+
import { buildFileConditionSQL } from '../../../db/query-builder.js';
|
|
2
3
|
import { normalizeSymbol } from '../../queries.js';
|
|
3
4
|
import { hasFtsIndex, sanitizeFtsQuery } from '../stores/fts5.js';
|
|
4
5
|
import { applyFilters } from './filters.js';
|
|
@@ -36,10 +37,16 @@ export function ftsSearchData(query, customDbPath, opts = {}) {
|
|
|
36
37
|
params.push(opts.kind);
|
|
37
38
|
}
|
|
38
39
|
|
|
39
|
-
const
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
40
|
+
const fp = opts.filePattern;
|
|
41
|
+
const fpArr = Array.isArray(fp) ? fp : fp ? [fp] : [];
|
|
42
|
+
const isGlob = fpArr.length > 0 && fpArr.some((p) => /[*?[\]]/.test(p));
|
|
43
|
+
// For non-glob patterns, push filtering into SQL via buildFileConditionSQL
|
|
44
|
+
// (handles escapeLike + ESCAPE clause). Glob patterns are handled post-query
|
|
45
|
+
// by applyFilters.
|
|
46
|
+
if (fpArr.length > 0 && !isGlob) {
|
|
47
|
+
const fc = buildFileConditionSQL(fpArr, 'n.file');
|
|
48
|
+
sql += fc.sql;
|
|
49
|
+
params.push(...fc.params);
|
|
43
50
|
}
|
|
44
51
|
|
|
45
52
|
sql += ' ORDER BY rank LIMIT ?';
|
|
@@ -53,7 +60,7 @@ export function ftsSearchData(query, customDbPath, opts = {}) {
|
|
|
53
60
|
return { results: [] };
|
|
54
61
|
}
|
|
55
62
|
|
|
56
|
-
rows = applyFilters(rows,
|
|
63
|
+
rows = applyFilters(rows, opts);
|
|
57
64
|
|
|
58
65
|
const hc = new Map();
|
|
59
66
|
const results = rows.slice(0, limit).map((row) => ({
|
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
import { openReadonlyOrFail } from '../../../db/index.js';
|
|
2
|
+
import { escapeLike } from '../../../db/query-builder.js';
|
|
2
3
|
import { getEmbeddingCount, getEmbeddingMeta } from '../../../db/repository/embeddings.js';
|
|
3
4
|
import { MODELS } from '../models.js';
|
|
4
5
|
import { applyFilters } from './filters.js';
|
|
@@ -35,7 +36,9 @@ export function prepareSearch(customDbPath, opts = {}) {
|
|
|
35
36
|
}
|
|
36
37
|
|
|
37
38
|
// Pre-filter: allow filtering by kind or file pattern to reduce search space
|
|
38
|
-
const
|
|
39
|
+
const fp = opts.filePattern;
|
|
40
|
+
const fpArr = Array.isArray(fp) ? fp : fp ? [fp] : [];
|
|
41
|
+
const isGlob = fpArr.length > 0 && fpArr.some((p) => /[*?[\]]/.test(p));
|
|
39
42
|
let sql = `
|
|
40
43
|
SELECT e.node_id, e.vector, e.text_preview, n.name, n.kind, n.file, n.line, n.end_line, n.role
|
|
41
44
|
FROM embeddings e
|
|
@@ -47,16 +50,21 @@ export function prepareSearch(customDbPath, opts = {}) {
|
|
|
47
50
|
conditions.push('n.kind = ?');
|
|
48
51
|
params.push(opts.kind);
|
|
49
52
|
}
|
|
50
|
-
if (
|
|
51
|
-
|
|
52
|
-
|
|
53
|
+
if (fpArr.length > 0 && !isGlob) {
|
|
54
|
+
if (fpArr.length === 1) {
|
|
55
|
+
conditions.push("n.file LIKE ? ESCAPE '\\'");
|
|
56
|
+
params.push(`%${escapeLike(fpArr[0])}%`);
|
|
57
|
+
} else {
|
|
58
|
+
conditions.push(`(${fpArr.map(() => "n.file LIKE ? ESCAPE '\\'").join(' OR ')})`);
|
|
59
|
+
params.push(...fpArr.map((f) => `%${escapeLike(f)}%`));
|
|
60
|
+
}
|
|
53
61
|
}
|
|
54
62
|
if (conditions.length > 0) {
|
|
55
63
|
sql += ` WHERE ${conditions.join(' AND ')}`;
|
|
56
64
|
}
|
|
57
65
|
|
|
58
66
|
let rows = db.prepare(sql).all(...params);
|
|
59
|
-
rows = applyFilters(rows,
|
|
67
|
+
rows = applyFilters(rows, opts);
|
|
60
68
|
|
|
61
69
|
return { db, rows, modelKey, storedDim };
|
|
62
70
|
} catch (err) {
|