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/git-signals.mjs
DELETED
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file git-signals.mjs
|
|
3
|
-
* @description Lightweight, air-gapped git-history signals for ranking and
|
|
4
|
-
* blast-radius hints. Reads ONLY the local repository's commit log
|
|
5
|
-
* (no network, no remote, no code sent anywhere) and derives three
|
|
6
|
-
* cheap per-file signals competitors mostly ignore:
|
|
7
|
-
*
|
|
8
|
-
* • churn — how often a file changes (commit count);
|
|
9
|
-
* • recency — how recently it last changed (last-commit time);
|
|
10
|
-
* • co-change — which files historically change *together* (the
|
|
11
|
-
* "if you touch A you usually touch B" hint that
|
|
12
|
-
* sharpens the refactoring/blast-radius story).
|
|
13
|
-
*
|
|
14
|
-
* Computed once at index time into a sidecar (code-index.git.json),
|
|
15
|
-
* kept out of the storage contract and the ranking math: the
|
|
16
|
-
* co-change hint is surfaced by the blast-radius tools, and the
|
|
17
|
-
* recency/churn boost is applied opt-in in the tool layer (default
|
|
18
|
-
* off), so the measured retrieval ranking is unchanged by default.
|
|
19
|
-
* @author MaquinaTech <https://github.com/MaquinaTech>
|
|
20
|
-
* @copyright (c) 2026 MaquinaTech. All rights reserved.
|
|
21
|
-
* @license MIT
|
|
22
|
-
*/
|
|
23
|
-
import { execFileSync } from 'child_process';
|
|
24
|
-
import fs from 'fs';
|
|
25
|
-
import path from 'path';
|
|
26
|
-
|
|
27
|
-
// Record/field separators chosen to never collide with file paths.
|
|
28
|
-
const REC = '\x01';
|
|
29
|
-
const FS_ = '\x1f';
|
|
30
|
-
|
|
31
|
-
/**
|
|
32
|
-
* Collect git signals for a repository from its local commit log.
|
|
33
|
-
*
|
|
34
|
-
* Strictly local: shells out to `git log` on `repoRoot` and parses stdout. No
|
|
35
|
-
* remote is contacted. Returns `null` (a no-op) when the directory is not a git
|
|
36
|
-
* repo, git is unavailable, or there is no history — callers degrade silently.
|
|
37
|
-
*
|
|
38
|
-
* @param {string} repoRoot
|
|
39
|
-
* @param {object} [opts]
|
|
40
|
-
* @param {number} [opts.maxCommits=500] History depth (bounds cost + size).
|
|
41
|
-
* @param {number} [opts.topCoChange=8] Co-changed files kept per file.
|
|
42
|
-
* @param {number} [opts.maxFilesPerCommit=50] Skip sweeping commits (mass renames,
|
|
43
|
-
* formatting) whose co-change is noise.
|
|
44
|
-
* @returns {null | {generated:number, commits:number, maxChurn:number,
|
|
45
|
-
* churn:Object<string,number>, lastCommit:Object<string,number>,
|
|
46
|
-
* coChange:Object<string,Array<[string,number]>>}}
|
|
47
|
-
*/
|
|
48
|
-
export function collectGitSignals(repoRoot, { maxCommits = 500, topCoChange = 8, maxFilesPerCommit = 50 } = {}) {
|
|
49
|
-
// Resolve the actual repo root. If `repoRoot` is a SUBDIRECTORY of a larger
|
|
50
|
-
// repo (e.g. indexing one package, or a fixture nested in this repo), a bare
|
|
51
|
-
// `git -C <dir> log` walks UP and returns the PARENT repo's history with
|
|
52
|
-
// parent-root-relative paths that never match this index's file paths. So we
|
|
53
|
-
// find the toplevel, scope the log to this subtree, and strip the prefix so
|
|
54
|
-
// emitted paths are relative to `repoRoot` (matching chunk.file_path).
|
|
55
|
-
let prefix = '';
|
|
56
|
-
try {
|
|
57
|
-
const toplevel = execFileSync('git', ['-C', repoRoot, 'rev-parse', '--show-toplevel'],
|
|
58
|
-
{ encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim();
|
|
59
|
-
if (!toplevel) return null;
|
|
60
|
-
// Compare REAL paths: git returns the physical toplevel, while repoRoot may
|
|
61
|
-
// contain symlinks (e.g. macOS /var → /private/var), which would otherwise
|
|
62
|
-
// produce a bogus prefix.
|
|
63
|
-
const realTop = (() => { try { return fs.realpathSync(toplevel); } catch { return toplevel; } })();
|
|
64
|
-
const realRoot = (() => { try { return fs.realpathSync(path.resolve(repoRoot)); } catch { return path.resolve(repoRoot); } })();
|
|
65
|
-
const rel = path.relative(realTop, realRoot).replace(/\\/g, '/');
|
|
66
|
-
prefix = rel && rel !== '.' ? rel : '';
|
|
67
|
-
} catch {
|
|
68
|
-
return null; // not a git repo / git missing — air-gapped no-op
|
|
69
|
-
}
|
|
70
|
-
|
|
71
|
-
let raw;
|
|
72
|
-
try {
|
|
73
|
-
const args = ['-C', repoRoot, 'log', '--no-merges', `-n${maxCommits}`,
|
|
74
|
-
`--pretty=format:${REC}%H${FS_}%ct`, '--name-only'];
|
|
75
|
-
if (prefix) args.push('--', '.'); // limit to this subtree (paths stay toplevel-relative)
|
|
76
|
-
raw = execFileSync('git', args,
|
|
77
|
-
{ encoding: 'utf-8', maxBuffer: 128 * 1024 * 1024, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
78
|
-
} catch {
|
|
79
|
-
return null;
|
|
80
|
-
}
|
|
81
|
-
if (!raw || !raw.includes(REC)) return null;
|
|
82
|
-
const stripPrefix = prefix ? prefix + '/' : '';
|
|
83
|
-
|
|
84
|
-
const churn = Object.create(null);
|
|
85
|
-
const lastCommit = Object.create(null);
|
|
86
|
-
const pairCounts = new Map(); // file → Map<coFile, count>
|
|
87
|
-
let commits = 0;
|
|
88
|
-
|
|
89
|
-
const bump = (f, ts) => {
|
|
90
|
-
churn[f] = (churn[f] || 0) + 1;
|
|
91
|
-
if (ts > (lastCommit[f] || 0)) lastCommit[f] = ts;
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
for (const block of raw.split(REC)) {
|
|
95
|
-
if (!block) continue;
|
|
96
|
-
const nl = block.indexOf('\n');
|
|
97
|
-
const header = nl === -1 ? block : block.slice(0, nl);
|
|
98
|
-
const ts = parseInt(header.split(FS_)[1], 10);
|
|
99
|
-
if (!Number.isFinite(ts)) continue;
|
|
100
|
-
commits++;
|
|
101
|
-
|
|
102
|
-
const files = [];
|
|
103
|
-
if (nl !== -1) {
|
|
104
|
-
for (const line of block.slice(nl + 1).split('\n')) {
|
|
105
|
-
let f = line.trim();
|
|
106
|
-
if (!f) continue;
|
|
107
|
-
// git quotes paths with unusual chars: "src/é.ts" — unwrap.
|
|
108
|
-
if (f.startsWith('"') && f.endsWith('"')) f = f.slice(1, -1);
|
|
109
|
-
if (stripPrefix) {
|
|
110
|
-
if (!f.startsWith(stripPrefix)) continue; // outside the indexed subtree
|
|
111
|
-
f = f.slice(stripPrefix.length);
|
|
112
|
-
}
|
|
113
|
-
files.push(f);
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
for (const f of files) bump(f, ts);
|
|
117
|
-
|
|
118
|
-
// Co-change: only for focused commits — a 400-file sweep links nothing useful.
|
|
119
|
-
if (files.length >= 2 && files.length <= maxFilesPerCommit) {
|
|
120
|
-
for (let i = 0; i < files.length; i++) {
|
|
121
|
-
const a = files[i];
|
|
122
|
-
let row = pairCounts.get(a);
|
|
123
|
-
if (!row) { row = new Map(); pairCounts.set(a, row); }
|
|
124
|
-
for (let j = 0; j < files.length; j++) {
|
|
125
|
-
if (i === j) continue;
|
|
126
|
-
const b = files[j];
|
|
127
|
-
row.set(b, (row.get(b) || 0) + 1);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
}
|
|
132
|
-
|
|
133
|
-
if (commits === 0) return null;
|
|
134
|
-
|
|
135
|
-
// Stamp HEAD so clients can report "indexed at commit X" and detect drift
|
|
136
|
-
// (the freshness contract). Best-effort — absence just omits the commit.
|
|
137
|
-
let head = null;
|
|
138
|
-
try {
|
|
139
|
-
head = execFileSync('git', ['-C', repoRoot, 'rev-parse', '--short', 'HEAD'],
|
|
140
|
-
{ encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() || null;
|
|
141
|
-
} catch { /* detached/empty — no stamp */ }
|
|
142
|
-
|
|
143
|
-
const coChange = Object.create(null);
|
|
144
|
-
for (const [file, row] of pairCounts) {
|
|
145
|
-
const top = [...row.entries()]
|
|
146
|
-
.sort((x, y) => y[1] - x[1] || x[0].localeCompare(y[0]))
|
|
147
|
-
.slice(0, topCoChange);
|
|
148
|
-
if (top.length) coChange[file] = top;
|
|
149
|
-
}
|
|
150
|
-
|
|
151
|
-
let maxChurn = 1;
|
|
152
|
-
for (const k in churn) if (churn[k] > maxChurn) maxChurn = churn[k];
|
|
153
|
-
|
|
154
|
-
return { generated: Math.floor(Date.now() / 1000), head, commits, maxChurn, churn, lastCommit, coChange };
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
/** Persist signals next to the index (best-effort). */
|
|
158
|
-
export function writeGitSignals(filePath, signals) {
|
|
159
|
-
try { fs.writeFileSync(filePath, JSON.stringify(signals)); return true; }
|
|
160
|
-
catch { return false; }
|
|
161
|
-
}
|
|
162
|
-
|
|
163
|
-
/** Load a signals sidecar, or null if absent/corrupt. */
|
|
164
|
-
export function loadGitSignals(filePath) {
|
|
165
|
-
try { return JSON.parse(fs.readFileSync(filePath, 'utf-8')); }
|
|
166
|
-
catch { return null; }
|
|
167
|
-
}
|
|
168
|
-
|
|
169
|
-
/** Files that historically change together with `file`, strongest first. */
|
|
170
|
-
export function coChangesFor(signals, file, limit = 5) {
|
|
171
|
-
if (!signals || !signals.coChange) return [];
|
|
172
|
-
const row = signals.coChange[file];
|
|
173
|
-
if (!row) return [];
|
|
174
|
-
return row.slice(0, limit).map(([f, count]) => ({ file: f, count }));
|
|
175
|
-
}
|
|
176
|
-
|
|
177
|
-
// ─── Index freshness (query-time drift detection) ────────────────────────────
|
|
178
|
-
// The blast-radius tools are only trustworthy if the index reflects the working
|
|
179
|
-
// tree. We surface a compact freshness signal on tool responses: index age, the
|
|
180
|
-
// commit it was built at (stamped above), and whether the tree has drifted since.
|
|
181
|
-
// The git read is cheap and cached briefly so a burst of tool calls costs one
|
|
182
|
-
// `git status`; it degrades silently (null) outside a git repo.
|
|
183
|
-
|
|
184
|
-
const _SRC_RE = /\.(js|jsx|mjs|cjs|ts|tsx|py|go|rs|java|kt|cs|rb|php|swift|c|h|cc|cpp|hpp|scss|css|sh|bash)$/i;
|
|
185
|
-
let _gitStateCache = null; // { at, root, head, dirtyCount }
|
|
186
|
-
|
|
187
|
-
/** Reset the cached working-tree state (tests). */
|
|
188
|
-
export function _resetGitStateCache() { _gitStateCache = null; }
|
|
189
|
-
|
|
190
|
-
/**
|
|
191
|
-
* Cheap, cached working-tree state: current short HEAD + count of changed SOURCE
|
|
192
|
-
* files (uncommitted). Cached `ttlMs` per root so a burst of tool calls shells out
|
|
193
|
-
* at most once. `exec`/`nowMs` injectable for tests. Returns {head:null,dirtyCount:null}
|
|
194
|
-
* outside a git repo.
|
|
195
|
-
*/
|
|
196
|
-
export function currentGitState(repoRoot, { ttlMs = 5000, nowMs = Date.now(), exec = execFileSync } = {}) {
|
|
197
|
-
if (_gitStateCache && _gitStateCache.root === repoRoot && (nowMs - _gitStateCache.at) < ttlMs) return _gitStateCache;
|
|
198
|
-
let head = null, dirtyCount = null;
|
|
199
|
-
try {
|
|
200
|
-
head = exec('git', ['-C', repoRoot, 'rev-parse', '--short', 'HEAD'],
|
|
201
|
-
{ encoding: 'utf-8', stdio: ['ignore', 'pipe', 'ignore'] }).trim() || null;
|
|
202
|
-
const status = exec('git', ['-C', repoRoot, 'status', '--porcelain'],
|
|
203
|
-
{ encoding: 'utf-8', maxBuffer: 16 * 1024 * 1024, stdio: ['ignore', 'pipe', 'ignore'] });
|
|
204
|
-
dirtyCount = status.split('\n').filter(l => l.trim() && _SRC_RE.test(l)).length;
|
|
205
|
-
} catch { /* not a git repo / git missing → unknown */ }
|
|
206
|
-
_gitStateCache = { at: nowMs, root: repoRoot, head, dirtyCount };
|
|
207
|
-
return _gitStateCache;
|
|
208
|
-
}
|
|
209
|
-
|
|
210
|
-
/**
|
|
211
|
-
* Derive the freshness verdict from index age + the stamped index commit + the
|
|
212
|
-
* current working-tree state + whether the watch daemon is live. Pure (no I/O) so
|
|
213
|
-
* it is trivially testable.
|
|
214
|
-
*
|
|
215
|
-
* @returns {{ageSeconds:number|null, ageLabel:string, indexedCommit:string|null,
|
|
216
|
-
* currentCommit:string|null, pendingChanges:number|null, commitMoved:boolean,
|
|
217
|
-
* daemonRunning:boolean, stale:boolean, syncing:boolean}}
|
|
218
|
-
*/
|
|
219
|
-
export function computeFreshness({ ageSeconds = null, indexedCommit = null, current = null, daemonRunning = false } = {}) {
|
|
220
|
-
const ageLabel = ageSeconds == null ? 'unknown'
|
|
221
|
-
: ageSeconds < 60 ? `${ageSeconds}s` : ageSeconds < 3600 ? `${Math.floor(ageSeconds / 60)}m` : `${Math.floor(ageSeconds / 3600)}h`;
|
|
222
|
-
const currentCommit = current?.head ?? null;
|
|
223
|
-
const pendingChanges = current?.dirtyCount ?? null;
|
|
224
|
-
const commitMoved = Boolean(indexedCommit && currentCommit && indexedCommit !== currentCommit);
|
|
225
|
-
const hasPending = (pendingChanges != null && pendingChanges > 0) || commitMoved;
|
|
226
|
-
// A live daemon syncs changes incrementally, so pending work is "syncing", not
|
|
227
|
-
// "stale". Only when no daemon is keeping the index fresh is drift truly stale.
|
|
228
|
-
return {
|
|
229
|
-
ageSeconds, ageLabel, indexedCommit, currentCommit, pendingChanges, commitMoved,
|
|
230
|
-
daemonRunning, stale: hasPending && !daemonRunning, syncing: hasPending && daemonRunning,
|
|
231
|
-
};
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
/**
|
|
235
|
-
* A bounded 0..1 "this file is hot" score combining recency and churn, for the
|
|
236
|
-
* OPT-IN ranking boost only. Returns 0 when there is no signal for the file, so a
|
|
237
|
-
* disabled or signal-less repo never perturbs ranking.
|
|
238
|
-
*/
|
|
239
|
-
export function gitBoostScore(signals, file, nowSec = Date.now() / 1000) {
|
|
240
|
-
if (!signals) return 0;
|
|
241
|
-
const churn = (signals.churn && signals.churn[file]) || 0;
|
|
242
|
-
const last = (signals.lastCommit && signals.lastCommit[file]) || 0;
|
|
243
|
-
if (!churn && !last) return 0;
|
|
244
|
-
const ageDays = last ? Math.max(0, (nowSec - last) / 86400) : 3650;
|
|
245
|
-
const recency = Math.exp(-ageDays / 90); // ~quarter half-life
|
|
246
|
-
const churnScore = Math.log1p(churn) / Math.log1p(signals.maxChurn || 1); // 0..1
|
|
247
|
-
return 0.5 * recency + 0.5 * churnScore;
|
|
248
|
-
}
|