graph-indexer 2.1.1 → 3.0.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/CHANGELOG.md +260 -1
- package/README.md +232 -312
- package/SECURITY.md +37 -59
- package/bin/graph-indexer.mjs +251 -0
- package/integrations/claude-code/.claude-plugin/plugin.json +8 -0
- package/integrations/claude-code/.mcp.json +8 -0
- package/integrations/claude-code/agents/code-structure.md +22 -0
- package/integrations/claude-code/hooks/gi-hook.mjs +99 -0
- package/integrations/claude-code/hooks/hooks.json +16 -0
- package/integrations/opencode/graph-indexer.js +124 -0
- package/package.json +32 -109
- package/src/cli/helper.mjs +67 -0
- package/src/cli/hook.mjs +356 -0
- package/src/cli/init.mjs +255 -0
- package/src/cli/resident.mjs +175 -0
- package/src/index/discover.mjs +130 -0
- package/src/index/fingerprint.mjs +30 -0
- package/src/index/graph.mjs +61 -0
- package/src/index/indexer.mjs +476 -0
- package/src/index/modules.mjs +331 -0
- package/src/index/resolver.mjs +954 -0
- package/src/mcp/render.mjs +60 -0
- package/src/mcp/server.mjs +147 -0
- package/src/mcp/tools.mjs +973 -0
- package/src/parse/arity.mjs +152 -0
- package/src/parse/extract.mjs +1031 -0
- package/src/parse/lang/csharp.mjs +106 -0
- package/src/parse/lang/ecma.mjs +438 -0
- package/src/parse/lang/go.mjs +194 -0
- package/src/parse/lang/jvm.mjs +118 -0
- package/src/parse/lang/kotlin.mjs +97 -0
- package/src/parse/lang/misc.mjs +115 -0
- package/src/parse/lang/native.mjs +126 -0
- package/src/parse/lang/python.mjs +153 -0
- package/src/parse/lang/rust.mjs +157 -0
- package/src/parse/lang/scripting.mjs +219 -0
- package/src/parse/languages.mjs +48 -0
- package/src/parse/runtime.mjs +56 -0
- package/src/query/check.mjs +343 -0
- package/src/query/facts.mjs +323 -0
- package/src/query/intel.mjs +680 -0
- package/src/query/read.mjs +81 -0
- package/src/query/testcmd.mjs +119 -0
- package/src/query/textsearch.mjs +160 -0
- package/src/search/search.mjs +210 -0
- package/src/search/tokenize.mjs +188 -0
- package/src/store/db.mjs +169 -0
- package/src/util/paths.mjs +24 -0
- package/vendor/grammars/bash.wasm.gz +0 -0
- package/vendor/grammars/c.wasm.gz +0 -0
- package/vendor/grammars/c_sharp.wasm.gz +0 -0
- package/vendor/grammars/cpp.wasm.gz +0 -0
- package/vendor/grammars/css.wasm.gz +0 -0
- package/vendor/grammars/go.wasm.gz +0 -0
- package/vendor/grammars/java.wasm.gz +0 -0
- package/vendor/grammars/javascript.wasm.gz +0 -0
- package/vendor/grammars/kotlin.wasm.gz +0 -0
- package/vendor/grammars/manifest.json +148 -0
- package/vendor/grammars/php.wasm.gz +0 -0
- package/vendor/grammars/python.wasm.gz +0 -0
- package/vendor/grammars/ruby.wasm.gz +0 -0
- package/vendor/grammars/rust.wasm.gz +0 -0
- package/vendor/grammars/scala.wasm.gz +0 -0
- package/vendor/grammars/tsx.wasm.gz +0 -0
- package/vendor/grammars/typescript.wasm.gz +0 -0
- package/vendor/web-tree-sitter/web-tree-sitter.js +3980 -0
- package/vendor/web-tree-sitter/web-tree-sitter.wasm +0 -0
- package/cli-ui.mjs +0 -78
- package/colbert.mjs +0 -168
- package/config.mjs +0 -494
- package/daemon-ctl.mjs +0 -175
- package/daemon-lock.mjs +0 -69
- package/embedders/python/mlx_embed_server.py +0 -61
- package/embedders/python/requirements-mlx.txt +0 -5
- package/embedders/setup-mlx.mjs +0 -110
- package/embeddings.mjs +0 -571
- package/engine/binary.mjs +0 -437
- package/engine/memory.mjs +0 -1003
- package/engine/sqlite.mjs +0 -1136
- package/enrichment.mjs +0 -521
- package/git-signals.mjs +0 -248
- package/indexer.mjs +0 -546
- package/init.mjs +0 -1809
- package/layout.mjs +0 -123
- package/mcp/centrality.mjs +0 -117
- package/mcp/format.mjs +0 -61
- package/mcp/resolver.mjs +0 -116
- package/mcp/symbolgraph.mjs +0 -137
- package/mcp/taint.mjs +0 -225
- package/mcp/tools.mjs +0 -1582
- package/mcp/topology.mjs +0 -676
- package/mcp-server.mjs +0 -197
- package/parse/extractor.mjs +0 -628
- package/parse/imports.mjs +0 -365
- package/parse/interprocedural.mjs +0 -134
- package/parse/languages.mjs +0 -253
- package/parse/metadata.mjs +0 -701
- package/parse/routes.mjs +0 -327
- package/parse/scip.mjs +0 -391
- package/parse/taint-patterns.mjs +0 -119
- package/prompts/CORE.md +0 -91
- package/prompts/DOMAIN_TEMPLATE.md +0 -46
- package/prompts/INTEGRATION.md +0 -123
- package/prompts/frameworks/ANDROID.md +0 -17
- package/prompts/frameworks/ASPNET_CORE.md +0 -19
- package/prompts/frameworks/FASTAPI_DJANGO.md +0 -20
- package/prompts/frameworks/LARAVEL_SYMFONY.md +0 -22
- package/prompts/frameworks/NODE_EXPRESS_NESTJS.md +0 -19
- package/prompts/frameworks/RAILS.md +0 -20
- package/prompts/frameworks/REACT.md +0 -20
- package/prompts/frameworks/SPRING_BOOT.md +0 -20
- package/prompts/languages/BASH.md +0 -22
- package/prompts/languages/C.md +0 -21
- package/prompts/languages/CSHARP.md +0 -24
- package/prompts/languages/CSS_SCSS.md +0 -22
- package/prompts/languages/GO.md +0 -24
- package/prompts/languages/JAVA.md +0 -23
- package/prompts/languages/JAVASCRIPT_TYPESCRIPT.md +0 -24
- package/prompts/languages/KOTLIN.md +0 -24
- package/prompts/languages/PHP.md +0 -23
- package/prompts/languages/PYTHON.md +0 -23
- package/prompts/languages/RUBY.md +0 -23
- package/prompts/languages/RUST.md +0 -21
- package/prompts/languages/SWIFT.md +0 -21
- package/seal.mjs +0 -400
- package/search-core.mjs +0 -777
- package/search-sparse.mjs +0 -183
- package/storage.mjs +0 -73
- package/watch-daemon.mjs +0 -289
package/parse/imports.mjs
DELETED
|
@@ -1,365 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file parse/imports.mjs
|
|
3
|
-
* @description Import resolution, barrel export expansion, embedding payload
|
|
4
|
-
* builders, and legacy Ollama embedding helpers.
|
|
5
|
-
* @author MaquinaTech <https://github.com/MaquinaTech>
|
|
6
|
-
* @copyright (c) 2026 MaquinaTech. All rights reserved.
|
|
7
|
-
* @license MIT
|
|
8
|
-
*/
|
|
9
|
-
import fs from 'fs';
|
|
10
|
-
import path from 'path';
|
|
11
|
-
import { getParserForFile, EXTENSIONS } from './languages.mjs';
|
|
12
|
-
import { truncateForEmbedding } from '../search-core.mjs';
|
|
13
|
-
|
|
14
|
-
// JS-family import-specifier extensions. Under TS node16/nodenext these appear in
|
|
15
|
-
// specifiers (`./x.js`) while the source on disk is `.ts`/`.tsx` — so when the literal
|
|
16
|
-
// path is missing we strip one of these and re-probe the real source extensions.
|
|
17
|
-
const JS_FAMILY_EXT = new Set(['.js', '.jsx', '.mjs', '.cjs']);
|
|
18
|
-
|
|
19
|
-
// ─── Ollama host / model resolution ──────────────────────────────────────────
|
|
20
|
-
// Priority: caller override → OLLAMA_HOST env var → PROJECT .graph-indexer.json
|
|
21
|
-
// (MCP_PROJECT_ROOT or cwd — NOT the package directory: when graph-indexer is
|
|
22
|
-
// installed as a dependency, the user's config lives in their project root) →
|
|
23
|
-
// default. Mirrors config.mjs precedence so every entry point agrees.
|
|
24
|
-
//
|
|
25
|
-
// OLLAMA_HOST in the shell is Ollama's binding address (e.g. "0.0.0.0:11434"), not an
|
|
26
|
-
// HTTP client URL. We normalise bare "host:port" strings by adding http:// and translating
|
|
27
|
-
// 0.0.0.0 → localhost so fetches work in both formats.
|
|
28
|
-
|
|
29
|
-
let _cachedHost = null;
|
|
30
|
-
let _cachedEmbedModel = null;
|
|
31
|
-
function _normalizeOllamaHost(raw) {
|
|
32
|
-
if (!raw) return null;
|
|
33
|
-
if (raw.startsWith('http://') || raw.startsWith('https://')) return raw;
|
|
34
|
-
return 'http://' + raw.replace(/^0\.0\.0\.0/, 'localhost');
|
|
35
|
-
}
|
|
36
|
-
function _readProjectConfig() {
|
|
37
|
-
const root = process.env.MCP_PROJECT_ROOT || process.cwd();
|
|
38
|
-
try {
|
|
39
|
-
return JSON.parse(fs.readFileSync(path.join(root, '.graph-indexer.json'), 'utf8'));
|
|
40
|
-
} catch { return null; }
|
|
41
|
-
}
|
|
42
|
-
function _resolveOllamaHost(override) {
|
|
43
|
-
if (override) return _normalizeOllamaHost(override) || 'http://localhost:11434';
|
|
44
|
-
if (_cachedHost) return _cachedHost;
|
|
45
|
-
if (process.env.OLLAMA_HOST) {
|
|
46
|
-
_cachedHost = _normalizeOllamaHost(process.env.OLLAMA_HOST) || 'http://localhost:11434';
|
|
47
|
-
return _cachedHost;
|
|
48
|
-
}
|
|
49
|
-
const cfg = _readProjectConfig();
|
|
50
|
-
_cachedHost = _normalizeOllamaHost(cfg?.ollamaHost) || 'http://localhost:11434';
|
|
51
|
-
return _cachedHost;
|
|
52
|
-
}
|
|
53
|
-
function _resolveEmbedModel(override) {
|
|
54
|
-
if (override) return override;
|
|
55
|
-
if (_cachedEmbedModel) return _cachedEmbedModel;
|
|
56
|
-
const cfg = _readProjectConfig();
|
|
57
|
-
_cachedEmbedModel = cfg?.embedModel || 'nomic-embed-text';
|
|
58
|
-
return _cachedEmbedModel;
|
|
59
|
-
}
|
|
60
|
-
|
|
61
|
-
// ─── Barrel export resolution ─────────────────────────────────────────────────
|
|
62
|
-
|
|
63
|
-
// Module-level cache: barrelAbsPath → Map<exportedName, sourceRelPath>
|
|
64
|
-
const _barrelCache = new Map();
|
|
65
|
-
|
|
66
|
-
/**
|
|
67
|
-
* Parses a barrel file (index.ts / index.js) and returns a map of
|
|
68
|
-
* exportedName → sourceFilePath (relative to projectRoot).
|
|
69
|
-
* e.g. { useAuthStore: 'src/stores/authStore.ts' }
|
|
70
|
-
*/
|
|
71
|
-
export function resolveBarrelExports(barrelAbsPath, projectRoot) {
|
|
72
|
-
if (_barrelCache.has(barrelAbsPath)) return _barrelCache.get(barrelAbsPath);
|
|
73
|
-
|
|
74
|
-
const result = new Map();
|
|
75
|
-
_barrelCache.set(barrelAbsPath, result);
|
|
76
|
-
|
|
77
|
-
const ext = path.extname(barrelAbsPath);
|
|
78
|
-
if (!['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(ext)) return result;
|
|
79
|
-
|
|
80
|
-
let content;
|
|
81
|
-
try { content = fs.readFileSync(barrelAbsPath, 'utf-8'); } catch { return result; }
|
|
82
|
-
|
|
83
|
-
const parser = getParserForFile(ext);
|
|
84
|
-
if (!parser) return result;
|
|
85
|
-
|
|
86
|
-
let tree;
|
|
87
|
-
try { tree = parser.parse((offset) => offset < content.length ? content.slice(offset, offset + 4096) : null); } catch { return result; }
|
|
88
|
-
|
|
89
|
-
const barrelDir = path.dirname(barrelAbsPath);
|
|
90
|
-
|
|
91
|
-
function walk(node) {
|
|
92
|
-
if (node.type === 'export_statement') {
|
|
93
|
-
const fromNode = node.children.find(c => c.type === 'string');
|
|
94
|
-
if (!fromNode) { node.children.forEach(walk); return; }
|
|
95
|
-
|
|
96
|
-
const rawSource = fromNode.text.replace(/['"]/g, '');
|
|
97
|
-
if (!rawSource.startsWith('.')) { node.children.forEach(walk); return; }
|
|
98
|
-
|
|
99
|
-
const absSource = path.resolve(barrelDir, rawSource);
|
|
100
|
-
let finalAbs = null;
|
|
101
|
-
if (EXTENSIONS.has(path.extname(absSource)) && fs.existsSync(absSource)) {
|
|
102
|
-
finalAbs = absSource;
|
|
103
|
-
} else {
|
|
104
|
-
for (const e of EXTENSIONS) {
|
|
105
|
-
if (fs.existsSync(absSource + e)) { finalAbs = absSource + e; break; }
|
|
106
|
-
const idx = path.join(absSource, 'index' + e);
|
|
107
|
-
if (fs.existsSync(idx)) { finalAbs = idx; break; }
|
|
108
|
-
}
|
|
109
|
-
}
|
|
110
|
-
if (!finalAbs) { node.children.forEach(walk); return; }
|
|
111
|
-
|
|
112
|
-
const relSource = path.relative(projectRoot, finalAbs).replace(/\\/g, '/');
|
|
113
|
-
|
|
114
|
-
const namedExports = node.children.find(c => c.type === 'named_imports' || c.type === 'export_clause');
|
|
115
|
-
if (namedExports) {
|
|
116
|
-
for (const child of namedExports.children) {
|
|
117
|
-
if (child.type === 'import_specifier' || child.type === 'export_specifier') {
|
|
118
|
-
const names = child.children.filter(c => c.type === 'identifier');
|
|
119
|
-
if (names.length > 0) {
|
|
120
|
-
result.set(names[names.length - 1].text, relSource);
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
}
|
|
124
|
-
}
|
|
125
|
-
|
|
126
|
-
const starNode = node.children.find(c => c.text === '*');
|
|
127
|
-
if (starNode) {
|
|
128
|
-
result.set('*', relSource);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
node.children.forEach(walk);
|
|
132
|
-
}
|
|
133
|
-
walk(tree.rootNode);
|
|
134
|
-
return result;
|
|
135
|
-
}
|
|
136
|
-
|
|
137
|
-
// ─── Go module-name cache (reads go.mod once per project root) ──────────────
|
|
138
|
-
const _goModCache = new Map();
|
|
139
|
-
function _readGoModuleName(projectRoot) {
|
|
140
|
-
if (_goModCache.has(projectRoot)) return _goModCache.get(projectRoot);
|
|
141
|
-
const modFile = path.join(projectRoot, 'go.mod');
|
|
142
|
-
let name = null;
|
|
143
|
-
if (fs.existsSync(modFile)) {
|
|
144
|
-
try {
|
|
145
|
-
const first = fs.readFileSync(modFile, 'utf-8').split('\n').find(l => l.trimStart().startsWith('module '));
|
|
146
|
-
if (first) name = first.trim().replace(/^module\s+/, '').split(/\s/)[0];
|
|
147
|
-
} catch { /* ignore */ }
|
|
148
|
-
}
|
|
149
|
-
_goModCache.set(projectRoot, name);
|
|
150
|
-
return name;
|
|
151
|
-
}
|
|
152
|
-
|
|
153
|
-
export function resolveLocalImports(rawImports, fromFileRelPath, projectRoot) {
|
|
154
|
-
const fileDir = path.dirname(path.join(projectRoot, fromFileRelPath));
|
|
155
|
-
const ext = path.extname(fromFileRelPath);
|
|
156
|
-
const resolved = [];
|
|
157
|
-
for (const raw of rawImports) {
|
|
158
|
-
if (raw.startsWith('.')) {
|
|
159
|
-
const absResolved = path.resolve(fileDir, raw);
|
|
160
|
-
const existingExt = path.extname(absResolved);
|
|
161
|
-
let finalAbs = null;
|
|
162
|
-
if (existingExt && EXTENSIONS.has(existingExt) && fs.existsSync(absResolved)) {
|
|
163
|
-
finalAbs = absResolved;
|
|
164
|
-
} else {
|
|
165
|
-
// TypeScript ESM convention (node16/nodenext moduleResolution): the import
|
|
166
|
-
// specifier carries a JS-family extension (`./Fade.js`) but the source file on
|
|
167
|
-
// disk is `.ts`/`.tsx`. The literal path doesn't exist, so strip that extension
|
|
168
|
-
// and re-probe the candidate source extensions — otherwise EVERY relative import
|
|
169
|
-
// in such a project drops and the file gets empty Deps:/Used by: topology.
|
|
170
|
-
const probeBase = (existingExt && JS_FAMILY_EXT.has(existingExt))
|
|
171
|
-
? absResolved.slice(0, -existingExt.length)
|
|
172
|
-
: absResolved;
|
|
173
|
-
for (const e of EXTENSIONS) {
|
|
174
|
-
if (fs.existsSync(probeBase + e)) { finalAbs = probeBase + e; break; }
|
|
175
|
-
const idx = path.join(probeBase, 'index' + e);
|
|
176
|
-
if (fs.existsSync(idx)) { finalAbs = idx; break; }
|
|
177
|
-
// Python: also try __init__.py for package directories
|
|
178
|
-
if (e === '.py') {
|
|
179
|
-
const init = path.join(probeBase, '__init__.py');
|
|
180
|
-
if (fs.existsSync(init)) { finalAbs = init; break; }
|
|
181
|
-
}
|
|
182
|
-
}
|
|
183
|
-
}
|
|
184
|
-
if (finalAbs) {
|
|
185
|
-
const relPath = path.relative(projectRoot, finalAbs).replace(/\\/g, '/');
|
|
186
|
-
const baseName = path.basename(finalAbs, path.extname(finalAbs));
|
|
187
|
-
if (baseName === 'index' && ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'].includes(path.extname(finalAbs))) {
|
|
188
|
-
const barrelMap = resolveBarrelExports(finalAbs, projectRoot);
|
|
189
|
-
if (barrelMap.size > 0) {
|
|
190
|
-
const sources = new Set(barrelMap.values());
|
|
191
|
-
for (const src of sources) {
|
|
192
|
-
if (!resolved.includes(src)) resolved.push(src);
|
|
193
|
-
}
|
|
194
|
-
} else {
|
|
195
|
-
if (!resolved.includes(relPath)) resolved.push(relPath);
|
|
196
|
-
}
|
|
197
|
-
} else {
|
|
198
|
-
if (!resolved.includes(relPath)) resolved.push(relPath);
|
|
199
|
-
}
|
|
200
|
-
}
|
|
201
|
-
}
|
|
202
|
-
// ── Go intra-module imports: module-prefix paths mapped to local dirs ──
|
|
203
|
-
else if (ext === '.go') {
|
|
204
|
-
const modName = _readGoModuleName(projectRoot);
|
|
205
|
-
if (modName && raw.startsWith(modName + '/')) {
|
|
206
|
-
const subPkg = raw.slice(modName.length + 1);
|
|
207
|
-
const absDir = path.join(projectRoot, subPkg);
|
|
208
|
-
if (fs.existsSync(absDir)) {
|
|
209
|
-
try {
|
|
210
|
-
const goFiles = fs.readdirSync(absDir)
|
|
211
|
-
.filter(f => f.endsWith('.go') && !f.includes('_test'))
|
|
212
|
-
.slice(0, 5); // cap to avoid fanning out too many deps per package
|
|
213
|
-
for (const gof of goFiles) {
|
|
214
|
-
const rel = path.relative(projectRoot, path.join(absDir, gof)).replace(/\\/g, '/');
|
|
215
|
-
if (!resolved.includes(rel)) resolved.push(rel);
|
|
216
|
-
}
|
|
217
|
-
} catch { /* directory unreadable — skip */ }
|
|
218
|
-
}
|
|
219
|
-
}
|
|
220
|
-
}
|
|
221
|
-
// ── Rust crate-local: crate::module::item → src/module.rs ──────────
|
|
222
|
-
else if (ext === '.rs' && raw.startsWith('crate::')) {
|
|
223
|
-
const parts = raw.slice('crate::'.length).split('::').filter(Boolean);
|
|
224
|
-
for (let depth = parts.length; depth >= 1; depth--) {
|
|
225
|
-
const subPath = parts.slice(0, depth).join('/');
|
|
226
|
-
const candidates = [
|
|
227
|
-
path.join(projectRoot, 'src', subPath + '.rs'),
|
|
228
|
-
path.join(projectRoot, 'src', subPath, 'mod.rs'),
|
|
229
|
-
];
|
|
230
|
-
let found = false;
|
|
231
|
-
for (const c of candidates) {
|
|
232
|
-
if (fs.existsSync(c)) {
|
|
233
|
-
resolved.push(path.relative(projectRoot, c).replace(/\\/g, '/'));
|
|
234
|
-
found = true; break;
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
if (found) break;
|
|
238
|
-
}
|
|
239
|
-
}
|
|
240
|
-
// ── C / Bash non-dotted relative includes ──────────────────────────
|
|
241
|
-
// C `#include "net/socket.h"` and Bash `source lib/util.sh` are resolved
|
|
242
|
-
// relative to the including file's directory, then to the project root.
|
|
243
|
-
else if ((ext === '.c' || ext === '.h' || ext === '.sh' || ext === '.bash')) {
|
|
244
|
-
for (const abs of [path.resolve(fileDir, raw), path.join(projectRoot, raw)]) {
|
|
245
|
-
if (EXTENSIONS.has(path.extname(abs)) && fs.existsSync(abs)) {
|
|
246
|
-
const rel = path.relative(projectRoot, abs).replace(/\\/g, '/');
|
|
247
|
-
if (!rel.startsWith('..') && !resolved.includes(rel)) resolved.push(rel);
|
|
248
|
-
break;
|
|
249
|
-
}
|
|
250
|
-
}
|
|
251
|
-
}
|
|
252
|
-
}
|
|
253
|
-
return resolved;
|
|
254
|
-
}
|
|
255
|
-
|
|
256
|
-
/**
|
|
257
|
-
* Build the text payload sent to the embedding model for a chunk.
|
|
258
|
-
*
|
|
259
|
-
* Shared by the bootstrap indexer (indexer.mjs) and the watch daemon
|
|
260
|
-
* (watch-daemon.mjs) so a chunk yields the SAME embedding regardless of which
|
|
261
|
-
* path embedded it first. This matters because the cache key (content_hash) is
|
|
262
|
-
* derived from code + docstring only — it does NOT include this payload — so two
|
|
263
|
-
* divergent payloads for the same hash would silently produce inconsistent
|
|
264
|
-
* embeddings across a full re-index vs. an incremental update.
|
|
265
|
-
*
|
|
266
|
-
* @param {object} chunk Semantic chunk: { file_path, node_type, name, docstring, type_refs, code_snippet }.
|
|
267
|
-
* @param {string[]} depRelPaths Resolved local imports of the chunk's file (project-relative paths).
|
|
268
|
-
* @returns {string}
|
|
269
|
-
*/
|
|
270
|
-
export function buildEmbeddingPayload(chunk, depRelPaths = [], bodyOverride = null) {
|
|
271
|
-
const neighbors = depRelPaths
|
|
272
|
-
.map(d => path.basename(d, path.extname(d)))
|
|
273
|
-
.filter(Boolean);
|
|
274
|
-
const topologicalContext = neighbors.length
|
|
275
|
-
? `This code architectural neighborhood connects with: ${neighbors.join(', ')}.`
|
|
276
|
-
: '';
|
|
277
|
-
// Decorators and inheritance edges are NOT included here (A/B-tested: neutral
|
|
278
|
-
// on vector recall, regression on BM25 — kept as metadata only).
|
|
279
|
-
// LLM summary leads: declarative voice aligns with nomic's search_document training
|
|
280
|
-
// objective. hyde/questions are excluded — stopword noise dilutes the code fingerprint.
|
|
281
|
-
// Concept keywords (chunk.hyde) go to BM25 only via buildLexicalDocument.
|
|
282
|
-
return [
|
|
283
|
-
chunk.summary || '',
|
|
284
|
-
`File Location: ${chunk.file_path}`,
|
|
285
|
-
`Symbol Name: ${chunk.node_type} -> ${chunk.name}`,
|
|
286
|
-
chunk.docstring ? `Developer Documentation: ${chunk.docstring}` : '',
|
|
287
|
-
chunk.type_refs?.length ? `Type References: ${chunk.type_refs.join(', ')}` : '',
|
|
288
|
-
topologicalContext,
|
|
289
|
-
`--- Source Code ---`,
|
|
290
|
-
// bodyOverride carries the full source for oversized chunks (>3000 chars); BM25
|
|
291
|
-
// keeps using code_snippet so lexical and dense channels stay independently capped.
|
|
292
|
-
bodyOverride || chunk.code_snippet,
|
|
293
|
-
].filter(Boolean).join('\n');
|
|
294
|
-
}
|
|
295
|
-
|
|
296
|
-
/**
|
|
297
|
-
* The full source body of a chunk whose code_snippet was truncated by the 3000-char
|
|
298
|
-
* cap — sliced from the file content by line range — so the dense channel can window
|
|
299
|
-
* the WHOLE definition (the lexical/BM25 path stays capped, byte-identical). Returns
|
|
300
|
-
* null when the chunk is not truncated (snippet == body) or is a god-class skeleton
|
|
301
|
-
* (its methods are already their own chunks). Used at embed time by the indexer and
|
|
302
|
-
* the watch daemon, both of which hold the source content.
|
|
303
|
-
*
|
|
304
|
-
* @param {object} chunk A semantic chunk (needs code_snippet, start_line, end_line).
|
|
305
|
-
* @param {string} fileContent The chunk's full file source.
|
|
306
|
-
* @returns {string|null}
|
|
307
|
-
*/
|
|
308
|
-
export function fullBodyForEmbedding(chunk, fileContent) {
|
|
309
|
-
if (!chunk || !fileContent) return null;
|
|
310
|
-
if ((chunk.code_snippet?.length || 0) < 3000) return null; // not truncated
|
|
311
|
-
if (chunk.code_snippet.includes('Large class:')) return null; // god-class skeleton
|
|
312
|
-
if (!chunk.start_line || !chunk.end_line) return null;
|
|
313
|
-
const body = fileContent.split('\n').slice(chunk.start_line - 1, chunk.end_line).join('\n');
|
|
314
|
-
return body.length > chunk.code_snippet.length ? body : null;
|
|
315
|
-
}
|
|
316
|
-
|
|
317
|
-
export async function getLocalEmbedding(text, graceful = true, { ollamaHost, model } = {}) {
|
|
318
|
-
if (process.env.INDEXER_EMBEDDINGS === 'off') return null; // lexical-only mode
|
|
319
|
-
const host = _resolveOllamaHost(ollamaHost);
|
|
320
|
-
const embedModel = _resolveEmbedModel(model);
|
|
321
|
-
const MAX_RETRIES = 3;
|
|
322
|
-
const safeText = "search_query: " + truncateForEmbedding(text);
|
|
323
|
-
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
324
|
-
try {
|
|
325
|
-
const res = await fetch(`${host}/api/embeddings`, {
|
|
326
|
-
method: "POST", headers: { "Content-Type": "application/json" },
|
|
327
|
-
body: JSON.stringify({ model: embedModel, prompt: safeText }),
|
|
328
|
-
signal: AbortSignal.timeout(15000),
|
|
329
|
-
});
|
|
330
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
331
|
-
const data = await res.json();
|
|
332
|
-
return data.embedding;
|
|
333
|
-
} catch (err) {
|
|
334
|
-
if (attempt < MAX_RETRIES) await new Promise(r => setTimeout(r, 500 * 2 ** attempt));
|
|
335
|
-
else if (!graceful) throw err;
|
|
336
|
-
}
|
|
337
|
-
}
|
|
338
|
-
return null;
|
|
339
|
-
}
|
|
340
|
-
|
|
341
|
-
export async function getLocalEmbeddingsBatch(texts, graceful = true, { ollamaHost, model } = {}) {
|
|
342
|
-
if (!texts || texts.length === 0) return [];
|
|
343
|
-
if (process.env.INDEXER_EMBEDDINGS === 'off') return null; // lexical-only mode
|
|
344
|
-
const host = _resolveOllamaHost(ollamaHost);
|
|
345
|
-
const embedModel = _resolveEmbedModel(model);
|
|
346
|
-
const MAX_RETRIES = 3;
|
|
347
|
-
const safeTexts = texts.map(t => "search_document: " + (t.length > 8000 ? t.slice(0, 8000) : t));
|
|
348
|
-
|
|
349
|
-
for (let attempt = 0; attempt <= MAX_RETRIES; attempt++) {
|
|
350
|
-
try {
|
|
351
|
-
const res = await fetch(`${host}/api/embed`, {
|
|
352
|
-
method: "POST", headers: { "Content-Type": "application/json" },
|
|
353
|
-
body: JSON.stringify({ model: embedModel, input: safeTexts }),
|
|
354
|
-
signal: AbortSignal.timeout(60000),
|
|
355
|
-
});
|
|
356
|
-
if (!res.ok) throw new Error(`HTTP ${res.status}`);
|
|
357
|
-
const data = await res.json();
|
|
358
|
-
return data.embeddings;
|
|
359
|
-
} catch (err) {
|
|
360
|
-
if (attempt < MAX_RETRIES) await new Promise(r => setTimeout(r, 500 * 2 ** attempt));
|
|
361
|
-
else if (!graceful) throw err;
|
|
362
|
-
}
|
|
363
|
-
}
|
|
364
|
-
return null;
|
|
365
|
-
}
|
|
@@ -1,134 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file parse/interprocedural.mjs
|
|
3
|
-
* @description Bounded, deterministic INTER-procedural return-type fixpoint (opt-in).
|
|
4
|
-
*
|
|
5
|
-
* The intra-procedural inference in parse/metadata.mjs already resolves a
|
|
6
|
-
* call-site receiver one hop: `const r = makeRepo(); r.save()` records
|
|
7
|
-
* `recv_via_call = 'makeRepo'`, and classifyCallers reads makeRepo's recorded
|
|
8
|
-
* `return_type` at query time. That misses MULTI-hop factory chains and
|
|
9
|
-
* unannotated factories: `makeRepoIndirect()` returns `makeRepo()` returns
|
|
10
|
-
* `new OrderRepo()` — the indirect factory has no return type to read.
|
|
11
|
-
*
|
|
12
|
-
* This pass propagates return types along factory return-call edges with a
|
|
13
|
-
* monotone worklist fixpoint, so `makeRepoIndirect` resolves transitively to
|
|
14
|
-
* `OrderRepo`. It runs ONCE at index time over all chunks and writes the result
|
|
15
|
-
* into each call site as `recv_resolved_type`; because call_sites is serialized
|
|
16
|
-
* identically by both backends, the resolved data is parity-free. classifyCallers
|
|
17
|
-
* prefers `recv_resolved_type` and keeps the 1-hop `recv_via_call` fallback, so an
|
|
18
|
-
* index built without this pass (or a per-file daemon update) is never worse.
|
|
19
|
-
*
|
|
20
|
-
* Determinism: iteration is over SORTED symbol/edge keys and the lattice is
|
|
21
|
-
* monotone (each step only adds the single concrete type or transitions to an
|
|
22
|
-
* absorbing CONFLICT), so the result is independent of map insertion order — a
|
|
23
|
-
* hard requirement for memory↔sqlite parity.
|
|
24
|
-
* @author MaquinaTech <https://github.com/MaquinaTech>
|
|
25
|
-
* @copyright (c) 2026 MaquinaTech. All rights reserved.
|
|
26
|
-
* @license MIT
|
|
27
|
-
*/
|
|
28
|
-
|
|
29
|
-
// Hard safety net against pathological/cyclic graphs. Each iteration propagates types one
|
|
30
|
-
// extra hop; real factory chains are 1–3 hops, so the `changed` flag normally converges well
|
|
31
|
-
// before this — the bound only guarantees termination, it is not the expected stop condition.
|
|
32
|
-
const MAX_ITERS = 8;
|
|
33
|
-
|
|
34
|
-
// Absorbing "two distinct concrete types" state — contributes nothing (conservative DROP,
|
|
35
|
-
// mirroring _inferLocalBindings' conflict handling in parse/metadata.mjs).
|
|
36
|
-
const CONFLICT = Symbol('conflict');
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* The single concrete type NAME a string denotes, or null when it is not exactly one
|
|
40
|
-
* type identifier (generics / unions / primitives are ambiguous → conservatively dropped).
|
|
41
|
-
* 'OrderRepo' → 'OrderRepo'; 'Promise<Order>' → null; 'number' → null.
|
|
42
|
-
*/
|
|
43
|
-
function singleTypeName(str) {
|
|
44
|
-
if (!str) return null;
|
|
45
|
-
const ids = String(str).match(/[A-Za-z_$][A-Za-z0-9_$]*/g) || [];
|
|
46
|
-
const types = ids.filter(t => /^[A-Z]/.test(t)); // PascalCase = a type (matches _typeMatchTokens)
|
|
47
|
-
return types.length === 1 ? types[0] : null;
|
|
48
|
-
}
|
|
49
|
-
|
|
50
|
-
/**
|
|
51
|
-
* Resolve, per function symbol (lowercased name), the single concrete return TYPE it
|
|
52
|
-
* yields — directly (annotation or `return new X()`) or transitively through factory
|
|
53
|
-
* return-call edges. Symbols whose chain yields ≥2 distinct types (or stays unresolved)
|
|
54
|
-
* are omitted.
|
|
55
|
-
*
|
|
56
|
-
* @param {object[]} chunks Chunks carrying `name`, `return_type`, and the transient
|
|
57
|
-
* `_return_via` ({type}|{viaCall}) from extractReturnVia.
|
|
58
|
-
* @returns {Map<string,string>} nameLower → resolved type name.
|
|
59
|
-
*/
|
|
60
|
-
export function resolveReturnTypes(chunks) {
|
|
61
|
-
const symRet = new Map(); // nameLower → typeName | CONFLICT
|
|
62
|
-
const edges = new Map(); // nameLower → Set<calleeLower> (only for symbols with no direct type)
|
|
63
|
-
|
|
64
|
-
const seed = (nm, type) => {
|
|
65
|
-
const prev = symRet.get(nm);
|
|
66
|
-
if (prev === CONFLICT) return;
|
|
67
|
-
if (prev === undefined) symRet.set(nm, type);
|
|
68
|
-
else if (prev !== type) symRet.set(nm, CONFLICT);
|
|
69
|
-
};
|
|
70
|
-
|
|
71
|
-
for (const c of chunks) {
|
|
72
|
-
if (!c.name || c.name === 'anonymous') continue;
|
|
73
|
-
const nm = c.name.toLowerCase();
|
|
74
|
-
const via = c._return_via;
|
|
75
|
-
const annot = singleTypeName(c.return_type);
|
|
76
|
-
// Direct type wins over the edge: annotation and/or `return new X()`.
|
|
77
|
-
let directType = annot || (via && via.type) || null;
|
|
78
|
-
if (annot && via && via.type && annot !== via.type) { symRet.set(nm, CONFLICT); directType = null; }
|
|
79
|
-
if (directType) {
|
|
80
|
-
seed(nm, directType);
|
|
81
|
-
} else if (via && via.viaCall) {
|
|
82
|
-
const callee = via.viaCall.toLowerCase();
|
|
83
|
-
if (!edges.has(nm)) edges.set(nm, new Set());
|
|
84
|
-
edges.get(nm).add(callee);
|
|
85
|
-
}
|
|
86
|
-
}
|
|
87
|
-
|
|
88
|
-
const sortedEdgeKeys = [...edges.keys()].sort();
|
|
89
|
-
for (let iter = 0; iter < MAX_ITERS; iter++) {
|
|
90
|
-
let changed = false;
|
|
91
|
-
for (const sym of sortedEdgeKeys) {
|
|
92
|
-
if (symRet.has(sym)) continue; // already resolved (or conflict)
|
|
93
|
-
let resolved = null, conflict = false;
|
|
94
|
-
for (const callee of [...edges.get(sym)].sort()) {
|
|
95
|
-
const ct = symRet.get(callee);
|
|
96
|
-
if (ct === undefined || ct === CONFLICT) continue;
|
|
97
|
-
if (resolved === null) resolved = ct;
|
|
98
|
-
else if (resolved !== ct) conflict = true;
|
|
99
|
-
}
|
|
100
|
-
if (conflict) { symRet.set(sym, CONFLICT); changed = true; }
|
|
101
|
-
else if (resolved !== null) { symRet.set(sym, resolved); changed = true; }
|
|
102
|
-
}
|
|
103
|
-
if (!changed) break;
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
const out = new Map();
|
|
107
|
-
for (const [nm, t] of symRet) if (t !== CONFLICT && typeof t === 'string') out.set(nm, t);
|
|
108
|
-
return out;
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
/**
|
|
112
|
-
* Run the fixpoint over a chunk list and annotate each factory-receiver call site with the
|
|
113
|
-
* resolved `recv_resolved_type`; strip the transient `_return_via` so it never serializes.
|
|
114
|
-
* Mutates chunks in place and returns them.
|
|
115
|
-
*
|
|
116
|
-
* @param {object[]} chunks
|
|
117
|
-
* @returns {object[]} the same array
|
|
118
|
-
*/
|
|
119
|
-
export function applyInterprocedural(chunks) {
|
|
120
|
-
const symRet = resolveReturnTypes(chunks);
|
|
121
|
-
for (const c of chunks) {
|
|
122
|
-
if (Array.isArray(c.call_sites)) {
|
|
123
|
-
for (const s of c.call_sites) {
|
|
124
|
-
// Only fill in what intra-procedural inference left unresolved.
|
|
125
|
-
if (s && !s.recv_type && s.recv_via_call) {
|
|
126
|
-
const t = symRet.get(String(s.recv_via_call).toLowerCase());
|
|
127
|
-
if (t) s.recv_resolved_type = t;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
if ('_return_via' in c) delete c._return_via;
|
|
132
|
-
}
|
|
133
|
-
return chunks;
|
|
134
|
-
}
|