graph-indexer 2.1.0 → 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 +31 -105
- 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/scip.mjs
DELETED
|
@@ -1,391 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file parse/scip.mjs
|
|
3
|
-
* @description A2 — SCIP ingestion for the precise resolver provider. Reads a LOCALLY-generated
|
|
4
|
-
* SCIP index (Sourcegraph's protocol; produced out-of-band by the user's own
|
|
5
|
-
* air-gapped toolchain — `scip-typescript`, `scip-python`, `scip-java`,
|
|
6
|
-
* `rust-analyzer --scip`) and aligns its symbol OCCURRENCES to our AST chunks by
|
|
7
|
-
* (file, line), yielding a genuinely cross-file binding relation:
|
|
8
|
-
*
|
|
9
|
-
* bindings: Map<fromChunkId, Set<defChunkId>>
|
|
10
|
-
*
|
|
11
|
-
* i.e. "chunk F references a symbol whose definition lives in chunk D." The `scip`
|
|
12
|
-
* resolver (mcp/resolver.mjs) consumes this to (a) PROMOTE a heuristic edge to the
|
|
13
|
-
* `resolved` tier when SCIP confirms it, and (b) SUPPRESS the wrong-target edges a
|
|
14
|
-
* name-only fan-out would otherwise emit — the precision A1's heuristic cannot reach.
|
|
15
|
-
*
|
|
16
|
-
* ZERO new runtime dependency: the `.scip` protobuf is decoded by a minimal,
|
|
17
|
-
* hand-rolled wire reader (only the four fields A2 needs — Index.documents,
|
|
18
|
-
* Document.relative_path / .occurrences, Occurrence.range / .symbol / .symbol_roles).
|
|
19
|
-
* A SCIP-shaped JSON file is also accepted (handy for hand-authored fixtures and for
|
|
20
|
-
* `scip print`-style dumps). Fully offline; pure parsing + graph alignment.
|
|
21
|
-
*
|
|
22
|
-
* HONEST SCOPE: SCIP coverage is only as good as the indexer that produced the file
|
|
23
|
-
* and the (file, range) alignment to our chunks. Suppression is SOUND under partial
|
|
24
|
-
* coverage — a candidate def is dropped only if SCIP actually recorded it as a
|
|
25
|
-
* definition (`definedChunks`); a def SCIP never saw falls through to heuristic. v1
|
|
26
|
-
* applies SCIP resolution to `calls` edges only (the binding relation is kind-agnostic,
|
|
27
|
-
* so `extends`/`type` edges stay heuristic to avoid cross-kind contamination).
|
|
28
|
-
*
|
|
29
|
-
* A2 v2 ADDS a second, kind-agnostic consumer that the same binding relation serves
|
|
30
|
-
* soundly precisely BECAUSE it is kind-agnostic: `buildScipReferers` inverts the
|
|
31
|
-
* bindings into a precise cross-file "referenced-by" map for find_references — no edge
|
|
32
|
-
* suppression, no cross-kind risk, just the binding-precise reference set serialized as
|
|
33
|
-
* an isolated index artifact (like centrality/taint), so it cannot perturb A4 edges or
|
|
34
|
-
* A5 centrality.
|
|
35
|
-
* @author MaquinaTech <https://github.com/MaquinaTech>
|
|
36
|
-
* @copyright (c) 2026 MaquinaTech. All rights reserved.
|
|
37
|
-
* @license MIT
|
|
38
|
-
*/
|
|
39
|
-
import fs from 'node:fs';
|
|
40
|
-
|
|
41
|
-
// ── SCIP schema field numbers (scip.proto) — only what A2 reads ─────────────────────────────
|
|
42
|
-
// Index.documents = 2 (repeated Document)
|
|
43
|
-
// Document.relative_path = 1 (string)
|
|
44
|
-
// Document.occurrences = 2 (repeated Occurrence)
|
|
45
|
-
// Occurrence.range = 1 (repeated int32, packed)
|
|
46
|
-
// Occurrence.symbol = 2 (string)
|
|
47
|
-
// Occurrence.symbol_roles = 3 (int32 bitset; Definition = 0x1)
|
|
48
|
-
const F_INDEX_DOCUMENTS = 2;
|
|
49
|
-
const F_DOC_RELATIVE_PATH = 1;
|
|
50
|
-
const F_DOC_OCCURRENCES = 2;
|
|
51
|
-
const F_OCC_RANGE = 1;
|
|
52
|
-
const F_OCC_SYMBOL = 2;
|
|
53
|
-
const F_OCC_SYMBOL_ROLES = 3;
|
|
54
|
-
const ROLE_DEFINITION = 0x1;
|
|
55
|
-
|
|
56
|
-
// ── Minimal protobuf wire-format reader (zero deps) ─────────────────────────────────────────
|
|
57
|
-
|
|
58
|
-
/**
|
|
59
|
-
* Read a base-128 varint as a JS Number. Uses multiplication (not bit-shift) so values up to
|
|
60
|
-
* 2^53 decode correctly — proto lengths / line numbers never exceed that in practice.
|
|
61
|
-
* @returns {[number, number]} [value, nextPos]
|
|
62
|
-
*/
|
|
63
|
-
function readVarint(buf, pos) {
|
|
64
|
-
let result = 0;
|
|
65
|
-
let mul = 1;
|
|
66
|
-
let p = pos;
|
|
67
|
-
for (;;) {
|
|
68
|
-
if (p >= buf.length) throw new Error('SCIP: truncated varint');
|
|
69
|
-
const b = buf[p++];
|
|
70
|
-
result += (b & 0x7f) * mul;
|
|
71
|
-
if ((b & 0x80) === 0) break;
|
|
72
|
-
mul *= 128;
|
|
73
|
-
}
|
|
74
|
-
return [result, p];
|
|
75
|
-
}
|
|
76
|
-
|
|
77
|
-
/**
|
|
78
|
-
* Iterate the fields of one protobuf message region [start, end). Yields, per field, either a
|
|
79
|
-
* scalar `{ field, wire, value }` (varint / fixed) or a length-delimited `{ field, wire, start,
|
|
80
|
-
* end }` slice. Unknown fields are yielded too (callers skip what they don't recognise).
|
|
81
|
-
*/
|
|
82
|
-
function* iterFields(buf, start, end) {
|
|
83
|
-
let p = start;
|
|
84
|
-
while (p < end) {
|
|
85
|
-
let tag;
|
|
86
|
-
[tag, p] = readVarint(buf, p);
|
|
87
|
-
const field = Math.floor(tag / 8);
|
|
88
|
-
const wire = tag % 8;
|
|
89
|
-
if (wire === 0) {
|
|
90
|
-
let value;
|
|
91
|
-
[value, p] = readVarint(buf, p);
|
|
92
|
-
yield { field, wire, value };
|
|
93
|
-
} else if (wire === 2) {
|
|
94
|
-
let len;
|
|
95
|
-
[len, p] = readVarint(buf, p);
|
|
96
|
-
const s = p;
|
|
97
|
-
p += len;
|
|
98
|
-
if (p > end) throw new Error('SCIP: length-delimited field overruns message');
|
|
99
|
-
yield { field, wire, start: s, end: p };
|
|
100
|
-
} else if (wire === 5) {
|
|
101
|
-
if (p + 4 > end) throw new Error('SCIP: fixed32 field overruns message');
|
|
102
|
-
const value = buf.readUInt32LE(p);
|
|
103
|
-
p += 4;
|
|
104
|
-
yield { field, wire, value };
|
|
105
|
-
} else if (wire === 1) {
|
|
106
|
-
if (p + 8 > end) throw new Error('SCIP: fixed64 field overruns message');
|
|
107
|
-
const lo = buf.readUInt32LE(p);
|
|
108
|
-
const hi = buf.readUInt32LE(p + 4);
|
|
109
|
-
p += 8;
|
|
110
|
-
yield { field, wire, value: hi * 4294967296 + lo };
|
|
111
|
-
} else {
|
|
112
|
-
throw new Error(`SCIP: unsupported wire type ${wire}`);
|
|
113
|
-
}
|
|
114
|
-
}
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
/** Read all packed varints in [start, end) into an array (Occurrence.range is packed int32). */
|
|
118
|
-
function readPackedVarints(buf, start, end) {
|
|
119
|
-
const out = [];
|
|
120
|
-
let p = start;
|
|
121
|
-
while (p < end) {
|
|
122
|
-
let v;
|
|
123
|
-
[v, p] = readVarint(buf, p);
|
|
124
|
-
out.push(v);
|
|
125
|
-
}
|
|
126
|
-
return out;
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
/**
|
|
130
|
-
* SCIP `range` → 0-based start line. Length-3 ranges are [startLine, startChar, endChar]
|
|
131
|
-
* (single line); length-4 are [startLine, startChar, endLine, endChar]. We key alignment on the
|
|
132
|
-
* start line only.
|
|
133
|
-
*/
|
|
134
|
-
function rangeStartLine(range) {
|
|
135
|
-
return Array.isArray(range) && range.length >= 1 ? range[0] : null;
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
function parseOccurrence(buf, start, end) {
|
|
139
|
-
let range = [];
|
|
140
|
-
let symbol = '';
|
|
141
|
-
let roles = 0;
|
|
142
|
-
for (const f of iterFields(buf, start, end)) {
|
|
143
|
-
if (f.field === F_OCC_RANGE) {
|
|
144
|
-
if (f.wire === 2) range = range.concat(readPackedVarints(buf, f.start, f.end)); // packed
|
|
145
|
-
else if (f.wire === 0) range.push(f.value); // unpacked fallback
|
|
146
|
-
} else if (f.field === F_OCC_SYMBOL && f.wire === 2) {
|
|
147
|
-
symbol = buf.toString('utf8', f.start, f.end);
|
|
148
|
-
} else if (f.field === F_OCC_SYMBOL_ROLES && f.wire === 0) {
|
|
149
|
-
roles = f.value;
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
const startLine = rangeStartLine(range);
|
|
153
|
-
if (startLine === null || !symbol) return null;
|
|
154
|
-
return { startLine, symbol, isDefinition: (roles & ROLE_DEFINITION) === ROLE_DEFINITION };
|
|
155
|
-
}
|
|
156
|
-
|
|
157
|
-
function parseDocument(buf, start, end) {
|
|
158
|
-
let relativePath = '';
|
|
159
|
-
const occurrences = [];
|
|
160
|
-
for (const f of iterFields(buf, start, end)) {
|
|
161
|
-
if (f.field === F_DOC_RELATIVE_PATH && f.wire === 2) {
|
|
162
|
-
relativePath = buf.toString('utf8', f.start, f.end);
|
|
163
|
-
} else if (f.field === F_DOC_OCCURRENCES && f.wire === 2) {
|
|
164
|
-
const occ = parseOccurrence(buf, f.start, f.end);
|
|
165
|
-
if (occ) occurrences.push(occ);
|
|
166
|
-
}
|
|
167
|
-
}
|
|
168
|
-
return { relativePath, occurrences };
|
|
169
|
-
}
|
|
170
|
-
|
|
171
|
-
/** Decode a `.scip` protobuf buffer into `{ documents: [{ relativePath, occurrences }] }`. */
|
|
172
|
-
function decodeScipProtobuf(buf) {
|
|
173
|
-
const documents = [];
|
|
174
|
-
for (const f of iterFields(buf, 0, buf.length)) {
|
|
175
|
-
if (f.field === F_INDEX_DOCUMENTS && f.wire === 2) {
|
|
176
|
-
documents.push(parseDocument(buf, f.start, f.end));
|
|
177
|
-
}
|
|
178
|
-
}
|
|
179
|
-
return { documents };
|
|
180
|
-
}
|
|
181
|
-
|
|
182
|
-
// ── JSON-shaped SCIP (convenience: hand-authored fixtures, `scip print` dumps) ──────────────
|
|
183
|
-
|
|
184
|
-
function normalizeJsonScip(obj) {
|
|
185
|
-
const rawDocs = obj.documents || obj.Documents || [];
|
|
186
|
-
const documents = [];
|
|
187
|
-
for (const d of rawDocs) {
|
|
188
|
-
const relativePath = d.relative_path || d.relativePath || d.path || '';
|
|
189
|
-
const occurrences = [];
|
|
190
|
-
for (const o of (d.occurrences || d.Occurrences || [])) {
|
|
191
|
-
const range = o.range || o.Range || [];
|
|
192
|
-
const symbol = o.symbol || o.Symbol || '';
|
|
193
|
-
const roles = o.symbol_roles || o.symbolRoles || o.roles || 0;
|
|
194
|
-
const startLine = rangeStartLine(range);
|
|
195
|
-
if (startLine === null || !symbol) continue;
|
|
196
|
-
occurrences.push({ startLine, symbol, isDefinition: (roles & ROLE_DEFINITION) === ROLE_DEFINITION });
|
|
197
|
-
}
|
|
198
|
-
documents.push({ relativePath, occurrences });
|
|
199
|
-
}
|
|
200
|
-
return { documents };
|
|
201
|
-
}
|
|
202
|
-
|
|
203
|
-
/**
|
|
204
|
-
* Load a SCIP index from disk. Accepts the `.scip` protobuf OR a SCIP-shaped JSON file
|
|
205
|
-
* (auto-detected: a leading `{` / `.json` extension → JSON). Returns the decoded documents and a
|
|
206
|
-
* loud-on-failure shape (throws with an actionable message; the caller decides whether a missing
|
|
207
|
-
* file is fatal or a warning).
|
|
208
|
-
*
|
|
209
|
-
* @param {string} filePath
|
|
210
|
-
* @returns {{ documents: Array<{ relativePath: string, occurrences: Array<{ startLine:number, symbol:string, isDefinition:boolean }> }> }}
|
|
211
|
-
*/
|
|
212
|
-
export function loadScip(filePath) {
|
|
213
|
-
let buf;
|
|
214
|
-
try {
|
|
215
|
-
buf = fs.readFileSync(filePath);
|
|
216
|
-
} catch (e) {
|
|
217
|
-
throw new Error(`SCIP index not readable at ${filePath}: ${e.message}`);
|
|
218
|
-
}
|
|
219
|
-
const looksJson = filePath.endsWith('.json') || (buf.length > 0 && (buf[0] === 0x7b /* { */ || buf[0] === 0x5b /* [ */));
|
|
220
|
-
if (looksJson) {
|
|
221
|
-
try {
|
|
222
|
-
return normalizeJsonScip(JSON.parse(buf.toString('utf8')));
|
|
223
|
-
} catch (e) {
|
|
224
|
-
throw new Error(`SCIP JSON parse failed at ${filePath}: ${e.message}`);
|
|
225
|
-
}
|
|
226
|
-
}
|
|
227
|
-
try {
|
|
228
|
-
return decodeScipProtobuf(buf);
|
|
229
|
-
} catch (e) {
|
|
230
|
-
throw new Error(`SCIP protobuf decode failed at ${filePath}: ${e.message}`);
|
|
231
|
-
}
|
|
232
|
-
}
|
|
233
|
-
|
|
234
|
-
// ── Alignment: SCIP occurrences → our chunk ids ─────────────────────────────────────────────
|
|
235
|
-
|
|
236
|
-
/** Normalize a SCIP document path to our chunk `file_path` form: repo-root-relative, POSIX, no `./`. */
|
|
237
|
-
export function normalizeScipPath(p) {
|
|
238
|
-
if (!p) return '';
|
|
239
|
-
let s = String(p).replace(/\\/g, '/');
|
|
240
|
-
s = s.replace(/^file:\/\//, ''); // tolerate a file:// URI prefix
|
|
241
|
-
s = s.replace(/^\.?\//, ''); // strip a leading './' or '/'
|
|
242
|
-
return s;
|
|
243
|
-
}
|
|
244
|
-
|
|
245
|
-
function addToSetMap(map, key, value) {
|
|
246
|
-
let s = map.get(key);
|
|
247
|
-
if (!s) { s = new Set(); map.set(key, s); }
|
|
248
|
-
s.add(value);
|
|
249
|
-
}
|
|
250
|
-
|
|
251
|
-
/**
|
|
252
|
-
* Find the MOST SPECIFIC chunk (smallest line span) whose 1-based [start_line, end_line] range
|
|
253
|
-
* contains `line1`. Deterministic tie-break: larger start_line (innermost), then smaller id.
|
|
254
|
-
* `arr` is the file's chunk list (any order — we scan it fully; files have few chunks).
|
|
255
|
-
*/
|
|
256
|
-
function chunkContaining(arr, line1) {
|
|
257
|
-
let best = null;
|
|
258
|
-
let bestSpan = Infinity;
|
|
259
|
-
for (const c of arr) {
|
|
260
|
-
if (c.start_line <= line1 && line1 <= c.end_line) {
|
|
261
|
-
const span = c.end_line - c.start_line;
|
|
262
|
-
if (span < bestSpan
|
|
263
|
-
|| (span === bestSpan && best && (c.start_line > best.start_line
|
|
264
|
-
|| (c.start_line === best.start_line && c.id < best.id)))) {
|
|
265
|
-
best = c;
|
|
266
|
-
bestSpan = span;
|
|
267
|
-
}
|
|
268
|
-
}
|
|
269
|
-
}
|
|
270
|
-
return best;
|
|
271
|
-
}
|
|
272
|
-
|
|
273
|
-
/**
|
|
274
|
-
* Align a decoded SCIP index to a loaded store's chunks and derive the cross-file binding
|
|
275
|
-
* relation the `scip` resolver consumes:
|
|
276
|
-
*
|
|
277
|
-
* bindings: Map<fromChunkId, Set<defChunkId>> // "chunk F references a symbol defined in D"
|
|
278
|
-
*
|
|
279
|
-
* Built by: (1) bucket chunks by file; (2) for each SCIP occurrence, locate its containing chunk
|
|
280
|
-
* by 1-based line range (SCIP lines are 0-based → +1); (3) split occurrences into per-symbol
|
|
281
|
-
* DEFINITIONS (symbol_roles & Definition) and per-chunk REFERENCES; (4) join references to the
|
|
282
|
-
* chunk(s) that define each referenced symbol. Self-references are dropped (a def referencing
|
|
283
|
-
* itself is not an inbound edge). Fully deterministic; pure graph alignment, no network.
|
|
284
|
-
*
|
|
285
|
-
* Also returns `definedChunks` — every chunk SCIP recorded a DEFINITION occurrence for. This is the
|
|
286
|
-
* SOUNDNESS gate for suppression: a candidate def may be dropped as a wrong-target only if SCIP
|
|
287
|
-
* actually *saw* it as a definition (`def ∈ definedChunks`). A def SCIP never covered (an uncovered
|
|
288
|
-
* file in a polyglot / partial run, a stale `.scip`, a missed occurrence) is NEVER suppressed — it
|
|
289
|
-
* falls through to heuristic confidence, honouring "files the SCIP index does not cover are never
|
|
290
|
-
* worse" even when the *caller* is covered but the *def* is not.
|
|
291
|
-
*
|
|
292
|
-
* @param {object} db A loaded store exposing iterateChunks() (chunks carry file_path/start_line/end_line/id).
|
|
293
|
-
* @param {{documents: Array}} scip The result of loadScip().
|
|
294
|
-
* @returns {{ bindings: Map<string, Set<string>>, definedChunks: Set<string>, stats: object }}
|
|
295
|
-
*/
|
|
296
|
-
export function buildScipBindings(db, scip) {
|
|
297
|
-
const documents = (scip && scip.documents) || [];
|
|
298
|
-
|
|
299
|
-
// (1) one pass over chunks → Map<file_path, chunk[]>
|
|
300
|
-
const byFile = new Map();
|
|
301
|
-
for (const c of db.iterateChunks()) {
|
|
302
|
-
let arr = byFile.get(c.file_path);
|
|
303
|
-
if (!arr) { arr = []; byFile.set(c.file_path, arr); }
|
|
304
|
-
arr.push(c);
|
|
305
|
-
}
|
|
306
|
-
|
|
307
|
-
const defChunkBySymbol = new Map(); // symbol → Set<chunkId> (definition occurrences)
|
|
308
|
-
const refSymbolsByChunk = new Map(); // chunkId → Set<symbol> (reference occurrences)
|
|
309
|
-
let matchedDocs = 0;
|
|
310
|
-
let placedOcc = 0;
|
|
311
|
-
let totalOcc = 0;
|
|
312
|
-
|
|
313
|
-
// (2)+(3) place occurrences into chunks
|
|
314
|
-
for (const doc of documents) {
|
|
315
|
-
const fp = normalizeScipPath(doc.relativePath);
|
|
316
|
-
const arr = byFile.get(fp);
|
|
317
|
-
if (!arr) continue; // SCIP doc not in our index (path mismatch or uncovered)
|
|
318
|
-
matchedDocs++;
|
|
319
|
-
for (const occ of (doc.occurrences || [])) {
|
|
320
|
-
totalOcc++;
|
|
321
|
-
const line1 = occ.startLine + 1; // SCIP 0-based → our 1-based
|
|
322
|
-
const c = chunkContaining(arr, line1);
|
|
323
|
-
if (!c) continue;
|
|
324
|
-
placedOcc++;
|
|
325
|
-
if (occ.isDefinition) addToSetMap(defChunkBySymbol, occ.symbol, c.id);
|
|
326
|
-
else addToSetMap(refSymbolsByChunk, c.id, occ.symbol);
|
|
327
|
-
}
|
|
328
|
-
}
|
|
329
|
-
|
|
330
|
-
// (4) derive fromChunk → Set<defChunk>
|
|
331
|
-
const bindings = new Map();
|
|
332
|
-
let pairCount = 0;
|
|
333
|
-
for (const [fromId, symbols] of refSymbolsByChunk) {
|
|
334
|
-
for (const sym of symbols) {
|
|
335
|
-
const defChunks = defChunkBySymbol.get(sym);
|
|
336
|
-
if (!defChunks) continue;
|
|
337
|
-
for (const defId of defChunks) {
|
|
338
|
-
if (defId === fromId) continue; // self-reference is not an inbound edge
|
|
339
|
-
const before = bindings.get(fromId)?.size || 0;
|
|
340
|
-
addToSetMap(bindings, fromId, defId);
|
|
341
|
-
if ((bindings.get(fromId)?.size || 0) > before) pairCount++;
|
|
342
|
-
}
|
|
343
|
-
}
|
|
344
|
-
}
|
|
345
|
-
|
|
346
|
-
// The set of chunks SCIP recorded as a definition — the suppression soundness gate.
|
|
347
|
-
const definedChunks = new Set();
|
|
348
|
-
for (const ids of defChunkBySymbol.values()) for (const id of ids) definedChunks.add(id);
|
|
349
|
-
|
|
350
|
-
const stats = {
|
|
351
|
-
docs: documents.length,
|
|
352
|
-
matchedDocs,
|
|
353
|
-
occurrences: totalOcc,
|
|
354
|
-
placedOccurrences: placedOcc,
|
|
355
|
-
definedSymbols: defChunkBySymbol.size,
|
|
356
|
-
definedChunks: definedChunks.size,
|
|
357
|
-
boundChunks: bindings.size,
|
|
358
|
-
bindingPairs: pairCount,
|
|
359
|
-
};
|
|
360
|
-
return { bindings, definedChunks, stats };
|
|
361
|
-
}
|
|
362
|
-
|
|
363
|
-
/**
|
|
364
|
-
* A2 v2 — invert the cross-file binding relation into the precise "referenced-by" map that
|
|
365
|
-
* find_references consumes:
|
|
366
|
-
*
|
|
367
|
-
* scipRefs: { defChunkId: [refererChunkId, ...] } // "this definition is referenced by …"
|
|
368
|
-
*
|
|
369
|
-
* SCIP occurrences are kind-AGNOSTIC — a Reference occurrence is "symbol X is used here," with no
|
|
370
|
-
* call/type-use/inheritance distinction — which is exactly the question find_references answers
|
|
371
|
-
* ("what references this symbol?"). So the inverse of `bindings` is a genuinely cross-file,
|
|
372
|
-
* binding-precise reference set: it (a) DISAMBIGUATES same-named symbols (a referer is attached to
|
|
373
|
-
* the one definition SCIP bound it to, not every same-named def the name heuristic would match), and
|
|
374
|
-
* (b) RECOVERS references the AST/name heuristic misses entirely (the v1 "recall concern"), e.g.
|
|
375
|
-
* type usages in languages whose `type_refs` channel is empty. Serialized deterministically (sorted
|
|
376
|
-
* def ids, sorted referer ids) so both backends round-trip byte-identically. Self-references were
|
|
377
|
-
* already dropped upstream in buildScipBindings.
|
|
378
|
-
*
|
|
379
|
-
* @param {Map<string, Set<string>>} bindings fromChunkId → set of def chunk ids (buildScipBindings).
|
|
380
|
-
* @returns {Record<string, string[]>} defChunkId → sorted referer chunk ids (empty object when none).
|
|
381
|
-
*/
|
|
382
|
-
export function buildScipReferers(bindings) {
|
|
383
|
-
const rel = bindings instanceof Map ? bindings : new Map();
|
|
384
|
-
const inv = new Map(); // defChunkId → Set<refererChunkId>
|
|
385
|
-
for (const [refererId, defIds] of rel) {
|
|
386
|
-
for (const defId of defIds) addToSetMap(inv, defId, refererId);
|
|
387
|
-
}
|
|
388
|
-
const out = {};
|
|
389
|
-
for (const defId of [...inv.keys()].sort()) out[defId] = [...inv.get(defId)].sort();
|
|
390
|
-
return out;
|
|
391
|
-
}
|
package/parse/taint-patterns.mjs
DELETED
|
@@ -1,119 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* @file parse/taint-patterns.mjs
|
|
3
|
-
* @description Source / sink / sanitizer catalogs for taint analysis (C2). Heuristic, line-scannable
|
|
4
|
-
* regexes per language family — the basis for a *finder*, not a verifier (see
|
|
5
|
-
* mcp/taint.mjs and docs/internals/PHASE3_TAINT_ANALYSIS.md for the honesty caveats).
|
|
6
|
-
*
|
|
7
|
-
* SOURCES = untrusted input (request bodies, argv/env, stdin, file/socket reads).
|
|
8
|
-
* SINKS = dangerous operations, grouped by category (rce | sqli | xss | path | ssrf).
|
|
9
|
-
* SANITIZERS = constructs that neutralise taint (escape/encode/parameterise/validate).
|
|
10
|
-
*
|
|
11
|
-
* Covers the JS/TS family ('js'), Python ('py'), Java ('java'), and Go ('go'); other
|
|
12
|
-
* languages return no patterns (the analysis simply finds nothing for them).
|
|
13
|
-
* @author MaquinaTech <https://github.com/MaquinaTech>
|
|
14
|
-
* @copyright (c) 2026 MaquinaTech. All rights reserved.
|
|
15
|
-
* @license MIT
|
|
16
|
-
*/
|
|
17
|
-
|
|
18
|
-
/** Normalise a file extension to a pattern-catalog key, or null if unsupported. */
|
|
19
|
-
export function langKeyForExt(ext) {
|
|
20
|
-
if (['.js', '.jsx', '.mjs', '.cjs', '.ts', '.tsx'].includes(ext)) return 'js';
|
|
21
|
-
if (ext === '.py') return 'py';
|
|
22
|
-
if (ext === '.java') return 'java';
|
|
23
|
-
if (ext === '.go') return 'go';
|
|
24
|
-
return null;
|
|
25
|
-
}
|
|
26
|
-
|
|
27
|
-
/** Untrusted-input sources: { re, kind }. */
|
|
28
|
-
export const SOURCES = {
|
|
29
|
-
js: [
|
|
30
|
-
{ re: /\breq(uest)?\.(body|query|params|cookies|headers|url|originalUrl)\b/, kind: 'http-request' },
|
|
31
|
-
{ re: /\bctx\.(request|query|params|body)\b/, kind: 'http-request' }, // Koa
|
|
32
|
-
{ re: /\bprocess\.(argv|env)\b/, kind: 'process-input' },
|
|
33
|
-
{ re: /\.on\s*\(\s*['"]data['"]/, kind: 'socket-data' },
|
|
34
|
-
{ re: /\b(location\.(search|hash|href)|document\.(URL|cookie|referrer))\b/, kind: 'dom-input' },
|
|
35
|
-
],
|
|
36
|
-
py: [
|
|
37
|
-
{ re: /\brequest\.(GET|POST|data|args|form|json|values|cookies|files|headers)\b/, kind: 'http-request' },
|
|
38
|
-
{ re: /\b(sys\.argv|os\.environ)\b/, kind: 'process-input' },
|
|
39
|
-
{ re: /\binput\s*\(/, kind: 'stdin' },
|
|
40
|
-
{ re: /\bflask\.request\b|\bself\.get_argument\s*\(/, kind: 'http-request' },
|
|
41
|
-
],
|
|
42
|
-
java: [
|
|
43
|
-
{ re: /\b(request|req)\.(getParameter|getParameterValues|getParameterMap|getHeader|getHeaders|getQueryString|getInputStream|getReader|getCookies)\s*\(/, kind: 'http-request' },
|
|
44
|
-
{ re: /@(RequestParam|PathVariable|RequestBody|RequestHeader|CookieValue|MatrixVariable|ModelAttribute)\b/, kind: 'http-request' },
|
|
45
|
-
{ re: /\bSystem\.(getenv|getProperty)\s*\(/, kind: 'process-input' },
|
|
46
|
-
{ re: /\bnew\s+Scanner\s*\(\s*System\.in\b/, kind: 'stdin' },
|
|
47
|
-
],
|
|
48
|
-
go: [
|
|
49
|
-
{ re: /\br\.(URL\.Query\s*\(\s*\)|FormValue|PostFormValue|Form\b|PostForm\b|MultipartForm)|\br\.Header\.Get\s*\(|\br\.Cookie\s*\(/, kind: 'http-request' },
|
|
50
|
-
{ re: /\b(mux\.Vars|c\.(Param|Query|PostForm|DefaultQuery)|ctx\.(Query|Param))\s*\(/, kind: 'http-request' },
|
|
51
|
-
{ re: /\b(os\.Args|os\.Getenv\s*\(|flag\.(String|Int|Bool|Parse)\s*\()/, kind: 'process-input' },
|
|
52
|
-
{ re: /\bbufio\.NewReader\s*\(\s*os\.Stdin\b|\bfmt\.Scan\w*\s*\(/, kind: 'stdin' },
|
|
53
|
-
],
|
|
54
|
-
};
|
|
55
|
-
|
|
56
|
-
/** Dangerous sinks: { re, category, label }. Categories: rce | sqli | xss | path | ssrf. */
|
|
57
|
-
export const SINKS = {
|
|
58
|
-
js: [
|
|
59
|
-
{ re: /\beval\s*\(/, category: 'rce', label: 'eval()' },
|
|
60
|
-
{ re: /\bnew\s+Function\s*\(/, category: 'rce', label: 'new Function()' },
|
|
61
|
-
{ re: /\b(child_process\b|\bexecSync\b|\bexecFile\b|\bexec\s*\(|\bspawn\s*\()/, category: 'rce', label: 'child_process exec/spawn' },
|
|
62
|
-
{ re: /\.(query|execute|raw)\s*\(/, category: 'sqli', label: 'db query/execute' },
|
|
63
|
-
{ re: /(\.innerHTML\s*=|dangerouslySetInnerHTML|\bres\.(send|write|end)\s*\()/, category: 'xss', label: 'html sink' },
|
|
64
|
-
{ re: /\b(fs\.(readFile|writeFile|unlink|createReadStream|createWriteStream|appendFile)(Sync)?|path\.join|res\.sendFile)\s*\(/, category: 'path', label: 'fs/path sink' },
|
|
65
|
-
{ re: /\b(fetch|axios|got|superagent)\s*\(|\bhttps?\.request\s*\(/, category: 'ssrf', label: 'outbound request' },
|
|
66
|
-
],
|
|
67
|
-
py: [
|
|
68
|
-
{ re: /\b(eval|exec)\s*\(/, category: 'rce', label: 'eval/exec' },
|
|
69
|
-
{ re: /\bos\.system\s*\(|\bsubprocess\.(call|run|Popen|check_output)\s*\(|\bos\.popen\s*\(/, category: 'rce', label: 'os.system/subprocess' },
|
|
70
|
-
{ re: /\.execute(many)?\s*\(|\bcursor\.execute\s*\(|\.raw\s*\(/, category: 'sqli', label: 'cursor.execute' },
|
|
71
|
-
{ re: /\b(mark_safe|render_template_string|HttpResponse)\s*\(/, category: 'xss', label: 'html sink' },
|
|
72
|
-
{ re: /\b(open|send_file|os\.path\.join)\s*\(/, category: 'path', label: 'open/send_file' },
|
|
73
|
-
{ re: /\brequests\.(get|post|put|delete|request)\s*\(|\burllib\.|\burlopen\s*\(/, category: 'ssrf', label: 'outbound request' },
|
|
74
|
-
],
|
|
75
|
-
java: [
|
|
76
|
-
{ re: /\bRuntime\.getRuntime\s*\(\s*\)\s*\.exec\s*\(|\bnew\s+ProcessBuilder\s*\(|\bProcessBuilder\s*\(/, category: 'rce', label: 'Runtime.exec/ProcessBuilder' },
|
|
77
|
-
{ re: /\.(executeQuery|executeUpdate|execute|createQuery|createNativeQuery|createStatement)\s*\(/, category: 'sqli', label: 'jdbc/jpa statement' },
|
|
78
|
-
{ re: /\b(getWriter\s*\(\s*\)\s*\.(print|println|write)|out\.(print|println)\s*\(|\.appendChild\s*\()/, category: 'xss', label: 'servlet/JSP writer' },
|
|
79
|
-
{ re: /\bnew\s+(File|FileInputStream|FileReader|FileOutputStream|FileWriter)\s*\(|\bPaths\.get\s*\(|\bFiles\.(read|write|newInputStream|copy)\w*\s*\(/, category: 'path', label: 'file/path sink' },
|
|
80
|
-
{ re: /\bnew\s+URL\s*\(|\.openConnection\s*\(|\b(RestTemplate|WebClient|HttpClient)\b|\bHttpRequest\.newBuilder\s*\(/, category: 'ssrf', label: 'outbound request' },
|
|
81
|
-
],
|
|
82
|
-
go: [
|
|
83
|
-
{ re: /\bexec\.Command(Context)?\s*\(/, category: 'rce', label: 'exec.Command' },
|
|
84
|
-
// The `.URL`-scoped lookbehind excludes only `x.URL.Query()` (net/url's parser) — a db handle
|
|
85
|
-
// like `dbURL.Query()` is still matched. Exec* needs no guard (url.URL has no Exec method).
|
|
86
|
-
{ re: /(?<!\.URL)\.(Query|QueryRow|QueryContext|QueryRowContext)\s*\(/, category: 'sqli', label: 'db query' },
|
|
87
|
-
{ re: /\.(Exec|ExecContext)\s*\(/, category: 'sqli', label: 'db exec' },
|
|
88
|
-
{ re: /\bw\.Write\s*\(|\bio\.WriteString\s*\(\s*w|\bfmt\.Fprint\w*\s*\(\s*w|\btemplate\.HTML\s*\(/, category: 'xss', label: 'response writer / template.HTML' },
|
|
89
|
-
{ re: /\b(os\.Open|os\.OpenFile|os\.ReadFile|ioutil\.ReadFile|os\.Create|filepath\.Join|http\.ServeFile)\s*\(/, category: 'path', label: 'file/path sink' },
|
|
90
|
-
{ re: /\bhttp\.(Get|Post|Head|PostForm|NewRequest|NewRequestWithContext)\s*\(|\b\w*[Cc]lient\.Do\s*\(/, category: 'ssrf', label: 'outbound request' },
|
|
91
|
-
],
|
|
92
|
-
};
|
|
93
|
-
|
|
94
|
-
/** Taint-clearing constructs: any match in/around the flow lowers a finding's confidence. */
|
|
95
|
-
export const SANITIZERS = {
|
|
96
|
-
js: [
|
|
97
|
-
/\b(escape|encodeURI|encodeURIComponent|sanitize|DOMPurify|validator|xss)\b/i,
|
|
98
|
-
/\b(parameteriz|prepared|placeholder)\b/i,
|
|
99
|
-
/\b(Number|parseInt|parseFloat)\s*\(/,
|
|
100
|
-
],
|
|
101
|
-
py: [
|
|
102
|
-
/\b(shlex\.quote|escape|bleach|markupsafe|secure_filename|quote)\b/i,
|
|
103
|
-
/\b(parameteriz|placeholder)\b/i,
|
|
104
|
-
/\bint\s*\(|\bfloat\s*\(/,
|
|
105
|
-
],
|
|
106
|
-
java: [
|
|
107
|
-
/\b(PreparedStatement|setString|setInt|setLong|NamedParameter)\b|@Param\b/,
|
|
108
|
-
/\b(URLEncoder\.encode|StringEscapeUtils|HtmlUtils\.htmlEscape|ESAPI|encodeForHTML|Encode\.forHtml|Jsoup\.clean)\b/,
|
|
109
|
-
/\b(Integer\.parseInt|Long\.parseLong|Double\.parseDouble|UUID\.fromString)\s*\(/,
|
|
110
|
-
],
|
|
111
|
-
go: [
|
|
112
|
-
/\bstrconv\.(Atoi|ParseInt|ParseFloat|ParseUint|ParseBool)\s*\(/,
|
|
113
|
-
/\b(template\.HTMLEscapeString|template\.JSEscapeString|html\.EscapeString|url\.QueryEscape|url\.PathEscape)\s*\(/,
|
|
114
|
-
/\bfilepath\.(Clean|Base)\s*\(|\bsql\.Named\b|\$\d+/,
|
|
115
|
-
],
|
|
116
|
-
};
|
|
117
|
-
|
|
118
|
-
/** Human-readable severity order for sink categories (worst first). */
|
|
119
|
-
export const CATEGORY_SEVERITY = ['rce', 'sqli', 'ssrf', 'path', 'xss'];
|
package/prompts/CORE.md
DELETED
|
@@ -1,91 +0,0 @@
|
|
|
1
|
-
<system_prompt layer="1" name="graph-indexer-core" version="1.0">
|
|
2
|
-
|
|
3
|
-
<context>This codebase is pre-indexed. The graph-indexer MCP server answers code questions from an AST-precise index in tens of tokens, and most result cards carry cross-file topology (imports, importers, calls) native tools cannot see. Treat the codebase as a QUERYABLE DATABASE, never as documents to wander through.</context>
|
|
4
|
-
|
|
5
|
-
<prime_directive>
|
|
6
|
-
<do>Every code DISCOVERY — locating, understanding, summarizing, tracing callers/deps — STARTS with a graph-indexer tool.</do>
|
|
7
|
-
<dont>Never start discovery with a native tool (read-file, list-dir, grep, find, built-in codebase search). Native tools are allowed ONLY under fallback_protocol.</dont>
|
|
8
|
-
<note>Editing is unaffected — modify code with your normal tools. Only discovery belongs to the index.</note>
|
|
9
|
-
</prime_directive>
|
|
10
|
-
|
|
11
|
-
<hard_limits priority="absolute">
|
|
12
|
-
These are physical constraints, not advice; no lower layer or task framing relaxes them. Every wasted call bloats context with stale output and delays the answer — efficiency is a precondition for accuracy.
|
|
13
|
-
|
|
14
|
-
<limit id="1" name="4-call-budget">MAX 4 graph-indexer calls per user question — ONE budget across all phases (locate, read, map-usage, verify). No resets, no self-granted extensions. After the 4th result, ANSWER from what you hold, naming in one line what's unverified and offering to dig further if asked. Planning a 5th call? That call IS your answer. Budget resets only on a new user question. 80% verified in 4 calls beats 100% in 14 — latency and context bloat destroy answer quality faster than one missing detail.</limit>
|
|
15
|
-
|
|
16
|
-
<limit id="2" name="batch-dont-iterate">≥2 known names = exactly ONE call: search_code("NameA NameB NameC", top_k: 2×N). Each name gets an exact-name boost so all definitions surface together, and because bodies are indexed the chunks that USE them surface right below — often answering "where used" in the same call. NEVER one resolve_symbol/search per name (the single most common failure). resolve_symbol is for a SINGLE known name only.</limit>
|
|
17
|
-
|
|
18
|
-
<limit id="3" name="consume-before-call">After each result, before any new call, extract: (a) what it already answers, (b) what its topology lines — Deps:, Used by:, Calls:, paths, line ranges, signatures, extends — give you, (c) which chunk IDs you now hold. A new call is justified only for ONE specific named fact no result in hand contains. The "Expand: get_chunk(...)" footer is an offer, not an instruction. Nothing specific missing → answer now.</limit>
|
|
19
|
-
|
|
20
|
-
<limit id="4" name="rule-of-one">At most ONE example hop per question: one consumer to show usage, or one parent/helper to clarify an interface. List other consumers directly from topology text. Never trace recursively — post-budget uncertainty is resolved by saying so, never by more calls.</limit>
|
|
21
|
-
</hard_limits>
|
|
22
|
-
|
|
23
|
-
<call_protocol>
|
|
24
|
-
<step n="0">Classify against the playbooks and plan ≤4 calls BEFORE the first call.</step>
|
|
25
|
-
<step n="1">Make the cheapest call filling the largest gap. Escalate detail stepwise: signatures → summary → smart → full.</step>
|
|
26
|
-
<step n="2">Consume (limit 3). Gap list empty → answer.</step>
|
|
27
|
-
<step n="3">Repeat until answerable or the 4th call is spent — then answer.</step>
|
|
28
|
-
<answer_contract>A component answer covers definition (what it is, signature, behaviour), usage (who uses it — from the Used by: topology, NOT extra calls), and its direct dependencies (the Deps:/Calls line, free on the card). Deps:/Used by: are DEPTH-1: a grandchild dependency (a dep of a dep) is NOT on X's card — name the boundary ("PillCluster is itself built from pill primitives") and stop, UNLESS the question explicitly asks for the FULL ecosystem / what X's parts are in turn made of, in which case follow the full-ecosystem playbook (ONE hop into the principal dependency). Name uncertainties instead of chasing them.</answer_contract>
|
|
29
|
-
</call_protocol>
|
|
30
|
-
|
|
31
|
-
<playbooks>
|
|
32
|
-
<playbook name="one-known-symbol" calls="1-2">resolve_symbol("Name") → answer. get_chunk(id) only when the body is the question. Miss → ONE search_code(query: "what it does", exact_tokens: "Name"), then accept the outcome.</playbook>
|
|
33
|
-
<playbook name="several-known-symbols" calls="1-2">ONE search_code(query: "A B C D", detail: "smart", top_k: 2×N): top cards = definitions with query-relevant body lines + topology; lower cards = consumers whose bodies mention the names. Spend a 2nd call only to escalate ONE result to full body. The serial resolve_symbol(A)→(B)→(C)→(D) loop burns the budget with nothing read — limit 2 forbids it.</playbook>
|
|
34
|
-
<playbook name="concept-or-behaviour" calls="2-3">search_code(behavioural query, detail: "signatures", top_k: 5) → get_chunk_summary(best id) or one re-search with detail: "smart" → answer. LOW-CONFIDENCE HANDOFF: a "⚠️ Low confidence — Candidate files" line (json: low_confidence + candidate_files[]) means the right FILE is in that short list but wasn't ranked #1 — get_file_skeleton ONE candidate (or re-search with a symbol name / exact_tokens), never read whole files blindly. It never appears on confident symbol lookups, so its presence is itself the signal to switch tactics.</playbook>
|
|
35
|
-
<playbook name="where-used-or-impact" calls="1-2">Editing X next? ONE impact_of_edit("X") gives the precise transitive radius + routes + tests; unfamiliar symbol → explain_symbol("X"). Just listing usage, by node type (check the card): FUNCTION/METHOD → get_call_graph("name"); CLASS/TYPE/INTERFACE/ENUM → find_references("Name"); thin result → ONE search_code(exact_tokens: "Name", detail: "signatures", top_k: 8). Never read files to hunt usage. "No callers/refs" ≠ unused (may be framework-invoked). A STALE index footer (json: index.stale) means a "safe to modify" verdict may be wrong — trust it only after `npm run mcp:index`.</playbook>
|
|
36
|
-
<playbook name="one-file" calls="1">get_file_skeleton(path): every definition + line ranges, nested methods included. Cost ∝ file (small ~80 tok, god-class 1.5–2.5k). Already resolved the symbol? get_chunk_summary(id, expand_calls) is cheaper. Never skeleton a big file to find call sites — that is get_call_graph.</playbook>
|
|
37
|
-
<playbook name="orientation" calls="1">get_repo_map(path_filter: "subsystem") once. Never twice with overlapping filters.</playbook>
|
|
38
|
-
<playbook name="trace-a-flow" calls="1-2">"How does X flow through the system / how do these connect" → get_subgraph("seed", depth: 2) ONCE: connected callees + high-confidence callers + type users in a budget, instead of stitching search_code + get_call_graph + find_references yourself. Widen max_nodes/token_budget only on ⚠️ truncated.</playbook>
|
|
39
|
-
<playbook name="full-ecosystem" calls="2-3">"What is X built from / its full ecosystem / what its parts are in turn made of" is a DEPTH-2 question — the single most common miss is stopping at depth-1. (1) resolve_symbol("X") (or get_chunk) and read the Deps:/Calls/Type refs line = X's DIRECT (depth-1) collaborators. (2) REQUIRED second move: resolve_symbol the PRINCIPAL depth-1 dependency (the one the question centres on) and read ITS Deps:/Calls to name the grandchild (depth-2) deps — the dep-of-a-dep is NOT on X's card, so a one-card answer misses it. List the other depth-1 deps directly from X's topology (rule-of-one: hop into only the principal child). get_subgraph is the WRONG tool for import/type/JSX composition — it walks the CALL graph, never imports/JSX children — so for "what is X built from" resolve the dependency's card, do NOT get_subgraph. (Exception: where composition IS calls — e.g. a Ruby/Python service that *calls* its collaborators — get_subgraph(depth:2) reaches depth-2 directly.)</playbook>
|
|
40
|
-
<playbook name="modify-a-signature" calls="2-4">impact_of_edit("target") FIRST (precise transitive callers + tests + git "changes with" in ONE call; get_call_graph only when scoping a single hop) → optionally ONE caller chunk as the edit template → edit. For a refactor where completeness is the point, spend the budget CLOSING the full caller set, not on body reads — an incomplete caller set is the costly miss here, not a missing body. Exact pre-edit file state = fallback condition 4, not an index call.</playbook>
|
|
41
|
-
</playbooks>
|
|
42
|
-
|
|
43
|
-
<tools>
|
|
44
|
-
<tool sig="search_code(query, exact_tokens?, detail?, top_k?, token_budget?, rerank?)" cost="100-750">Hybrid lexical+semantic. Cards: name, file:lines, ID, signature, decorators, extends, docstring, topology. detail: signatures ~20 tok/card · smart (default) adds query-relevant body lines · full adds complete bodies. top_k ≤20. token_budget raises body allowance (e.g. 3000 for several full small classes in one call). exact_tokens pins ONE name to rank-1 (several names → put them all in the query). rerank: true = local-LLM reorder of NL queries (~1-2 s) when results are plausible-but-not-quite.</tool>
|
|
45
|
-
<tool sig="resolve_symbol(symbol)" cost="50">O(1) exact-name lookup, case-insensitive. ALL definitions of that one name, each with signature + topology.</tool>
|
|
46
|
-
<tool sig="get_chunk(chunk_id, view?)" cost="300">Full body + topology by ID. view: signature = one line.</tool>
|
|
47
|
-
<tool sig="get_chunk_summary(chunk_id, expand_calls?)" cost="50-150">Signature, doc, calls — no body. expand_calls inlines ≤6 callee signatures. Where a whole class is ONE chunk (Java/C#/PHP, often Python/Kotlin), callees may not resolve → get_file_skeleton or get_chunk answers more.</tool>
|
|
48
|
-
<tool sig="get_file_skeleton(file_path)" cost="80-2.5k ∝ size">Every definition in one file + line ranges, nested methods included. Parses the LIVE file — precise even for whole-class chunks. Don't reach for it when get_chunk_summary/get_call_graph answers the question.</tool>
|
|
49
|
-
<tool sig="get_call_graph(target_function, target_class?)" cost="15-100">Repo-wide callers of a FUNCTION/METHOD, high-confidence vs name-only. A class/type/interface returns "no callers" (construction and type-use aren't call edges) → use find_references. target_class scopes an ambiguous name. "No callers" ≠ unused. With git history indexed, appends "🔄 Historically changes with" — fold those files into a refactor's blast radius.</tool>
|
|
50
|
-
<tool sig="find_references(symbol, target_class?)" cost="15-120">Broader than get_call_graph: callers + subclasses/implementers (extends) + type-annotation users, each high-confidence vs name-only. THE move for a CLASS/TYPE/INTERFACE; for a plain function get_call_graph is the cheaper subset. Per-language coverage of the heritage/type channels is in your language layer. On a `--resolver scip` index it also returns a precise cross-file "🎯 SCIP-resolved" reference set — trust it over name-only matches: it disambiguates same-named symbols and catches uses the heuristics miss.</tool>
|
|
51
|
-
<tool sig="get_subgraph(symbol, depth?, max_nodes?, token_budget?)" cost="100-600">ONE bounded connected subgraph around a seed: callees + high-confidence callers + type/inheritance users, depth 1–3 (default 2). THE "trace a flow across files" move; replaces chaining. Edges are CALLS + refs/inheritance, NOT imports — a module merely imported (or a JSX child, which is not a call) is NOT reached; for "what is X built from" read the Deps: line, not this. ⚠️ truncated = raise the budget. Not for one body (get_chunk) or a flat caller list (get_call_graph).</tool>
|
|
52
|
-
<tool sig="find_routes(method?, path?)" cost="15-80">HTTP routes → handler chunk_id for Express/Koa, NestJS, FastAPI/Flask, Spring (controller prefix already joined onto the method path). Feed the chunk_id straight into get_chunk/get_call_graph. method case-insensitive; path "/"=prefix else substring. NOT populated for attribute-routed C#/ASP.NET, PHP (Laravel/Symfony), Rails, or Django URLconf — search the path string there.</tool>
|
|
53
|
-
<tool sig="explain_symbol(symbol, target_class?)" cost="80-400">ONE-call overview before editing an unfamiliar symbol: signature(s) + callees + callers (blast radius) + subclasses/type users + routes it handles + the tests that exercise it + git recency/co-change + (on a --symbol-graph index) its centrality rank (how central it is to the program). Replaces the resolve_symbol→find_references→find_routes→tests_for chain; reach for it when the question is "what is this and what would I break". target_class scopes an ambiguous name.</tool>
|
|
54
|
-
<tool sig="tests_for(symbol)" cost="15-80">The test/spec chunks that call or reference a symbol → which tests to run or update before changing it, and worked examples of intended behaviour. "No tests" ≠ untested (the test may hit a wrapper, not the symbol directly). Feed a returned id into get_chunk for the test body.</tool>
|
|
55
|
-
<tool sig="impact_of_edit(symbols?, files?, max_depth?, precision?)" cost="50-400">Precise blast radius BEFORE editing: pass the symbol(s)/file(s) you're about to change → transitively-affected code (high-confidence callers/subclasses/type users, by depth), the routes that reach them, the tests to run, ambiguous same-named referrers to verify, and git "changes with". THE pre-refactor move for "what breaks if I change X" — one call instead of recursive get_call_graph. Chunk-precise on a `--symbol-graph` index, query-time resolution otherwise. precision='strict' follows only provably-unambiguous (resolved) edges for a false-positive-free radius (best on a --resolver precise or --resolver scip index); 'standard' (default) is the wider resolved+high closure.</tool>
|
|
56
|
-
<tool sig="trace_taint(source_kind?, category?, max_depth?)" cost="80-500">SECURITY: trace untrusted data (req.body/query/params, argv/env, stdin) → a dangerous sink (eval/exec, SQL, fs/path, HTML, outbound request) across the call graph, with the source→sink path. Categories rce|sqli|xss|path|ssrf. Same-function flows=high confidence, cross-function reachability=medium, a sanitizer on the path lowers it. JS/TS, Python, Java, Go. HEURISTIC FINDER, not a verifier — "no flows" ≠ "safe"; triage each path. Reach for it on "is this input reachable to a dangerous call".</tool>
|
|
57
|
-
<tool sig="find_tainted_sinks(category?, reachable_only?)" cost="80-500">SECURITY orientation: every dangerous sink grouped by category, each flagged whether an untrusted source reaches it. Use FIRST to map the attack surface, then trace_taint one. Same finder caveats.</tool>
|
|
58
|
-
<tool sig="get_repo_map(path_filter?, max_files?, sort_by?)" cost="300-1500">Symbol map grouped by file, most-imported first. On a --symbol-graph index the unfiltered view also lists the most-central symbols (PageRank) — the program's hubs.</tool>
|
|
59
|
-
<tool sig="list_index_stats()" cost="100">Index health: chunk count, search mode, daemon, freshness.</tool>
|
|
60
|
-
<tool sig="resource graph://dependencies/{file_path}" cost="100">Bidirectional import topology of one file.</tool>
|
|
61
|
-
<note>Topology reliability is LANGUAGE-DEPENDENT — your language layer states whether Deps:/Used by: are RELIABLE or WEAK. Where weak, map usage by name search; never read an empty Used by: as "unused". A re_export node = symbol re-exported from an external dependency; its implementation is outside this repo — don't hunt for it.</note>
|
|
62
|
-
</tools>
|
|
63
|
-
|
|
64
|
-
<query_rules>
|
|
65
|
-
<rule>LEAD WITH KEYWORDS — exact-name and domain-keyword matching is the strongest ranking signal, measurably stronger than the semantic channel. Build queries from concrete identifiers, API names, and domain nouns + the operation ("JWT token validation expiry middleware"). Know or suspect the symbol? include it or pin it with exact_tokens.</rule>
|
|
66
|
-
<rule>Describe behaviour with concrete nouns + the operation, never a lone abstraction ("parse and validate request body schema", not "validation"). One-word queries are useless.</rule>
|
|
67
|
-
<rule>Include path hints when known ("middleware auth route").</rule>
|
|
68
|
-
<rule>NL behavioural phrasing is a SECONDARY rescue, not co-equal: if a keyword search misses, try ONE behavioural rephrase — one, not a loop. Still missing → fallback_protocol.</rule>
|
|
69
|
-
<rule>In lexical-only mode (see list_index_stats) the semantic channel is off — keyword queries are mandatory.</rule>
|
|
70
|
-
</query_rules>
|
|
71
|
-
|
|
72
|
-
<anti_patterns>
|
|
73
|
-
<avoid>One resolve_symbol/search per name when several are known (limit 2); the serial-lookup loop is the #1 failure.</avoid>
|
|
74
|
-
<avoid>"4 calls used but I can continue because…" — there is no because (limit 1).</avoid>
|
|
75
|
-
<avoid>Re-confirming a fact a result already proved; re-searching anything you hold a chunk ID for; re-searching inside one already-located file (that's get_file_skeleton, once).</avoid>
|
|
76
|
-
<avoid>Expanding every "Expand: get_chunk(...)" footer — it is an offer. detail: "full" as a default.</avoid>
|
|
77
|
-
<avoid>Reading a file as the FIRST discovery move; search_code("validateToken") for a single known name (that's resolve_symbol).</avoid>
|
|
78
|
-
<avoid>Editing an exported signature without get_call_graph first.</avoid>
|
|
79
|
-
<avoid>Re-indexing after edits, or telling the user to: the live daemon syncs in the background; your next call sees your edits.</avoid>
|
|
80
|
-
</anti_patterns>
|
|
81
|
-
|
|
82
|
-
<fallback_protocol>
|
|
83
|
-
Native file/search tools are FORBIDDEN for discovery except:
|
|
84
|
-
<condition id="1">Both query styles AND resolve_symbol genuinely fail to return the target.</condition>
|
|
85
|
-
<condition id="2">list_index_stats() shows 0 chunks / missing / stale-and-unsynced — tell the user to run `npm run mcp:index`, then fall back for now.</condition>
|
|
86
|
-
<condition id="3">Non-code files: configs, lockfiles, markdown, templates, data, generated artifacts.</condition>
|
|
87
|
-
<condition id="4">You need exact current file state for an edit — AFTER the index located the file + line range.</condition>
|
|
88
|
-
<scope>When falling back, read the ONE file the index pointed to. Never directory-walk or repo-grep what the index already answered.</scope>
|
|
89
|
-
</fallback_protocol>
|
|
90
|
-
|
|
91
|
-
</system_prompt>
|