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/layout.mjs
DELETED
|
@@ -1,123 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file layout.mjs
|
|
3
|
-
* @description Single source of truth for graph-indexer's on-disk layout. All
|
|
4
|
-
* machine-generated runtime state lives in one tidy `.graph-indexer/`
|
|
5
|
-
* directory at the project root (index, vectors, SQLite db, enrichment
|
|
6
|
-
* cache, daemon pid/log, resolved config) instead of littering the
|
|
7
|
-
* root. Earlier versions wrote these artifacts straight into the root;
|
|
8
|
-
* `migrateLegacyLayout` relocates them transparently on the next run.
|
|
9
|
-
*
|
|
10
|
-
* Files that other tools must discover at conventional paths — the
|
|
11
|
-
* agent prompt suite (CLAUDE.md, GRAPH_INDEXER_PROMPT.md, Cursor
|
|
12
|
-
* rules) and IDE MCP configs (.vscode/mcp.json, .cursor/mcp.json, …) —
|
|
13
|
-
* intentionally stay where those tools expect them.
|
|
14
|
-
* @author MaquinaTech <https://github.com/MaquinaTech>
|
|
15
|
-
* @copyright (c) 2026 MaquinaTech. All rights reserved.
|
|
16
|
-
* @license MIT
|
|
17
|
-
*/
|
|
18
|
-
import fs from 'fs';
|
|
19
|
-
import path from 'path';
|
|
20
|
-
|
|
21
|
-
export const DATA_DIR_NAME = '.graph-indexer';
|
|
22
|
-
|
|
23
|
-
export const CONFIG_FILE_NAME = 'config.json';
|
|
24
|
-
|
|
25
|
-
export function dataDir(root) {
|
|
26
|
-
return path.join(root, DATA_DIR_NAME);
|
|
27
|
-
}
|
|
28
|
-
|
|
29
|
-
export function ensureDataDir(root) {
|
|
30
|
-
const dir = dataDir(root);
|
|
31
|
-
fs.mkdirSync(dir, { recursive: true });
|
|
32
|
-
return dir;
|
|
33
|
-
}
|
|
34
|
-
|
|
35
|
-
/**
|
|
36
|
-
* All generated artifact paths for a project root, derived from one stem so the
|
|
37
|
-
* indexer, daemon, MCP server and test harness can never drift apart.
|
|
38
|
-
* @returns {{dataDir,indexPath,embeddingPath,sqlitePath,enrichmentCachePath,pidFile,logFile,configPath}}
|
|
39
|
-
*/
|
|
40
|
-
export function artifactPaths(root) {
|
|
41
|
-
const dir = dataDir(root);
|
|
42
|
-
return {
|
|
43
|
-
dataDir: dir,
|
|
44
|
-
indexPath: path.join(dir, 'code-index.json'),
|
|
45
|
-
embeddingPath: path.join(dir, 'code-index.embeddings.bin'),
|
|
46
|
-
sqlitePath: path.join(dir, 'code-index.db'),
|
|
47
|
-
enrichmentCachePath: path.join(dir, 'code-index.enrichment.json'),
|
|
48
|
-
gitSignalsPath: path.join(dir, 'code-index.git.json'),
|
|
49
|
-
pidFile: path.join(dir, 'daemon.pid'),
|
|
50
|
-
logFile: path.join(dir, 'daemon.log'),
|
|
51
|
-
configPath: path.join(dir, CONFIG_FILE_NAME),
|
|
52
|
-
};
|
|
53
|
-
}
|
|
54
|
-
|
|
55
|
-
// ─── Legacy → current migration ─────────────────────────────────────────────────
|
|
56
|
-
|
|
57
|
-
// Regenerable runtime artifacts that lived at the project root before v1.4.
|
|
58
|
-
// Each [legacy-root-name, new-name-inside-data-dir]. When both copies exist the
|
|
59
|
-
// stale root one is safe to drop — it is a derived cache, not user data.
|
|
60
|
-
const LEGACY_ARTIFACTS = [
|
|
61
|
-
['code-index.json', 'code-index.json'],
|
|
62
|
-
['code-index.json.tmp', 'code-index.json.tmp'],
|
|
63
|
-
['code-index.embeddings.bin', 'code-index.embeddings.bin'],
|
|
64
|
-
['code-index.embeddings.bin.tmp', 'code-index.embeddings.bin.tmp'],
|
|
65
|
-
['code-index.db', 'code-index.db'],
|
|
66
|
-
['code-index.db-wal', 'code-index.db-wal'],
|
|
67
|
-
['code-index.db-shm', 'code-index.db-shm'],
|
|
68
|
-
['code-index.enrichment.json', 'code-index.enrichment.json'],
|
|
69
|
-
['.idx-daemon.pid', 'daemon.pid'],
|
|
70
|
-
['.idx-daemon.log', 'daemon.log'],
|
|
71
|
-
];
|
|
72
|
-
|
|
73
|
-
function relocate(src, dst) {
|
|
74
|
-
try { fs.renameSync(src, dst); return true; }
|
|
75
|
-
catch {
|
|
76
|
-
try { fs.copyFileSync(src, dst); fs.unlinkSync(src); return true; }
|
|
77
|
-
catch { return false; }
|
|
78
|
-
}
|
|
79
|
-
}
|
|
80
|
-
|
|
81
|
-
/**
|
|
82
|
-
* Relocate pre-v1.4 root artifacts into `.graph-indexer/`. Idempotent and
|
|
83
|
-
* non-destructive to live data: an artifact is only *moved* when the destination
|
|
84
|
-
* is absent; when a fresh copy already exists in the data dir, the orphaned root
|
|
85
|
-
* copy (a regenerable cache) is removed to declutter. The user-editable config
|
|
86
|
-
* (`.graph-indexer.json`) is only ever moved when no new config exists yet —
|
|
87
|
-
* never overwritten or deleted out from under the user.
|
|
88
|
-
*
|
|
89
|
-
* @returns {{moved:Array<{from:string,to:string}>, removed:string[], stoppedDaemon:boolean}}
|
|
90
|
-
*/
|
|
91
|
-
export function migrateLegacyLayout(root) {
|
|
92
|
-
const moved = [];
|
|
93
|
-
const removed = [];
|
|
94
|
-
const dir = dataDir(root);
|
|
95
|
-
|
|
96
|
-
for (const [legacyName, newName] of LEGACY_ARTIFACTS) {
|
|
97
|
-
const src = path.join(root, legacyName);
|
|
98
|
-
if (!fs.existsSync(src)) continue;
|
|
99
|
-
const dst = path.join(dir, newName);
|
|
100
|
-
if (fs.existsSync(dst)) {
|
|
101
|
-
try { fs.unlinkSync(src); removed.push(legacyName); } catch { /* keep on failure */ }
|
|
102
|
-
} else {
|
|
103
|
-
ensureDataDir(root);
|
|
104
|
-
if (relocate(src, dst)) moved.push({ from: legacyName, to: path.join(DATA_DIR_NAME, newName) });
|
|
105
|
-
}
|
|
106
|
-
}
|
|
107
|
-
|
|
108
|
-
const legacyCfg = path.join(root, '.graph-indexer.json');
|
|
109
|
-
const newCfg = path.join(dir, CONFIG_FILE_NAME);
|
|
110
|
-
if (fs.existsSync(legacyCfg) && !fs.existsSync(newCfg)) {
|
|
111
|
-
ensureDataDir(root);
|
|
112
|
-
if (relocate(legacyCfg, newCfg)) {
|
|
113
|
-
moved.push({ from: '.graph-indexer.json', to: path.join(DATA_DIR_NAME, CONFIG_FILE_NAME) });
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
return { moved, removed, stoppedDaemon: false };
|
|
118
|
-
}
|
|
119
|
-
|
|
120
|
-
export function hasLegacyLayout(root) {
|
|
121
|
-
if (fs.existsSync(path.join(root, '.graph-indexer.json'))) return true;
|
|
122
|
-
return LEGACY_ARTIFACTS.some(([legacyName]) => fs.existsSync(path.join(root, legacyName)));
|
|
123
|
-
}
|
package/mcp/centrality.mjs
DELETED
|
@@ -1,117 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file mcp/centrality.mjs
|
|
3
|
-
* @description Symbol-level centrality (A5): confidence-weighted PageRank over the A4
|
|
4
|
-
* resolved chunk→chunk symbol graph. A definition referenced (called / extended /
|
|
5
|
-
* used as a type) by many *other central* symbols scores high — it is a hub the
|
|
6
|
-
* rest of the program leans on.
|
|
7
|
-
*
|
|
8
|
-
* This is DISTINCT from `computePageRank` in search-core.mjs, which ranks FILES
|
|
9
|
-
* over the import graph (and drives enrichment selection + get_repo_map file
|
|
10
|
-
* ordering). A5 is symbol-granular and call/type-aware, not import-aware.
|
|
11
|
-
*
|
|
12
|
-
* Like the edges themselves, it is computed ONCE at index time and serialized into
|
|
13
|
-
* the index, so both backends serve byte-identical scores/ranks — parity is free,
|
|
14
|
-
* and it never touches search ranking. Opt-in: only built when --symbol-graph is on.
|
|
15
|
-
* @author MaquinaTech <https://github.com/MaquinaTech>
|
|
16
|
-
* @copyright (c) 2026 MaquinaTech. All rights reserved.
|
|
17
|
-
* @license MIT
|
|
18
|
-
*/
|
|
19
|
-
|
|
20
|
-
/**
|
|
21
|
-
* Per-confidence edge weight. A `name_only` edge is a heuristic name match (no receiver type
|
|
22
|
-
* / no import evidence) and is noisier than a `high` edge, so it contributes half the flow.
|
|
23
|
-
* The A1 `resolved` tier is a *precision* refinement of `high` (a provably-unambiguous binding),
|
|
24
|
-
* so it carries the SAME weight — enabling the precise resolver does not perturb centrality.
|
|
25
|
-
* All deterministic constants — no tuning knob, no config surface.
|
|
26
|
-
*/
|
|
27
|
-
const CONFIDENCE_WEIGHT = { resolved: 1.0, high: 1.0, name_only: 0.5 };
|
|
28
|
-
|
|
29
|
-
const DAMPING = 0.85;
|
|
30
|
-
const MAX_ITERS = 100; // hard safety net; sparse symbol graphs converge in ~20–40
|
|
31
|
-
const TOLERANCE = 1e-12; // L1 delta convergence
|
|
32
|
-
const SCORE_DECIMALS = 8; // round before serialization → identical bytes both backends
|
|
33
|
-
|
|
34
|
-
/**
|
|
35
|
-
* Confidence-weighted PageRank over the resolved symbol graph.
|
|
36
|
-
*
|
|
37
|
-
* Deterministic: nodes are the sorted set of edge endpoints, multi-edges between the same
|
|
38
|
-
* (from→to) pair sum their confidence weights, iteration walks nodes/adjacency in fixed order,
|
|
39
|
-
* and scores are rounded to a fixed precision. Ranks are a dense ordering by score DESC, then
|
|
40
|
-
* chunk-id ASC (the same tie-break the rest of the engine uses). The result is serialized, so
|
|
41
|
-
* cross-backend parity is by construction — both engines read these exact numbers.
|
|
42
|
-
*
|
|
43
|
-
* @param {Array<{from_chunk_id:string,to_chunk_id:string,kind:string,confidence:string}>} edges
|
|
44
|
-
* @param {object} [opts]
|
|
45
|
-
* @param {number} [opts.damping]
|
|
46
|
-
* @param {number} [opts.maxIters]
|
|
47
|
-
* @param {number} [opts.tolerance]
|
|
48
|
-
* @returns {{ centrality: Record<string,{score:number,rank:number}>, total:number, iters:number }}
|
|
49
|
-
* `centrality` is keyed by chunk id; `total` is the number of ranked (connected) chunks.
|
|
50
|
-
*/
|
|
51
|
-
export function computeSymbolCentrality(edges, {
|
|
52
|
-
damping = DAMPING, maxIters = MAX_ITERS, tolerance = TOLERANCE,
|
|
53
|
-
} = {}) {
|
|
54
|
-
// ── Node set = every chunk that appears in ANY edge (the connected sub-graph). Isolated
|
|
55
|
-
// chunks are not ranked — "rank N of M connected symbols" stays meaningful. ──────────
|
|
56
|
-
const nodeSet = new Set();
|
|
57
|
-
for (const e of (edges || [])) { nodeSet.add(e.from_chunk_id); nodeSet.add(e.to_chunk_id); }
|
|
58
|
-
const nodes = [...nodeSet].sort();
|
|
59
|
-
const N = nodes.length;
|
|
60
|
-
if (N === 0) return { centrality: {}, total: 0, iters: 0 };
|
|
61
|
-
|
|
62
|
-
const index = new Map();
|
|
63
|
-
nodes.forEach((id, i) => index.set(id, i));
|
|
64
|
-
|
|
65
|
-
// ── Aggregate edge weights by (from → to), summing confidence weights for multi-edges
|
|
66
|
-
// (a chunk that both calls AND extends a target flows more) → adjacency sorted by `to`
|
|
67
|
-
// index for deterministic float accumulation. ────────────────────────────────────────
|
|
68
|
-
const outMaps = nodes.map(() => new Map()); // i → Map(j → summed weight)
|
|
69
|
-
for (const e of (edges || [])) {
|
|
70
|
-
const i = index.get(e.from_chunk_id);
|
|
71
|
-
const j = index.get(e.to_chunk_id);
|
|
72
|
-
const w = CONFIDENCE_WEIGHT[e.confidence] ?? CONFIDENCE_WEIGHT.name_only;
|
|
73
|
-
outMaps[i].set(j, (outMaps[i].get(j) || 0) + w);
|
|
74
|
-
}
|
|
75
|
-
const adj = new Array(N); // i → [[j, weight], …] sorted by j
|
|
76
|
-
const outWeight = new Float64Array(N);
|
|
77
|
-
for (let i = 0; i < N; i++) {
|
|
78
|
-
const arr = [...outMaps[i].entries()].sort((a, b) => a[0] - b[0]);
|
|
79
|
-
let tot = 0;
|
|
80
|
-
for (const [, w] of arr) tot += w;
|
|
81
|
-
adj[i] = arr;
|
|
82
|
-
outWeight[i] = tot;
|
|
83
|
-
}
|
|
84
|
-
|
|
85
|
-
// ── Power iteration. Dangling nodes (no out-edges) redistribute their mass uniformly so
|
|
86
|
-
// the rank vector stays a probability distribution. ────────────────────────────────────
|
|
87
|
-
let rank = new Float64Array(N).fill(1 / N);
|
|
88
|
-
const teleport = (1 - damping) / N;
|
|
89
|
-
let iters = 0;
|
|
90
|
-
for (; iters < maxIters; iters++) {
|
|
91
|
-
let dangling = 0;
|
|
92
|
-
for (let i = 0; i < N; i++) if (outWeight[i] === 0) dangling += rank[i];
|
|
93
|
-
const danglingShare = damping * dangling / N;
|
|
94
|
-
|
|
95
|
-
const next = new Float64Array(N);
|
|
96
|
-
for (let i = 0; i < N; i++) next[i] = teleport + danglingShare;
|
|
97
|
-
for (let i = 0; i < N; i++) {
|
|
98
|
-
if (outWeight[i] === 0) continue;
|
|
99
|
-
const share = damping * rank[i] / outWeight[i];
|
|
100
|
-
for (const [j, w] of adj[i]) next[j] += share * w;
|
|
101
|
-
}
|
|
102
|
-
|
|
103
|
-
let delta = 0;
|
|
104
|
-
for (let i = 0; i < N; i++) delta += Math.abs(next[i] - rank[i]);
|
|
105
|
-
rank = next;
|
|
106
|
-
if (delta < tolerance) { iters++; break; }
|
|
107
|
-
}
|
|
108
|
-
|
|
109
|
-
// ── Round, then assign dense ranks by score DESC / id ASC. ───────────────────────────────
|
|
110
|
-
const round = 10 ** SCORE_DECIMALS;
|
|
111
|
-
const scored = nodes.map((id, i) => ({ id, score: Math.round(rank[i] * round) / round }));
|
|
112
|
-
scored.sort((a, b) => (b.score - a.score) || (a.id < b.id ? -1 : a.id > b.id ? 1 : 0));
|
|
113
|
-
|
|
114
|
-
const centrality = {};
|
|
115
|
-
scored.forEach((s, i) => { centrality[s.id] = { score: s.score, rank: i + 1 }; });
|
|
116
|
-
return { centrality, total: N, iters };
|
|
117
|
-
}
|
package/mcp/format.mjs
DELETED
|
@@ -1,61 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file mcp/format.mjs
|
|
3
|
-
* @description MCP rendering helpers: signature extraction, body pruning.
|
|
4
|
-
* @author MaquinaTech <https://github.com/MaquinaTech>
|
|
5
|
-
* @copyright (c) 2026 MaquinaTech. All rights reserved.
|
|
6
|
-
* @license MIT
|
|
7
|
-
*/
|
|
8
|
-
|
|
9
|
-
// ─── Rendering helpers ──────────────────────────────────────────────────────────
|
|
10
|
-
|
|
11
|
-
export function extractSignatureLine(codeSnippet) {
|
|
12
|
-
if (!codeSnippet) return '';
|
|
13
|
-
const lines = codeSnippet.split('\n');
|
|
14
|
-
const sigLines = [];
|
|
15
|
-
for (let i = 0; i < Math.min(lines.length, 10); i++) {
|
|
16
|
-
sigLines.push(lines[i]);
|
|
17
|
-
const l = lines[i];
|
|
18
|
-
if (i > 0 && (l.trimEnd().endsWith('{') || l.includes('=>') || l.trimEnd().endsWith(':'))) break;
|
|
19
|
-
}
|
|
20
|
-
return sigLines.join('\n');
|
|
21
|
-
}
|
|
22
|
-
|
|
23
|
-
/**
|
|
24
|
-
* When no lexical token matches (e.g. the agent used a high-level description
|
|
25
|
-
* like "authentication bottleneck" that isn't in the code verbatim), fall back
|
|
26
|
-
* to preserving structural skeleton — control-flow lines and calls — rather than
|
|
27
|
-
* blindly truncating, so the caller still gets meaningful context.
|
|
28
|
-
*/
|
|
29
|
-
export function pruneBodyByQuery(codeSnippet, queryTokens, maxLines = 40) {
|
|
30
|
-
if (!codeSnippet) return '';
|
|
31
|
-
const lines = codeSnippet.split('\n');
|
|
32
|
-
if (lines.length <= maxLines) return codeSnippet;
|
|
33
|
-
|
|
34
|
-
const querySet = new Set(queryTokens.filter(t => t.length >= 3).map(t => t.toLowerCase()));
|
|
35
|
-
if (querySet.size === 0) return lines.slice(0, maxLines).join('\n') + '\n// …';
|
|
36
|
-
|
|
37
|
-
const SIG_LINES = Math.min(5, lines.length);
|
|
38
|
-
const TAIL_LINES = Math.min(3, lines.length);
|
|
39
|
-
const sigBlock = lines.slice(0, SIG_LINES);
|
|
40
|
-
const tailBlock = lines.slice(Math.max(lines.length - TAIL_LINES, SIG_LINES));
|
|
41
|
-
|
|
42
|
-
const bodyLines = lines.slice(SIG_LINES, lines.length - TAIL_LINES);
|
|
43
|
-
const relevant = bodyLines.filter(line => {
|
|
44
|
-
const ll = line.toLowerCase();
|
|
45
|
-
if (/^\s*(return|throw|raise|yield)\b/.test(ll)) return true;
|
|
46
|
-
return [...querySet].some(token => ll.includes(token));
|
|
47
|
-
});
|
|
48
|
-
|
|
49
|
-
if (relevant.length === 0) {
|
|
50
|
-
const budget = Math.max(4, maxLines - SIG_LINES - TAIL_LINES);
|
|
51
|
-
const structural = bodyLines.filter(line => {
|
|
52
|
-
const ll = line.trimStart().toLowerCase();
|
|
53
|
-
if (/^(if |else |for |while |switch |try |catch |finally |return |throw |raise |yield |await )/.test(ll)) return true;
|
|
54
|
-
if (/[a-zA-Z_]\w*\s*\(/.test(line) && line.trim().length > 4) return true;
|
|
55
|
-
return false;
|
|
56
|
-
}).slice(0, budget);
|
|
57
|
-
if (structural.length > 0) return [...sigBlock, ...structural, ...tailBlock].join('\n');
|
|
58
|
-
return lines.slice(0, maxLines).join('\n') + '\n// …';
|
|
59
|
-
}
|
|
60
|
-
return [...sigBlock, ...relevant, ...tailBlock].join('\n');
|
|
61
|
-
}
|
package/mcp/resolver.mjs
DELETED
|
@@ -1,116 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file mcp/resolver.mjs
|
|
3
|
-
* @description Resolver providers (A1): pluggable confidence assignment for the A4 symbol
|
|
4
|
-
* graph. The DEFAULT `heuristic` provider preserves today's two-tier confidence
|
|
5
|
-
* ({ high, name_only }) exactly. The opt-in `precise` provider lifts the subset of
|
|
6
|
-
* edges whose binding is provably UNAMBIGUOUS into a third, stronger tier,
|
|
7
|
-
* `resolved` — a reference is `resolved` when there is no question *which*
|
|
8
|
-
* definition it binds to:
|
|
9
|
-
* • the symbol has a single definition in the repo (no same-named rival), or
|
|
10
|
-
* • the receiver's TYPE matches the defining class (typed receiver, the A3
|
|
11
|
-
* inter-procedural fixpoint result, `this.m()` inside the class, or an explicit
|
|
12
|
-
* target class) — type-resolved, so immune to name shadowing.
|
|
13
|
-
* `high` then means "ambiguous name, disambiguated by import/proximity" and
|
|
14
|
-
* `name_only` means "ambiguous name, no evidence."
|
|
15
|
-
*
|
|
16
|
-
* The provider only changes the `confidence` STRING on edges that already exist —
|
|
17
|
-
* it never adds, drops, or reorders edges — so it is parity-free (confidence is
|
|
18
|
-
* already serialized) and `findCallers`/`findReferers` set-equivalence is preserved.
|
|
19
|
-
* It runs at index time inside buildSymbolGraph and is inert without --symbol-graph.
|
|
20
|
-
*
|
|
21
|
-
* SCOPE (honest): v1 exposes the unambiguous-binding tier the engine can already
|
|
22
|
-
* prove from index-time signals. Detecting a LOCAL variable that *shadows* an import
|
|
23
|
-
* (and refuting that edge), and cross-file precise resolution via stack-graphs, are
|
|
24
|
-
* the documented next layers — both require per-language AST/scope analysis.
|
|
25
|
-
* @author MaquinaTech <https://github.com/MaquinaTech>
|
|
26
|
-
* @copyright (c) 2026 MaquinaTech. All rights reserved.
|
|
27
|
-
* @license MIT
|
|
28
|
-
*/
|
|
29
|
-
|
|
30
|
-
/** Total order on confidence — strongest first. Used by the symbol-graph dedupe. */
|
|
31
|
-
export const CONFIDENCE_RANK = { resolved: 3, high: 2, name_only: 1 };
|
|
32
|
-
|
|
33
|
-
/** The stronger of two confidence strings (ties → `a`). */
|
|
34
|
-
export function strongerConfidence(a, b) {
|
|
35
|
-
return (CONFIDENCE_RANK[b] || 0) > (CONFIDENCE_RANK[a] || 0) ? b : a;
|
|
36
|
-
}
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Default provider: identity. Edge confidence stays exactly { high, name_only } — the index is
|
|
40
|
-
* byte-identical to a pre-A1 build.
|
|
41
|
-
*/
|
|
42
|
-
const HEURISTIC = {
|
|
43
|
-
name: 'heuristic',
|
|
44
|
-
/** @param {string} base 'high' | 'name_only' @param {boolean} _proven */
|
|
45
|
-
confidenceFor(base /* , proven */) { return base; },
|
|
46
|
-
};
|
|
47
|
-
|
|
48
|
-
/**
|
|
49
|
-
* Precise provider: promote a `high` edge to `resolved` when its binding is provably
|
|
50
|
-
* unambiguous (the `proven` flag set by classifyCallers / findReferences). Never touches
|
|
51
|
-
* `name_only` (no evidence stays no evidence) and never downgrades.
|
|
52
|
-
*/
|
|
53
|
-
const PRECISE = {
|
|
54
|
-
name: 'precise',
|
|
55
|
-
/** @param {string} base @param {boolean} proven */
|
|
56
|
-
confidenceFor(base, proven) { return base === 'high' && proven ? 'resolved' : base; },
|
|
57
|
-
};
|
|
58
|
-
|
|
59
|
-
const PROVIDERS = { heuristic: HEURISTIC, precise: PRECISE };
|
|
60
|
-
|
|
61
|
-
/** Resolve a provider by name; unknown names fall back to the default heuristic. */
|
|
62
|
-
export function getResolver(name) { return PROVIDERS[name] || HEURISTIC; }
|
|
63
|
-
|
|
64
|
-
/**
|
|
65
|
-
* SCIP provider (A2): a DATA-BACKED resolver built from a cross-file binding relation
|
|
66
|
-
* (`bindings: Map<fromChunkId, Set<defChunkId>>`) and the set of defs SCIP actually saw
|
|
67
|
-
* (`definedChunks`), both produced from a locally-generated SCIP index (see parse/scip.mjs
|
|
68
|
-
* `buildScipBindings`). Unlike heuristic/precise — which only RELABEL a fixed edge set — the SCIP
|
|
69
|
-
* provider can also SUPPRESS edges, because SCIP knows the *exact* binding among several same-named
|
|
70
|
-
* definitions. For a caller `fromId` and the candidate defs `defIds` of one name:
|
|
71
|
-
*
|
|
72
|
-
* • no SCIP target for `fromId` at all, OR none of `defIds` is a target → return null: the
|
|
73
|
-
* builder falls through to the heuristic confidence (absence ≠ refutation for this name).
|
|
74
|
-
* • SCIP confirms `fromId` binds to ≥1 of `defIds` → `resolved` = those confirmed def(s);
|
|
75
|
-
* `suppressed` = the OTHER candidate defs **that SCIP actually saw as definitions**
|
|
76
|
-
* (`def ∈ definedChunks`). A candidate def SCIP NEVER recorded — an uncovered file in a
|
|
77
|
-
* polyglot / partial run, a stale `.scip`, a missed occurrence — is neither resolved nor
|
|
78
|
-
* suppressed: it falls through to heuristic, so a covered caller referencing an *uncovered* def
|
|
79
|
-
* is never wrongly dropped. This is the soundness gate the partial-coverage case needs.
|
|
80
|
-
*
|
|
81
|
-
* It refines the heuristic's CANDIDATE edges (promote + suppress); it does not synthesize edges the
|
|
82
|
-
* AST call-extraction missed (a v2 recall concern). The symbol-graph builder applies this to `calls`
|
|
83
|
-
* edges only — the binding relation is kind-agnostic, so `extends`/`type` edges stay heuristic.
|
|
84
|
-
* `confidenceFor` is identity, so any edge SCIP does not cover keeps its base confidence.
|
|
85
|
-
*
|
|
86
|
-
* @param {Map<string, Set<string>>} bindings fromChunkId → set of def chunk ids SCIP confirms.
|
|
87
|
-
* @param {Set<string>} [definedChunks] every chunk SCIP recorded as a definition (suppression gate).
|
|
88
|
-
* @returns {{ name:string, confidenceFor:Function, resolveEdges:Function }}
|
|
89
|
-
*/
|
|
90
|
-
export function createScipResolver(bindings, definedChunks) {
|
|
91
|
-
const rel = bindings instanceof Map ? bindings : new Map();
|
|
92
|
-
const defined = definedChunks instanceof Set ? definedChunks : new Set();
|
|
93
|
-
return {
|
|
94
|
-
name: 'scip',
|
|
95
|
-
/** Uncovered edges keep their base confidence (no promotion without SCIP evidence). */
|
|
96
|
-
confidenceFor(base /* , proven */) { return base; },
|
|
97
|
-
/**
|
|
98
|
-
* @param {{ fromId:string, defIds:string[] }} q
|
|
99
|
-
* @returns {{ resolved: Set<string>, suppressed: Set<string> } | null} null → no SCIP info.
|
|
100
|
-
*/
|
|
101
|
-
resolveEdges({ fromId, defIds }) {
|
|
102
|
-
const targets = rel.get(fromId);
|
|
103
|
-
if (!targets || !targets.size) return null; // caller uncovered → no opinion
|
|
104
|
-
const resolved = new Set();
|
|
105
|
-
for (const id of defIds) if (targets.has(id)) resolved.add(id);
|
|
106
|
-
if (!resolved.size) return null; // binds none of THIS name's defs → fall through
|
|
107
|
-
const suppressed = new Set();
|
|
108
|
-
for (const id of defIds) {
|
|
109
|
-
if (resolved.has(id)) continue;
|
|
110
|
-
if (defined.has(id)) suppressed.add(id); // SCIP saw this def but bound elsewhere → wrong-target
|
|
111
|
-
// else: def uncovered by SCIP → keep heuristic (sound under partial coverage)
|
|
112
|
-
}
|
|
113
|
-
return { resolved, suppressed };
|
|
114
|
-
},
|
|
115
|
-
};
|
|
116
|
-
}
|
package/mcp/symbolgraph.mjs
DELETED
|
@@ -1,137 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file mcp/symbolgraph.mjs
|
|
3
|
-
* @description Builds the persistent RESOLVED symbol graph (A4): one edge per resolved
|
|
4
|
-
* (referencing-chunk → definition-chunk) pair, tagged with `kind`
|
|
5
|
-
* ('calls' | 'extends' | 'type') and `confidence`. Confidence is `high` | `name_only`
|
|
6
|
-
* on the default `heuristic` path; the opt-in resolvers add a `resolved` tier — A1
|
|
7
|
-
* (`precise`) promotes provably-unambiguous edges, A2 (`scip`) confirms a cross-file
|
|
8
|
-
* binding AND suppresses the wrong-target fan-out a same-name ambiguity would emit.
|
|
9
|
-
*
|
|
10
|
-
* It REUSES the query-time resolvers (classifyCallers / findReferences) over
|
|
11
|
-
* every defined symbol, so an edge's confidence is identical to what
|
|
12
|
-
* get_call_graph / find_references would report — no logic duplication, no
|
|
13
|
-
* drift. The result is serialized into the index so both backends expose the
|
|
14
|
-
* same graph through getEdges() (deterministically ordered → parity-free), and
|
|
15
|
-
* findCallers / findReferers can read it instead of scanning.
|
|
16
|
-
*
|
|
17
|
-
* Opt-in and index-time only (see config.symbolGraph / indexer.mjs). It does
|
|
18
|
-
* not touch search ranking. This is the structural foundation for impact_of_edit
|
|
19
|
-
* (transitive getEdges), symbol-level PageRank (A5), and precise resolution (A1,
|
|
20
|
-
* which upgrades 'high' → 'resolved').
|
|
21
|
-
* @author MaquinaTech <https://github.com/MaquinaTech>
|
|
22
|
-
* @copyright (c) 2026 MaquinaTech. All rights reserved.
|
|
23
|
-
* @license MIT
|
|
24
|
-
*/
|
|
25
|
-
import { classifyCallers, findReferences } from './topology.mjs';
|
|
26
|
-
import { getResolver, strongerConfidence } from './resolver.mjs';
|
|
27
|
-
|
|
28
|
-
/** Deterministic total order on edges — required for memory↔sqlite parity. */
|
|
29
|
-
export function edgeOrder(a, b) {
|
|
30
|
-
return (a.from_chunk_id < b.from_chunk_id ? -1 : a.from_chunk_id > b.from_chunk_id ? 1 : 0)
|
|
31
|
-
|| (a.to_chunk_id < b.to_chunk_id ? -1 : a.to_chunk_id > b.to_chunk_id ? 1 : 0)
|
|
32
|
-
|| (a.kind < b.kind ? -1 : a.kind > b.kind ? 1 : 0)
|
|
33
|
-
|| (a.confidence < b.confidence ? -1 : a.confidence > b.confidence ? 1 : 0);
|
|
34
|
-
}
|
|
35
|
-
|
|
36
|
-
const normConfidence = (c) => (c === 'high' ? 'high' : 'name_only');
|
|
37
|
-
|
|
38
|
-
/**
|
|
39
|
-
* Build the resolved edge list from a loaded store.
|
|
40
|
-
*
|
|
41
|
-
* @param {object} db A loaded store (iterateChunks, resolveSymbol, findCallers,
|
|
42
|
-
* findReferers, getDependencies — the same contract the resolvers use).
|
|
43
|
-
* @param {object} [opts]
|
|
44
|
-
* @param {number} [opts.maxPerName] Safety cap on edges emitted for one symbol name
|
|
45
|
-
* (pathological ambiguous names). Excess is logged, not
|
|
46
|
-
* silently dropped.
|
|
47
|
-
* @param {object} [opts.resolver] A1 resolver provider (default `heuristic` → byte-identical
|
|
48
|
-
* { high, name_only }; `precise` → adds the `resolved` tier).
|
|
49
|
-
* @returns {{ edges: Array<{from_chunk_id:string,to_chunk_id:string,kind:string,confidence:string}>,
|
|
50
|
-
* cappedNames: string[] }}
|
|
51
|
-
*/
|
|
52
|
-
export function buildSymbolGraph(db, { maxPerName = 20000, resolver = getResolver('heuristic') } = {}) {
|
|
53
|
-
const edges = [];
|
|
54
|
-
const cappedNames = [];
|
|
55
|
-
|
|
56
|
-
const calleeNames = new Set();
|
|
57
|
-
const refNames = new Set();
|
|
58
|
-
for (const c of db.iterateChunks()) {
|
|
59
|
-
for (const n of (c.calls || [])) calleeNames.add(n);
|
|
60
|
-
for (const t of (c.extends || [])) refNames.add(t);
|
|
61
|
-
for (const t of (c.type_refs || [])) refNames.add(t);
|
|
62
|
-
}
|
|
63
|
-
|
|
64
|
-
// ── 'calls' edges: every name-match caller → each definition of the callee name,
|
|
65
|
-
// bucketed high / name_only exactly as classifyCallers reports. ──────────────
|
|
66
|
-
for (const name of [...calleeNames].sort()) {
|
|
67
|
-
const defs = db.resolveSymbol(name);
|
|
68
|
-
if (!defs.length) continue; // undefined name → no edge (findCallers falls back to scan)
|
|
69
|
-
const defIds = defs.map(d => d.id);
|
|
70
|
-
const { high, nameOnly } = classifyCallers(db, name);
|
|
71
|
-
let n = 0; let capped = false;
|
|
72
|
-
const emit = (callers, baseConf) => {
|
|
73
|
-
for (const { chunk, proven } of callers) {
|
|
74
|
-
// A2 (calls edges only): a data-backed resolver (scip) may CONFIRM the exact binding
|
|
75
|
-
// (→ `resolved`) and SUPPRESS the SCIP-known wrong-target siblings. A def SCIP never
|
|
76
|
-
// saw is neither — it falls through to the heuristic confidence (sound under partial
|
|
77
|
-
// coverage). No resolveEdges (heuristic/precise) → decision null → byte-identical.
|
|
78
|
-
const decision = resolver.resolveEdges
|
|
79
|
-
? resolver.resolveEdges({ fromId: chunk.id, defIds })
|
|
80
|
-
: null;
|
|
81
|
-
const fallbackConf = resolver.confidenceFor(baseConf, Boolean(proven));
|
|
82
|
-
for (const def of defs) {
|
|
83
|
-
if (n >= maxPerName) { capped = true; return; }
|
|
84
|
-
if (decision && decision.resolved.has(def.id)) {
|
|
85
|
-
edges.push({ from_chunk_id: chunk.id, to_chunk_id: def.id, kind: 'calls', confidence: 'resolved' });
|
|
86
|
-
} else if (decision && decision.suppressed.has(def.id)) {
|
|
87
|
-
continue; // SCIP-confirmed wrong-target → suppress
|
|
88
|
-
} else {
|
|
89
|
-
edges.push({ from_chunk_id: chunk.id, to_chunk_id: def.id, kind: 'calls', confidence: fallbackConf });
|
|
90
|
-
}
|
|
91
|
-
n++;
|
|
92
|
-
}
|
|
93
|
-
}
|
|
94
|
-
};
|
|
95
|
-
emit(high, 'high');
|
|
96
|
-
if (!capped) emit(nameOnly, 'name_only');
|
|
97
|
-
if (capped) cappedNames.push(name);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
// ── 'extends' / 'type' edges: subclasses/implementers and type users → each
|
|
101
|
-
// definition, with the per-reference confidence findReferences assigns. ──────
|
|
102
|
-
for (const name of [...refNames].sort()) {
|
|
103
|
-
const defs = db.resolveSymbol(name);
|
|
104
|
-
if (!defs.length) continue;
|
|
105
|
-
const { inherits, types } = findReferences(db, name);
|
|
106
|
-
let n = 0; let capped = false;
|
|
107
|
-
// A2 scopes SCIP resolution to `calls` edges only: the binding relation is kind-agnostic, so
|
|
108
|
-
// applying it here would let a call-site binding suppress/relabel `extends`/`type` siblings of
|
|
109
|
-
// a different kind. These edges therefore stay heuristic under every resolver (byte-identical).
|
|
110
|
-
const emit = (refs, kind) => {
|
|
111
|
-
for (const r of refs) {
|
|
112
|
-
const confidence = resolver.confidenceFor(normConfidence(r.confidence), Boolean(r.proven));
|
|
113
|
-
for (const def of defs) {
|
|
114
|
-
if (n >= maxPerName) { capped = true; return; }
|
|
115
|
-
edges.push({ from_chunk_id: r.chunk.id, to_chunk_id: def.id, kind, confidence });
|
|
116
|
-
n++;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
};
|
|
120
|
-
emit(inherits, 'extends');
|
|
121
|
-
if (!capped) emit(types, 'type');
|
|
122
|
-
if (capped && !cappedNames.includes(name)) cappedNames.push(name);
|
|
123
|
-
}
|
|
124
|
-
|
|
125
|
-
// Dedupe by (from, to, kind) keeping the STRONGEST confidence (resolved > high > name_only),
|
|
126
|
-
// then return in the deterministic parity order. (On the default `heuristic` path the only
|
|
127
|
-
// values are high/name_only and high is strongest → identical to the prior keep-first dedupe.)
|
|
128
|
-
const rep = new Map();
|
|
129
|
-
for (const e of edges) {
|
|
130
|
-
const k = `${e.from_chunk_id}|${e.to_chunk_id}|${e.kind}`;
|
|
131
|
-
const cur = rep.get(k);
|
|
132
|
-
if (!cur) rep.set(k, { from_chunk_id: e.from_chunk_id, to_chunk_id: e.to_chunk_id, kind: e.kind, confidence: e.confidence });
|
|
133
|
-
else cur.confidence = strongerConfidence(cur.confidence, e.confidence);
|
|
134
|
-
}
|
|
135
|
-
const out = [...rep.values()].sort(edgeOrder);
|
|
136
|
-
return { edges: out, cappedNames };
|
|
137
|
-
}
|