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.
Files changed (129) hide show
  1. package/CHANGELOG.md +260 -1
  2. package/README.md +232 -312
  3. package/SECURITY.md +37 -59
  4. package/bin/graph-indexer.mjs +251 -0
  5. package/integrations/claude-code/.claude-plugin/plugin.json +8 -0
  6. package/integrations/claude-code/.mcp.json +8 -0
  7. package/integrations/claude-code/agents/code-structure.md +22 -0
  8. package/integrations/claude-code/hooks/gi-hook.mjs +99 -0
  9. package/integrations/claude-code/hooks/hooks.json +16 -0
  10. package/integrations/opencode/graph-indexer.js +124 -0
  11. package/package.json +32 -109
  12. package/src/cli/helper.mjs +67 -0
  13. package/src/cli/hook.mjs +356 -0
  14. package/src/cli/init.mjs +255 -0
  15. package/src/cli/resident.mjs +175 -0
  16. package/src/index/discover.mjs +130 -0
  17. package/src/index/fingerprint.mjs +30 -0
  18. package/src/index/graph.mjs +61 -0
  19. package/src/index/indexer.mjs +476 -0
  20. package/src/index/modules.mjs +331 -0
  21. package/src/index/resolver.mjs +954 -0
  22. package/src/mcp/render.mjs +60 -0
  23. package/src/mcp/server.mjs +147 -0
  24. package/src/mcp/tools.mjs +973 -0
  25. package/src/parse/arity.mjs +152 -0
  26. package/src/parse/extract.mjs +1031 -0
  27. package/src/parse/lang/csharp.mjs +106 -0
  28. package/src/parse/lang/ecma.mjs +438 -0
  29. package/src/parse/lang/go.mjs +194 -0
  30. package/src/parse/lang/jvm.mjs +118 -0
  31. package/src/parse/lang/kotlin.mjs +97 -0
  32. package/src/parse/lang/misc.mjs +115 -0
  33. package/src/parse/lang/native.mjs +126 -0
  34. package/src/parse/lang/python.mjs +153 -0
  35. package/src/parse/lang/rust.mjs +157 -0
  36. package/src/parse/lang/scripting.mjs +219 -0
  37. package/src/parse/languages.mjs +48 -0
  38. package/src/parse/runtime.mjs +56 -0
  39. package/src/query/check.mjs +343 -0
  40. package/src/query/facts.mjs +323 -0
  41. package/src/query/intel.mjs +680 -0
  42. package/src/query/read.mjs +81 -0
  43. package/src/query/testcmd.mjs +119 -0
  44. package/src/query/textsearch.mjs +160 -0
  45. package/src/search/search.mjs +210 -0
  46. package/src/search/tokenize.mjs +188 -0
  47. package/src/store/db.mjs +169 -0
  48. package/src/util/paths.mjs +24 -0
  49. package/vendor/grammars/bash.wasm.gz +0 -0
  50. package/vendor/grammars/c.wasm.gz +0 -0
  51. package/vendor/grammars/c_sharp.wasm.gz +0 -0
  52. package/vendor/grammars/cpp.wasm.gz +0 -0
  53. package/vendor/grammars/css.wasm.gz +0 -0
  54. package/vendor/grammars/go.wasm.gz +0 -0
  55. package/vendor/grammars/java.wasm.gz +0 -0
  56. package/vendor/grammars/javascript.wasm.gz +0 -0
  57. package/vendor/grammars/kotlin.wasm.gz +0 -0
  58. package/vendor/grammars/manifest.json +148 -0
  59. package/vendor/grammars/php.wasm.gz +0 -0
  60. package/vendor/grammars/python.wasm.gz +0 -0
  61. package/vendor/grammars/ruby.wasm.gz +0 -0
  62. package/vendor/grammars/rust.wasm.gz +0 -0
  63. package/vendor/grammars/scala.wasm.gz +0 -0
  64. package/vendor/grammars/tsx.wasm.gz +0 -0
  65. package/vendor/grammars/typescript.wasm.gz +0 -0
  66. package/vendor/web-tree-sitter/web-tree-sitter.js +3980 -0
  67. package/vendor/web-tree-sitter/web-tree-sitter.wasm +0 -0
  68. package/cli-ui.mjs +0 -78
  69. package/colbert.mjs +0 -168
  70. package/config.mjs +0 -494
  71. package/daemon-ctl.mjs +0 -175
  72. package/daemon-lock.mjs +0 -69
  73. package/embedders/python/mlx_embed_server.py +0 -61
  74. package/embedders/python/requirements-mlx.txt +0 -5
  75. package/embedders/setup-mlx.mjs +0 -110
  76. package/embeddings.mjs +0 -571
  77. package/engine/binary.mjs +0 -437
  78. package/engine/memory.mjs +0 -1003
  79. package/engine/sqlite.mjs +0 -1136
  80. package/enrichment.mjs +0 -521
  81. package/git-signals.mjs +0 -248
  82. package/indexer.mjs +0 -546
  83. package/init.mjs +0 -1809
  84. package/layout.mjs +0 -123
  85. package/mcp/centrality.mjs +0 -117
  86. package/mcp/format.mjs +0 -61
  87. package/mcp/resolver.mjs +0 -116
  88. package/mcp/symbolgraph.mjs +0 -137
  89. package/mcp/taint.mjs +0 -225
  90. package/mcp/tools.mjs +0 -1582
  91. package/mcp/topology.mjs +0 -676
  92. package/mcp-server.mjs +0 -197
  93. package/parse/extractor.mjs +0 -628
  94. package/parse/imports.mjs +0 -365
  95. package/parse/interprocedural.mjs +0 -134
  96. package/parse/languages.mjs +0 -253
  97. package/parse/metadata.mjs +0 -701
  98. package/parse/routes.mjs +0 -327
  99. package/parse/scip.mjs +0 -391
  100. package/parse/taint-patterns.mjs +0 -119
  101. package/prompts/CORE.md +0 -91
  102. package/prompts/DOMAIN_TEMPLATE.md +0 -46
  103. package/prompts/INTEGRATION.md +0 -123
  104. package/prompts/frameworks/ANDROID.md +0 -17
  105. package/prompts/frameworks/ASPNET_CORE.md +0 -19
  106. package/prompts/frameworks/FASTAPI_DJANGO.md +0 -20
  107. package/prompts/frameworks/LARAVEL_SYMFONY.md +0 -22
  108. package/prompts/frameworks/NODE_EXPRESS_NESTJS.md +0 -19
  109. package/prompts/frameworks/RAILS.md +0 -20
  110. package/prompts/frameworks/REACT.md +0 -20
  111. package/prompts/frameworks/SPRING_BOOT.md +0 -20
  112. package/prompts/languages/BASH.md +0 -22
  113. package/prompts/languages/C.md +0 -21
  114. package/prompts/languages/CSHARP.md +0 -24
  115. package/prompts/languages/CSS_SCSS.md +0 -22
  116. package/prompts/languages/GO.md +0 -24
  117. package/prompts/languages/JAVA.md +0 -23
  118. package/prompts/languages/JAVASCRIPT_TYPESCRIPT.md +0 -24
  119. package/prompts/languages/KOTLIN.md +0 -24
  120. package/prompts/languages/PHP.md +0 -23
  121. package/prompts/languages/PYTHON.md +0 -23
  122. package/prompts/languages/RUBY.md +0 -23
  123. package/prompts/languages/RUST.md +0 -21
  124. package/prompts/languages/SWIFT.md +0 -21
  125. package/seal.mjs +0 -400
  126. package/search-core.mjs +0 -777
  127. package/search-sparse.mjs +0 -183
  128. package/storage.mjs +0 -73
  129. 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
- }
@@ -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
- }
@@ -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
- }