graph-indexer 2.0.0 → 2.1.1
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 +442 -0
- package/README.md +54 -11
- package/SECURITY.md +17 -0
- package/colbert.mjs +168 -0
- package/config.mjs +246 -21
- package/embeddings.mjs +90 -8
- package/engine/binary.mjs +38 -0
- package/engine/memory.mjs +214 -1
- package/engine/sqlite.mjs +278 -5
- package/enrichment.mjs +95 -0
- package/indexer.mjs +254 -9
- package/init.mjs +25 -2
- package/mcp/centrality.mjs +117 -0
- package/mcp/resolver.mjs +116 -0
- package/mcp/symbolgraph.mjs +137 -0
- package/mcp/taint.mjs +225 -0
- package/mcp/tools.mjs +444 -19
- package/mcp/topology.mjs +138 -13
- package/mcp-server.mjs +26 -2
- package/package.json +33 -22
- package/parse/extractor.mjs +17 -6
- package/parse/interprocedural.mjs +134 -0
- package/parse/languages.mjs +221 -57
- package/parse/metadata.mjs +46 -0
- package/parse/scip.mjs +391 -0
- package/parse/taint-patterns.mjs +119 -0
- package/prompts/CORE.md +9 -4
- package/prompts/frameworks/FASTAPI_DJANGO.md +1 -0
- package/prompts/frameworks/NODE_EXPRESS_NESTJS.md +1 -0
- package/prompts/frameworks/REACT.md +1 -1
- package/prompts/frameworks/SPRING_BOOT.md +1 -0
- package/prompts/languages/GO.md +1 -0
- package/prompts/languages/JAVA.md +1 -1
- package/prompts/languages/PYTHON.md +1 -1
- package/prompts/languages/RUST.md +1 -1
- package/seal.mjs +400 -0
- package/search-core.mjs +151 -1
- package/search-sparse.mjs +183 -0
- package/storage.mjs +5 -0
- package/watch-daemon.mjs +7 -2
package/parse/scip.mjs
ADDED
|
@@ -0,0 +1,391 @@
|
|
|
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
|
+
}
|
|
@@ -0,0 +1,119 @@
|
|
|
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
CHANGED
|
@@ -32,12 +32,12 @@
|
|
|
32
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
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
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">
|
|
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
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
37
|
<playbook name="orientation" calls="1">get_repo_map(path_filter: "subsystem") once. Never twice with overlapping filters.</playbook>
|
|
38
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
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">
|
|
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
41
|
</playbooks>
|
|
42
42
|
|
|
43
43
|
<tools>
|
|
@@ -47,10 +47,15 @@
|
|
|
47
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
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
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.</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
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
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="
|
|
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>
|
|
54
59
|
<tool sig="list_index_stats()" cost="100">Index health: chunk count, search mode, daemon, freshness.</tool>
|
|
55
60
|
<tool sig="resource graph://dependencies/{file_path}" cost="100">Bidirectional import topology of one file.</tool>
|
|
56
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>
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
<rule name="schema-models">Pydantic models, serializers, forms are declarative — the class definition IS the validation. resolve_symbol("UserCreate") answers "what is validated"; imperative validation code doesn't exist, don't search for it.</rule>
|
|
13
13
|
<rule name="signals-and-tasks">Signals connect by the signal OBJECT and tasks by name/registration — no call edges. ONE exact_tokens search on the signal/task name finds both ends. Middleware lists and settings are config files — fallback condition 3 once the consuming code is located.</rule>
|
|
14
14
|
<rule name="query-style">URL fragments and signal names beat function names. For "where is this field validated/transformed", name the FIELD and behaviour ("normalize email before saving user") — model, serializer, or signal handler, one behavioural search covers all three.</rule>
|
|
15
|
+
<rule name="request-input-to-sink">SECURITY ("can user input reach SQL/RCE/template here"): find_tainted_sinks(category) to map the surface, then trace_taint one — request.json/POST/GET + path/query params → raw()/extra()/.execute() (sqli), os.system/subprocess (rce), template/HTML (xss). Finder, not proof: "no flows" ≠ safe.</rule>
|
|
15
16
|
</rules>
|
|
16
17
|
|
|
17
18
|
<playbook question="what happens on POST /orders" calls="3-4">find_routes("POST", "/orders") (FastAPI/Flask; for Django URLconf fall back to search_code(exact_tokens: "/orders")) → get_chunk(handler/view) — decorators + signature reveal deps and models → resolve_symbol(the ONE model/service it delegates to) → answer; ONE dependency/parent hop only if the question demands it.</playbook>
|
|
@@ -11,6 +11,7 @@
|
|
|
11
11
|
<rule name="container-wiring">WHICH implementation backs an injected token is container wiring, not a call: constructor injection creates no edge. Find the binding with ONE search of the module/provider registration (exact_tokens: "UserService") — never via the constructor's call graph.</rule>
|
|
12
12
|
<rule name="lifecycle-hooks">onModuleInit-style hooks, error handlers, scheduled/cron methods, event/queue consumers have no inbound edges — the decorator in the summary says WHO invokes them; don't hunt for callers.</rule>
|
|
13
13
|
<rule name="query-style">Path strings beat handler names (routes rename rarely, handlers often). Cross-cutting behaviour ("where do we attach the request ID") lives in middleware whose name you can't guess — search behaviourally.</rule>
|
|
14
|
+
<rule name="request-taint">SECURITY: req.body/query/params is the untrusted source. "Is this endpoint injectable" → find_tainted_sinks(category) to map the surface, then trace_taint one source→sink path (sqli|rce|xss|path|ssrf). Finder, not proof — "no flows" ≠ safe.</rule>
|
|
14
15
|
</rules>
|
|
15
16
|
|
|
16
17
|
<playbook question="what happens on POST /orders" calls="3-4">find_routes("POST", "/orders") → get_chunk(handler chunk_id) → get_chunk_summary(the service it calls, expand_calls: true) → answer; ONE middleware/guard chunk only if the question is about it.</playbook>
|
|
@@ -13,7 +13,7 @@
|
|
|
13
13
|
<rule name="query-style">Component names are PascalCase and unique — exact_tokens with the component name is the highest-precision query here. For visual/behavioural questions ("the dropdown that closes on outside click"), use a behavioural sentence — handler names vary too much.</rule>
|
|
14
14
|
</rules>
|
|
15
15
|
|
|
16
|
-
<playbook question="explain component X and where it renders" calls="2-3">resolve_symbol("X") → get_chunk(id) — a component's body IS its behaviour (hooks + JSX), so the full chunk replaces two summary calls → answer citing Used by: as the render sites; ≤1 consumer chunk as a render example. Do NOT resolve every child component in its JSX.</playbook>
|
|
16
|
+
<playbook question="explain component X and where it renders" calls="2-3">resolve_symbol("X") → get_chunk(id) — a component's body IS its behaviour (hooks + JSX), so the full chunk replaces two summary calls → answer citing Used by: as the render sites; ≤1 consumer chunk as a render example. Do NOT resolve every child component in its JSX. BEFORE a prop/signature change use explain_symbol("X") instead — render sites (Used by:), tests, and git recency in ONE call, since get_call_graph is blind to JSX.</playbook>
|
|
17
17
|
|
|
18
18
|
<playbook question="explain component X and its full ecosystem / what it's built from" calls="2-3">The child components X renders are IMPORTS, not calls — get_call_graph AND get_subgraph are BLIND to them (both walk the call graph, returning X's hooks/util calls, never its JSX children). The composition tree is the Deps: line on the resolve_symbol/get_chunk card (depth-1); render sites are Used by:. (1) resolve_symbol("X") → read Deps: for the direct child components. (2) For the FULL ecosystem / "what it's built from", the depth-2 grandchild is REQUIRED, not optional: resolve_symbol the PRINCIPAL child to read ITS Deps: — e.g. Modal's Deps: lists Fade; resolving Fade surfaces TransitionWrapper, which is NOT on Modal's card. A depth-1-only answer (Modal's children, stop) is INCOMPLETE for this question. Rule-of-one: hop into only the principal child; list the rest of X's children straight from its Deps:. NEVER get_subgraph to enumerate composition — it returns animation/hook plumbing, not the component tree.</playbook>
|
|
19
19
|
|
|
@@ -12,6 +12,7 @@
|
|
|
12
12
|
<rule name="deep-inheritance">If a resolved class looks too thin, the behaviour lives in an abstract base — resolve ONE parent, guided by the extends list on the card.</rule>
|
|
13
13
|
<rule name="events-and-config">Event publishers and @EventListener handlers connect by the EVENT CLASS — ONE exact_tokens search on the event class finds both ends. @Value/configuration-properties bind to yml/properties — fallback condition 3 once the consuming class is located.</rule>
|
|
14
14
|
<rule name="query-style">Annotation names are high-precision tokens (exact_tokens: "@Scheduled"). Cross-cutting behaviour ("where do we audit changes") lives in an aspect/listener you can't name — search behaviourally.</rule>
|
|
15
|
+
<rule name="request-input-taint">SECURITY: controller request data (@RequestBody/@RequestParam/@PathVariable) is the untrusted source. For "can request input reach SQL/exec/path" use find_tainted_sinks(category) then trace_taint — Java-supported. Finder, not proof: "no flows" ≠ safe.</rule>
|
|
15
16
|
</rules>
|
|
16
17
|
|
|
17
18
|
<playbook question="what happens on POST /orders" calls="3-4">find_routes("POST", "/orders") → get_chunk_summary(handler chunk_id, expand_calls: true) — annotations + called services in one shot → get_chunk_summary(the ONE service method doing the work) → answer; ONE impl/parent/aspect hop only if the question demands it.</playbook>
|
package/prompts/languages/GO.md
CHANGED
|
@@ -15,6 +15,7 @@
|
|
|
15
15
|
<rule name="embedded-promotion">A call to a method the outer type doesn't define lives in an EMBEDDED type — check the type-declaration chunk for embedded fields before distrusting the index.</rule>
|
|
16
16
|
<rule name="implicit-execution">init() and `_` blank imports run with no inbound edges — absence of callers is not dead code. `go fn()` and `defer fn()` ARE edges; channel flow is not — trace channels by searching the channel field name.</rule>
|
|
17
17
|
<rule name="first-class-functions">Functions passed as values (http.HandlerFunc(handle), middleware constructors) create no edge to the passed function — find sites with exact_tokens.</rule>
|
|
18
|
+
<rule name="input-reachability">SECURITY — "can request/CLI input reach a dangerous call" (exec, SQL, fs/path) → trace_taint (Go is supported); find_tainted_sinks first to map the surface. Finder, not proof — "no flows" ≠ safe.</rule>
|
|
18
19
|
<rule name="query-style">Always include package/domain words ("auth middleware token refresh", never bare "Refresh"). To find where an error originates, search its message string literal — the most unique token in the flow.</rule>
|
|
19
20
|
</rules>
|
|
20
21
|
|
|
@@ -18,6 +18,6 @@
|
|
|
18
18
|
<rule name="accessor-noise">Never read a POJO/bean body: detail: "signatures" or get_file_skeleton tells you everything a data class can. Exception flow: ONE exact_tokens search on the exception class finds throw sites and handlers together.</rule>
|
|
19
19
|
</rules>
|
|
20
20
|
|
|
21
|
-
<playbook question="explain service X / change a method safely" calls="2-4">resolve_symbol("X") (annotations visible) → get_chunk_summary(id, expand_calls: true) → answer with Used by:. Signature change:
|
|
21
|
+
<playbook question="explain service X / change a method safely" calls="2-4">resolve_symbol("X") (annotations visible) → get_chunk_summary(id, expand_calls: true) → answer with Used by:. Signature change: impact_of_edit(symbols:["method"]) — transitive callers+overrides+tests in ONE call, no recursive get_call_graph (class-granular chunks make that painful) → ONE caller chunk as template → edit.</playbook>
|
|
22
22
|
|
|
23
23
|
</environment_prompt>
|
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
</index_facts>
|
|
11
11
|
|
|
12
12
|
<rules>
|
|
13
|
-
<rule name="name-overmatch">Dispatch binds by NAME and Python is duck-typed: get_call_graph("save") mixes every save repo-wide, and resolve_symbol may return several — pick by file path + class context from the cards, no extra reads.</rule>
|
|
13
|
+
<rule name="name-overmatch">Dispatch binds by NAME and Python is duck-typed: get_call_graph("save") mixes every save repo-wide, and resolve_symbol may return several — pick by file path + class context from the cards, no extra reads. If your index was built --resolver scip (scip-python), find_references returns a 🎯 SCIP-resolved set that disambiguates the right one — trust it over name-only.</rule>
|
|
14
14
|
<rule name="dunder-magic">Implicit protocol calls have no edges: `obj()`↛__call__, `a+b`↛__add__, attr access↛__getattr__, `with`↛__enter__/__exit__. An empty call graph on a dunder means nothing.</rule>
|
|
15
15
|
<rule name="package-barrels">`__init__.py` re-export hubs produce re_export nodes: internal origin → ONE hop; external dependency → stop.</rule>
|
|
16
16
|
<rule name="decorators-wrap">Call sites point at the DECORATED name even when a decorator wraps it. If runtime behaviour is the question, read the decorator's chunk ONCE — never trace every wrapped function.</rule>
|