graph-indexer 1.1.0 → 2.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 +412 -0
- package/README.md +228 -350
- package/SECURITY.md +68 -0
- package/TRADEMARK.md +31 -0
- package/cli-ui.mjs +78 -0
- package/config.mjs +269 -0
- package/daemon-ctl.mjs +175 -0
- package/daemon-lock.mjs +69 -0
- package/embedders/python/mlx_embed_server.py +61 -0
- package/embedders/python/requirements-mlx.txt +5 -0
- package/embedders/setup-mlx.mjs +110 -0
- package/embeddings.mjs +489 -0
- package/engine/binary.mjs +399 -0
- package/{core-engine.mjs → engine/memory.mjs} +359 -249
- package/engine/sqlite.mjs +863 -0
- package/enrichment.mjs +426 -0
- package/git-signals.mjs +248 -0
- package/indexer.mjs +216 -77
- package/init.mjs +1604 -170
- package/layout.mjs +123 -0
- package/mcp/format.mjs +61 -0
- package/mcp/tools.mjs +1157 -0
- package/mcp/topology.mjs +551 -0
- package/mcp-server.mjs +118 -573
- package/package.json +56 -16
- package/parse/extractor.mjs +617 -0
- package/parse/imports.mjs +365 -0
- package/parse/languages.mjs +89 -0
- package/parse/metadata.mjs +655 -0
- package/parse/routes.mjs +327 -0
- package/prompts/CORE.md +86 -0
- package/prompts/DOMAIN_TEMPLATE.md +46 -0
- package/prompts/INTEGRATION.md +123 -0
- package/prompts/frameworks/ANDROID.md +17 -0
- package/prompts/frameworks/ASPNET_CORE.md +19 -0
- package/prompts/frameworks/FASTAPI_DJANGO.md +19 -0
- package/prompts/frameworks/LARAVEL_SYMFONY.md +22 -0
- package/prompts/frameworks/NODE_EXPRESS_NESTJS.md +18 -0
- package/prompts/frameworks/RAILS.md +20 -0
- package/prompts/frameworks/REACT.md +20 -0
- package/prompts/frameworks/SPRING_BOOT.md +19 -0
- package/prompts/languages/BASH.md +22 -0
- package/prompts/languages/C.md +21 -0
- package/prompts/languages/CSHARP.md +24 -0
- package/prompts/languages/CSS_SCSS.md +22 -0
- package/prompts/languages/GO.md +23 -0
- package/prompts/languages/JAVA.md +23 -0
- package/prompts/languages/JAVASCRIPT_TYPESCRIPT.md +24 -0
- package/prompts/languages/KOTLIN.md +24 -0
- package/prompts/languages/PHP.md +23 -0
- package/prompts/languages/PYTHON.md +23 -0
- package/prompts/languages/RUBY.md +23 -0
- package/prompts/languages/RUST.md +21 -0
- package/prompts/languages/SWIFT.md +21 -0
- package/search-core.mjs +627 -0
- package/storage.mjs +68 -0
- package/watch-daemon.mjs +204 -69
- package/PROMPT.md +0 -143
- package/parser-utils.mjs +0 -861
package/parse/routes.mjs
ADDED
|
@@ -0,0 +1,327 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* @file parse/routes.mjs
|
|
3
|
+
* @description HTTP route extraction from AST (Express/NestJS/FastAPI/Flask/Spring Boot).
|
|
4
|
+
* @author MaquinaTech <https://github.com/MaquinaTech>
|
|
5
|
+
* @copyright (c) 2026 MaquinaTech. All rights reserved.
|
|
6
|
+
* @license MIT
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
// ─── HTTP route extraction ────────────────────────────────────────────────────
|
|
10
|
+
// The call graph records that a function was called, but cannot connect an HTTP
|
|
11
|
+
// verb+path (`GET /api/users`) to the handler that serves it. extractRoutes mines
|
|
12
|
+
// that mapping at index time from the four common shapes:
|
|
13
|
+
// • decorator-on-method — NestJS (@Get/@Post on a class method)
|
|
14
|
+
// • decorator-on-function — FastAPI/Flask (@app.get / @app.route)
|
|
15
|
+
// • annotation-on-method — Spring Boot (@GetMapping / @RequestMapping)
|
|
16
|
+
// • functional registration — Express/Koa (router.get('/x', handler))
|
|
17
|
+
// Each route resolves its handler to a chunk id (by name, case-sensitive) so an
|
|
18
|
+
// agent can jump straight from an endpoint to get_chunk / get_call_graph on the
|
|
19
|
+
// handler. No framework names are hardcoded beyond the verb sets — detection is by
|
|
20
|
+
// AST node type, mirroring extractDecorators / extractCallSites.
|
|
21
|
+
|
|
22
|
+
const HTTP_VERBS = new Set(['get', 'post', 'put', 'delete', 'patch', 'all', 'head', 'options']);
|
|
23
|
+
const ROUTE_JS_LIKE = ['.ts', '.tsx', '.js', '.jsx', '.mjs', '.cjs'];
|
|
24
|
+
|
|
25
|
+
/** Unquote a string-literal node across grammars (JS string_fragment, Python
|
|
26
|
+
* string_content, Java string_fragment, …). Returns null for non-string nodes. */
|
|
27
|
+
function _routeLiteral(node) {
|
|
28
|
+
if (!node) return null;
|
|
29
|
+
const STR = new Set(['string', 'template_string', 'string_literal',
|
|
30
|
+
'interpreted_string_literal', 'raw_string_literal']);
|
|
31
|
+
if (!STR.has(node.type)) return null;
|
|
32
|
+
const frag = node.namedChildren?.find(c => /fragment|content/.test(c.type));
|
|
33
|
+
if (frag) return frag.text;
|
|
34
|
+
return node.text.replace(/^[`'"]+|[`'"]+$/g, '');
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
/** Join a controller/class path prefix to a method path. Empty prefix → path as-is
|
|
38
|
+
* (spec: "if no Controller prefix, leave it as-is"). Collapses duplicate slashes. */
|
|
39
|
+
function _joinRoutePath(prefix, sub) {
|
|
40
|
+
prefix = (prefix || '').trim();
|
|
41
|
+
sub = (sub || '').trim();
|
|
42
|
+
if (!prefix) return sub;
|
|
43
|
+
if (!sub) return prefix;
|
|
44
|
+
return (prefix.replace(/\/+$/, '') + '/' + sub.replace(/^\/+/, '')).replace(/\/{2,}/g, '/');
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
/** Decorators that lead a node as siblings (mirrors extractDecorators' sibling walk).
|
|
48
|
+
* NestJS method decorators sit as previous siblings inside the class_body. */
|
|
49
|
+
function _leadingDecorators(node) {
|
|
50
|
+
const out = [];
|
|
51
|
+
let prev = node.previousSibling;
|
|
52
|
+
while (prev) {
|
|
53
|
+
if (prev.type === 'decorator') out.push(prev);
|
|
54
|
+
else if (prev.isNamed && prev.type !== 'comment') break;
|
|
55
|
+
prev = prev.previousSibling;
|
|
56
|
+
}
|
|
57
|
+
return out;
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/** Parse a TS/JS decorator node into { name, path } — name is the callee (`Get`,
|
|
61
|
+
* `Controller`), path is its first string argument (or null for bare `@Get()`). */
|
|
62
|
+
function _parseTsDecorator(decoNode) {
|
|
63
|
+
const call = decoNode.namedChildren?.find(c => c.type === 'call_expression');
|
|
64
|
+
if (!call) {
|
|
65
|
+
const id = decoNode.namedChildren?.find(c => c.type === 'identifier');
|
|
66
|
+
return { name: id?.text || decoNode.text.replace(/^@/, '').split(/[\s(]/)[0], path: null };
|
|
67
|
+
}
|
|
68
|
+
const fn = call.childForFieldName?.('function');
|
|
69
|
+
let name = '';
|
|
70
|
+
if (fn?.type === 'identifier') name = fn.text;
|
|
71
|
+
else if (fn?.type === 'member_expression') name = fn.childForFieldName?.('property')?.text || '';
|
|
72
|
+
const args = call.childForFieldName?.('arguments');
|
|
73
|
+
const firstStr = args?.namedChildren?.find(c => _routeLiteral(c) != null);
|
|
74
|
+
return { name, path: _routeLiteral(firstStr) };
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
/** NestJS: @Get/@Post/… on class methods, with the enclosing
|
|
78
|
+
* @Controller(prefix) prepended. */
|
|
79
|
+
function _extractNestRoutes(rootNode, emit) {
|
|
80
|
+
const stack = [rootNode];
|
|
81
|
+
while (stack.length) {
|
|
82
|
+
const n = stack.pop();
|
|
83
|
+
if (n.type === 'class_declaration' || n.type === 'abstract_class_declaration') {
|
|
84
|
+
// For `export class` the @Controller decorator is a leading sibling inside
|
|
85
|
+
// export_statement; for bare/abstract class it is a direct child before class_body.
|
|
86
|
+
const classDecos = [..._leadingDecorators(n)];
|
|
87
|
+
for (let i = 0; i < n.namedChildCount; i++) {
|
|
88
|
+
const c = n.namedChild(i);
|
|
89
|
+
if (c.type === 'class_body') break;
|
|
90
|
+
if (c.type === 'decorator') classDecos.push(c);
|
|
91
|
+
}
|
|
92
|
+
let prefix = '';
|
|
93
|
+
for (const d of classDecos) {
|
|
94
|
+
const { name, path } = _parseTsDecorator(d);
|
|
95
|
+
if (name === 'Controller' && path != null) { prefix = path; break; }
|
|
96
|
+
}
|
|
97
|
+
const body = n.childForFieldName?.('body')
|
|
98
|
+
|| n.namedChildren?.find(c => c.type === 'class_body');
|
|
99
|
+
if (body) {
|
|
100
|
+
for (let i = 0; i < body.namedChildCount; i++) {
|
|
101
|
+
const member = body.namedChild(i);
|
|
102
|
+
if (member.type !== 'method_definition') continue;
|
|
103
|
+
const handler = member.childForFieldName?.('name')?.text
|
|
104
|
+
|| member.namedChildren?.find(c => c.type === 'property_identifier')?.text;
|
|
105
|
+
for (const d of _leadingDecorators(member)) {
|
|
106
|
+
const { name, path } = _parseTsDecorator(d);
|
|
107
|
+
if (!name || !HTTP_VERBS.has(name.toLowerCase())) continue;
|
|
108
|
+
emit(name, _joinRoutePath(prefix, path), handler, member.startPosition.row + 1, 'nestjs');
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
for (let i = 0; i < n.namedChildCount; i++) stack.push(n.namedChild(i));
|
|
114
|
+
}
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
/** Derive a handler name from an Express argument node: a bare identifier, a member
|
|
118
|
+
* expression (`ctrl.getThing` → 'getThing', `this.handler` → 'handler'), or a
|
|
119
|
+
* `.bind()` call (`handler.bind(this)` → 'handler'). Arrow/inline fn → 'anonymous'. */
|
|
120
|
+
function _jsHandlerName(node) {
|
|
121
|
+
if (!node) return 'anonymous';
|
|
122
|
+
if (node.type === 'identifier') return node.text;
|
|
123
|
+
if (node.type === 'member_expression') {
|
|
124
|
+
return node.childForFieldName?.('property')?.text || 'anonymous';
|
|
125
|
+
}
|
|
126
|
+
if (node.type === 'call_expression') {
|
|
127
|
+
const callee = node.childForFieldName?.('function');
|
|
128
|
+
if (callee?.type === 'member_expression') {
|
|
129
|
+
const obj = callee.childForFieldName?.('object');
|
|
130
|
+
if (obj?.type === 'identifier') return obj.text; // handler.bind(this) → handler
|
|
131
|
+
if (obj?.type === 'member_expression') return obj.childForFieldName?.('property')?.text || 'anonymous';
|
|
132
|
+
return callee.childForFieldName?.('property')?.text || 'anonymous';
|
|
133
|
+
}
|
|
134
|
+
if (callee?.type === 'identifier') return callee.text; // makeHandler() → makeHandler
|
|
135
|
+
}
|
|
136
|
+
return 'anonymous'; // arrow_function / function / function_expression / unknown
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/** Express / Koa: `<router|app>.<verb>(<path>, …, <handler>)` call sites. The route
|
|
140
|
+
* path must be rooted ('/…') or a wildcard ('*') — this matches Express/Koa
|
|
141
|
+
* convention and rejects look-alike calls (`cache.get('key', cb)`, `map.get(k)`). */
|
|
142
|
+
function _extractExpressRoutes(rootNode, emit) {
|
|
143
|
+
const stack = [rootNode];
|
|
144
|
+
while (stack.length) {
|
|
145
|
+
const n = stack.pop();
|
|
146
|
+
if (n.type === 'call_expression') {
|
|
147
|
+
const fn = n.childForFieldName?.('function');
|
|
148
|
+
if (fn?.type === 'member_expression') {
|
|
149
|
+
const verb = fn.childForFieldName?.('property')?.text || '';
|
|
150
|
+
const args = n.childForFieldName?.('arguments');
|
|
151
|
+
if (verb && HTTP_VERBS.has(verb.toLowerCase()) && args) {
|
|
152
|
+
const named = args.namedChildren || [];
|
|
153
|
+
const routePath = _routeLiteral(named[0]);
|
|
154
|
+
if (routePath != null && /^[/*]/.test(routePath) && named.length >= 2) {
|
|
155
|
+
// The handler is the LAST argument — preceding args may be middleware.
|
|
156
|
+
const handler = _jsHandlerName(named[named.length - 1]);
|
|
157
|
+
emit(verb, routePath, handler, n.startPosition.row + 1, 'express');
|
|
158
|
+
}
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
}
|
|
162
|
+
for (let i = 0; i < n.namedChildCount; i++) stack.push(n.namedChild(i));
|
|
163
|
+
}
|
|
164
|
+
}
|
|
165
|
+
|
|
166
|
+
/** FastAPI / Flask: @app.get / @router.post / @app.route(path, methods=[…]). */
|
|
167
|
+
function _extractPyRoutes(rootNode, emit) {
|
|
168
|
+
const stack = [rootNode];
|
|
169
|
+
while (stack.length) {
|
|
170
|
+
const n = stack.pop();
|
|
171
|
+
if (n.type === 'decorated_definition') {
|
|
172
|
+
const def = n.namedChildren?.find(c => c.type === 'function_definition');
|
|
173
|
+
const handler = def?.childForFieldName?.('name')?.text
|
|
174
|
+
|| def?.namedChildren?.find(c => c.type === 'identifier')?.text;
|
|
175
|
+
const line = (def || n).startPosition.row + 1;
|
|
176
|
+
for (const d of n.namedChildren || []) {
|
|
177
|
+
if (d.type !== 'decorator') continue;
|
|
178
|
+
const call = d.namedChildren?.find(c => c.type === 'call');
|
|
179
|
+
const fn = call?.childForFieldName?.('function');
|
|
180
|
+
if (fn?.type !== 'attribute') continue;
|
|
181
|
+
const verb = fn.childForFieldName?.('attribute')?.text || '';
|
|
182
|
+
const args = call.childForFieldName?.('arguments');
|
|
183
|
+
// FastAPI uses a positional path string; Flask uses `path=`/`rule=` keyword argument.
|
|
184
|
+
let routePath = _routeLiteral(args?.namedChildren?.find(c => c.type === 'string'));
|
|
185
|
+
if (routePath == null) {
|
|
186
|
+
const kw = args?.namedChildren?.find(c => c.type === 'keyword_argument'
|
|
187
|
+
&& ['path', 'rule'].includes(c.childForFieldName?.('name')?.text));
|
|
188
|
+
routePath = _routeLiteral(kw?.childForFieldName?.('value'));
|
|
189
|
+
}
|
|
190
|
+
// Require a rooted path string to reject look-alike decorators such as
|
|
191
|
+
// `@cache.get("key")` or `@retry.post(n=3)` that share verb names.
|
|
192
|
+
const isRoute = routePath != null && routePath.startsWith('/');
|
|
193
|
+
if (HTTP_VERBS.has(verb.toLowerCase())) {
|
|
194
|
+
if (isRoute) emit(verb, routePath, handler, line, 'fastapi');
|
|
195
|
+
} else if (verb === 'route' && isRoute) {
|
|
196
|
+
// Flask `@app.route` can declare multiple HTTP methods; default GET when `methods` is omitted.
|
|
197
|
+
const kw = args?.namedChildren?.find(c => c.type === 'keyword_argument'
|
|
198
|
+
&& c.childForFieldName?.('name')?.text === 'methods');
|
|
199
|
+
const list = kw?.childForFieldName?.('value');
|
|
200
|
+
const methods = list
|
|
201
|
+
? (list.namedChildren || []).map(_routeLiteral).filter(Boolean)
|
|
202
|
+
: [];
|
|
203
|
+
for (const m of (methods.length ? methods : ['GET'])) {
|
|
204
|
+
emit(m, routePath, handler, line, 'flask');
|
|
205
|
+
}
|
|
206
|
+
}
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
for (let i = 0; i < n.namedChildCount; i++) stack.push(n.namedChild(i));
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/** Spring Boot: @GetMapping/@PostMapping/… and @RequestMapping(value=…, method=…)
|
|
214
|
+
* on methods, with class-level @RequestMapping as a prefix. */
|
|
215
|
+
function _springAnnotation(annNode) {
|
|
216
|
+
// annotation > identifier <name> + annotation_argument_list
|
|
217
|
+
const name = annNode.namedChildren?.find(c => c.type === 'identifier')?.text || '';
|
|
218
|
+
const argList = annNode.namedChildren?.find(c => c.type === 'annotation_argument_list');
|
|
219
|
+
const verbOf = (t) => (t || '').split('.').pop(); // RequestMethod.POST → POST
|
|
220
|
+
let path = null, methods = [];
|
|
221
|
+
if (argList) {
|
|
222
|
+
const positional = argList.namedChildren?.find(c => _routeLiteral(c) != null);
|
|
223
|
+
if (positional) path = _routeLiteral(positional);
|
|
224
|
+
for (const pair of argList.namedChildren || []) {
|
|
225
|
+
if (pair.type !== 'element_value_pair') continue;
|
|
226
|
+
const key = pair.namedChildren?.[0]?.text;
|
|
227
|
+
const valNode = pair.namedChildren?.[1];
|
|
228
|
+
if (key === 'value' || key === 'path') path = _routeLiteral(valNode) ?? path;
|
|
229
|
+
else if (key === 'method') {
|
|
230
|
+
// `method` may be a single value or an array literal — handle both shapes.
|
|
231
|
+
methods = (valNode?.type === 'element_value_array_initializer'
|
|
232
|
+
? (valNode.namedChildren || []).map(c => verbOf(c.text))
|
|
233
|
+
: [verbOf(valNode?.text)]).filter(Boolean);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
}
|
|
237
|
+
return { name, path, methods };
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
const SPRING_VERB = {
|
|
241
|
+
GetMapping: 'GET', PostMapping: 'POST', PutMapping: 'PUT',
|
|
242
|
+
DeleteMapping: 'DELETE', PatchMapping: 'PATCH',
|
|
243
|
+
};
|
|
244
|
+
|
|
245
|
+
function _extractSpringRoutes(rootNode, emit) {
|
|
246
|
+
const stack = [rootNode];
|
|
247
|
+
while (stack.length) {
|
|
248
|
+
const n = stack.pop();
|
|
249
|
+
if (n.type === 'class_declaration') {
|
|
250
|
+
const mods = n.namedChildren?.find(c => c.type === 'modifiers');
|
|
251
|
+
let prefix = '';
|
|
252
|
+
for (const a of (mods?.namedChildren || [])) {
|
|
253
|
+
if (a.type !== 'annotation') continue;
|
|
254
|
+
const { name, path } = _springAnnotation(a);
|
|
255
|
+
if (name === 'RequestMapping' && path != null) { prefix = path; break; }
|
|
256
|
+
}
|
|
257
|
+
const body = n.namedChildren?.find(c => c.type === 'class_body');
|
|
258
|
+
for (const member of (body?.namedChildren || [])) {
|
|
259
|
+
if (member.type !== 'method_declaration') continue;
|
|
260
|
+
const handler = member.childForFieldName?.('name')?.text;
|
|
261
|
+
const mMods = member.namedChildren?.find(c => c.type === 'modifiers');
|
|
262
|
+
for (const a of (mMods?.namedChildren || [])) {
|
|
263
|
+
if (a.type !== 'annotation' && a.type !== 'marker_annotation') continue;
|
|
264
|
+
const { name, path, methods } = _springAnnotation(a);
|
|
265
|
+
let verbs;
|
|
266
|
+
if (SPRING_VERB[name]) verbs = [SPRING_VERB[name]];
|
|
267
|
+
else if (name === 'RequestMapping') verbs = methods.length ? methods : ['ALL'];
|
|
268
|
+
else continue;
|
|
269
|
+
// @RequestMapping(method = {GET, POST}) → one route per verb.
|
|
270
|
+
for (const verb of verbs) {
|
|
271
|
+
emit(verb, _joinRoutePath(prefix, path), handler, member.startPosition.row + 1, 'spring');
|
|
272
|
+
}
|
|
273
|
+
}
|
|
274
|
+
}
|
|
275
|
+
}
|
|
276
|
+
for (let i = 0; i < n.namedChildCount; i++) stack.push(n.namedChild(i));
|
|
277
|
+
}
|
|
278
|
+
}
|
|
279
|
+
|
|
280
|
+
/**
|
|
281
|
+
* Map HTTP routes to their handlers for one parsed file.
|
|
282
|
+
*
|
|
283
|
+
* @param {object} rootNode parsed AST root
|
|
284
|
+
* @param {string} relPath repo-relative file path (stored on each route)
|
|
285
|
+
* @param {object[]} chunks the file's already-extracted chunks (handler→id lookup)
|
|
286
|
+
* @param {string} ext dotted file extension
|
|
287
|
+
* @returns {Array<{method,path,handler_name,handler_chunk_id,file_path,line,framework}>}
|
|
288
|
+
*/
|
|
289
|
+
export function extractRoutes(rootNode, relPath, chunks, ext) {
|
|
290
|
+
const routes = [];
|
|
291
|
+
if (!rootNode) return routes;
|
|
292
|
+
|
|
293
|
+
// Case-sensitive; first definition wins to match the chunk extraction order.
|
|
294
|
+
const idByName = new Map();
|
|
295
|
+
for (const c of (chunks || [])) {
|
|
296
|
+
if (c && c.name && !idByName.has(c.name)) idByName.set(c.name, c.id);
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
const emit = (method, routePath, handlerName, line, framework) => {
|
|
300
|
+
if (!method) return;
|
|
301
|
+
// NestJS controller-prefixed paths omit the leading slash ('users/:id'); normalise
|
|
302
|
+
// so a '/'-prefix query matches uniformly across all frameworks.
|
|
303
|
+
let p = routePath == null ? '' : String(routePath);
|
|
304
|
+
if (p && !p.startsWith('/')) p = '/' + p;
|
|
305
|
+
routes.push({
|
|
306
|
+
method: String(method).toUpperCase(),
|
|
307
|
+
path: p,
|
|
308
|
+
handler_name: handlerName || 'anonymous',
|
|
309
|
+
handler_chunk_id: (handlerName && idByName.has(handlerName)) ? idByName.get(handlerName) : null,
|
|
310
|
+
file_path: relPath,
|
|
311
|
+
line,
|
|
312
|
+
framework,
|
|
313
|
+
});
|
|
314
|
+
};
|
|
315
|
+
|
|
316
|
+
try {
|
|
317
|
+
if (ROUTE_JS_LIKE.includes(ext)) {
|
|
318
|
+
_extractNestRoutes(rootNode, emit);
|
|
319
|
+
_extractExpressRoutes(rootNode, emit);
|
|
320
|
+
} else if (ext === '.py') {
|
|
321
|
+
_extractPyRoutes(rootNode, emit);
|
|
322
|
+
} else if (ext === '.java') {
|
|
323
|
+
_extractSpringRoutes(rootNode, emit);
|
|
324
|
+
}
|
|
325
|
+
} catch { /* best-effort — never fail indexing */ }
|
|
326
|
+
return routes;
|
|
327
|
+
}
|
package/prompts/CORE.md
ADDED
|
@@ -0,0 +1,86 @@
|
|
|
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">Blast radius, 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">get_call_graph("target") FIRST → 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 (high-confidence callers + the name-only bucket + the git "changes with" line), 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.</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="get_repo_map(path_filter?, max_files?, sort_by?)" cost="300-1500">Symbol map grouped by file, most-imported first.</tool>
|
|
54
|
+
<tool sig="list_index_stats()" cost="100">Index health: chunk count, search mode, daemon, freshness.</tool>
|
|
55
|
+
<tool sig="resource graph://dependencies/{file_path}" cost="100">Bidirectional import topology of one file.</tool>
|
|
56
|
+
<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>
|
|
57
|
+
</tools>
|
|
58
|
+
|
|
59
|
+
<query_rules>
|
|
60
|
+
<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>
|
|
61
|
+
<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>
|
|
62
|
+
<rule>Include path hints when known ("middleware auth route").</rule>
|
|
63
|
+
<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>
|
|
64
|
+
<rule>In lexical-only mode (see list_index_stats) the semantic channel is off — keyword queries are mandatory.</rule>
|
|
65
|
+
</query_rules>
|
|
66
|
+
|
|
67
|
+
<anti_patterns>
|
|
68
|
+
<avoid>One resolve_symbol/search per name when several are known (limit 2); the serial-lookup loop is the #1 failure.</avoid>
|
|
69
|
+
<avoid>"4 calls used but I can continue because…" — there is no because (limit 1).</avoid>
|
|
70
|
+
<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>
|
|
71
|
+
<avoid>Expanding every "Expand: get_chunk(...)" footer — it is an offer. detail: "full" as a default.</avoid>
|
|
72
|
+
<avoid>Reading a file as the FIRST discovery move; search_code("validateToken") for a single known name (that's resolve_symbol).</avoid>
|
|
73
|
+
<avoid>Editing an exported signature without get_call_graph first.</avoid>
|
|
74
|
+
<avoid>Re-indexing after edits, or telling the user to: the live daemon syncs in the background; your next call sees your edits.</avoid>
|
|
75
|
+
</anti_patterns>
|
|
76
|
+
|
|
77
|
+
<fallback_protocol>
|
|
78
|
+
Native file/search tools are FORBIDDEN for discovery except:
|
|
79
|
+
<condition id="1">Both query styles AND resolve_symbol genuinely fail to return the target.</condition>
|
|
80
|
+
<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>
|
|
81
|
+
<condition id="3">Non-code files: configs, lockfiles, markdown, templates, data, generated artifacts.</condition>
|
|
82
|
+
<condition id="4">You need exact current file state for an edit — AFTER the index located the file + line range.</condition>
|
|
83
|
+
<scope>When falling back, read the ONE file the index pointed to. Never directory-walk or repo-grep what the index already answered.</scope>
|
|
84
|
+
</fallback_protocol>
|
|
85
|
+
|
|
86
|
+
</system_prompt>
|
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
<domain_prompt layer="3" name="project-domain" requires="graph-indexer-core">
|
|
2
|
+
|
|
3
|
+
<!--
|
|
4
|
+
LAYER 3 — YOUR PROJECT'S RULES. Replace every [BRACKETED] placeholder with
|
|
5
|
+
facts about YOUR codebase and delete sections that don't apply. Keep it under
|
|
6
|
+
~40 lines: this exists to make the agent's FIRST query hit rank-1, not to
|
|
7
|
+
restate your architecture docs. May ADD precision to Layers 1–2 but may NEVER
|
|
8
|
+
relax the 4-call budget, the prime directive, or the fallback protocol.
|
|
9
|
+
Tip: delete these comments once filled — they cost tokens every session.
|
|
10
|
+
-->
|
|
11
|
+
|
|
12
|
+
<project_identity>
|
|
13
|
+
<name>[PROJECT_NAME]</name>
|
|
14
|
+
<summary>[One sentence: what this system does and for whom.]</summary>
|
|
15
|
+
</project_identity>
|
|
16
|
+
|
|
17
|
+
<entry_points>
|
|
18
|
+
<!-- Where execution starts — saves the agent its first orientation call. -->
|
|
19
|
+
<entry path="[src/main.ts]" role="[application bootstrap]"/>
|
|
20
|
+
<entry path="[src/api/routes/]" role="[HTTP surface]"/>
|
|
21
|
+
</entry_points>
|
|
22
|
+
|
|
23
|
+
<domain_vocabulary>
|
|
24
|
+
<!-- Map business words to code words so search queries hit rank-1. -->
|
|
25
|
+
<term business="[invoice]" code="[BillingDocument]"/>
|
|
26
|
+
<term business="[customer]" code="[Account, AccountHolder]"/>
|
|
27
|
+
</domain_vocabulary>
|
|
28
|
+
|
|
29
|
+
<critical_paths>
|
|
30
|
+
<!-- Symbols where get_call_graph is MANDATORY before any change. -->
|
|
31
|
+
<symbol name="[processPayment]" reason="[money-moving path; callers in 3 services]"/>
|
|
32
|
+
</critical_paths>
|
|
33
|
+
|
|
34
|
+
<no_go_zones>
|
|
35
|
+
<!-- Code the agent should never explore or edit. -->
|
|
36
|
+
<zone path="[src/generated/]" reason="[generated — regenerate, never edit]"/>
|
|
37
|
+
<zone path="[legacy/]" reason="[frozen for deprecation]"/>
|
|
38
|
+
</no_go_zones>
|
|
39
|
+
|
|
40
|
+
<house_rules>
|
|
41
|
+
<!-- Project-specific conventions the agent must respect. -->
|
|
42
|
+
<rule>[Example: all new endpoints require a schema in src/schemas/.]</rule>
|
|
43
|
+
<rule>[Example: feature flags are checked via FlagService, never process.env.]</rule>
|
|
44
|
+
</house_rules>
|
|
45
|
+
|
|
46
|
+
</domain_prompt>
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
# Integrating the graph-indexer prompt suite
|
|
2
|
+
|
|
3
|
+
The suite is a **3-layer architecture**. Each layer is a standalone XML block; you combine them by **concatenation, in layer order**. Never merge layers into one hand-edited blob ("god prompt") — that is what causes context dilution and framework hallucinations.
|
|
4
|
+
|
|
5
|
+
```
|
|
6
|
+
prompts/
|
|
7
|
+
├── CORE.md Layer 1 — universal rules (always required)
|
|
8
|
+
├── languages/ Layer 2 — pick the ones matching your code
|
|
9
|
+
│ ├── JAVASCRIPT_TYPESCRIPT.md
|
|
10
|
+
│ ├── PYTHON.md
|
|
11
|
+
│ ├── GO.md
|
|
12
|
+
│ ├── RUST.md
|
|
13
|
+
│ ├── JAVA.md
|
|
14
|
+
│ ├── KOTLIN.md
|
|
15
|
+
│ ├── CSHARP.md
|
|
16
|
+
│ ├── RUBY.md
|
|
17
|
+
│ ├── PHP.md
|
|
18
|
+
│ ├── CSS_SCSS.md
|
|
19
|
+
│ ├── BASH.md
|
|
20
|
+
│ ├── C.md
|
|
21
|
+
│ └── SWIFT.md
|
|
22
|
+
├── frameworks/ Layer 2 — pick the ones matching your stack
|
|
23
|
+
│ ├── REACT.md
|
|
24
|
+
│ ├── NODE_EXPRESS_NESTJS.md
|
|
25
|
+
│ ├── FASTAPI_DJANGO.md
|
|
26
|
+
│ ├── SPRING_BOOT.md
|
|
27
|
+
│ ├── RAILS.md
|
|
28
|
+
│ ├── LARAVEL_SYMFONY.md
|
|
29
|
+
│ ├── ASPNET_CORE.md
|
|
30
|
+
│ └── ANDROID.md
|
|
31
|
+
└── DOMAIN_TEMPLATE.md Layer 3 — copy, then fill with YOUR project's rules
|
|
32
|
+
```
|
|
33
|
+
|
|
34
|
+
## The assembly rule
|
|
35
|
+
|
|
36
|
+
```
|
|
37
|
+
Layer 1 (CORE) → Layer 2 (language) → Layer 2 (framework) → Layer 3 (domain)
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
1. **Order matters.** Core first, then language, then framework, then domain. Frameworks assume their base language layer is already loaded (`requires=` attribute in each file).
|
|
41
|
+
2. **Precedence on conflict:** a more specific layer *refines* a more general one (e.g. REACT's render-site protocol refines the core call-graph rule). But Layer 1's `<hard_limits>`, `<prime_directive>`, and `<fallback_protocol>` are **inviolable** — no lower layer may relax them.
|
|
42
|
+
3. **Only include layers you need.** A Go microservice needs `CORE + GO` — nothing else. Every unused layer dilutes the agent's attention.
|
|
43
|
+
|
|
44
|
+
## Automatic setup (recommended)
|
|
45
|
+
|
|
46
|
+
```bash
|
|
47
|
+
npx graph-indexer init
|
|
48
|
+
```
|
|
49
|
+
|
|
50
|
+
`init` asks for your languages and frameworks, then lets you **multi-select which agents/IDEs to wire** (Claude Code/Desktop, Cursor, VS Code/Copilot, Windsurf, Cline/Roo, JetBrains Junie, Codex/AGENTS.md, Gemini CLI). The choice is pre-checked from what's detected (or your last run) and remembered in `.graph-indexer/config.json`, so only the agents you pick get files — the two `GRAPH_INDEXER_*.md` source files are always written. For each selected agent it generates:
|
|
51
|
+
|
|
52
|
+
| File | Agent | Ownership |
|
|
53
|
+
| :--- | :--- | :--- |
|
|
54
|
+
| `GRAPH_INDEXER_PROMPT.md` | source of truth (Layers 1 + 2, assembled for your selection) | Generated — re-run `init` to regenerate; do not edit |
|
|
55
|
+
| `GRAPH_INDEXER_DOMAIN.md` | source of truth (Layer 3 template) | **Yours** — fill it in; `init` never overwrites it |
|
|
56
|
+
| `CLAUDE.md` | Claude Code (`@`-import lines) | Appended once, idempotent |
|
|
57
|
+
| `.cursor/rules/graph-indexer.mdc` | Cursor (always-on rule) | Generated — regenerated on re-run |
|
|
58
|
+
| `.windsurf/rules/graph-indexer.md` | Windsurf (always-on rule) | Generated — regenerated on re-run |
|
|
59
|
+
| `.clinerules/graph-indexer.md` | Cline / Roo Code (rule) | Generated — regenerated on re-run |
|
|
60
|
+
| `.github/copilot-instructions.md` | GitHub Copilot (managed block) | Shared — your text outside the markers is preserved |
|
|
61
|
+
| `.junie/guidelines.md` | JetBrains Junie (managed block) | Shared — your text outside the markers is preserved |
|
|
62
|
+
| `AGENTS.md` | AGENTS.md standard: Codex, Zed, Jules, … (managed block) | Shared — your text outside the markers is preserved |
|
|
63
|
+
| `GEMINI.md` | Gemini CLI (`@`-import) | Shared — references the canonical files (no duplication); your text outside the markers is preserved |
|
|
64
|
+
|
|
65
|
+
**Why some files reference and others embed.** `CLAUDE.md` and `GEMINI.md` use `@`-import (Claude Code imports, Gemini CLI's Memory Import Processor), so they just point at the canonical `GRAPH_INDEXER_*.md` — zero duplication. The Cursor/Windsurf/Cline rule files hold the assembled prompt verbatim, and the Copilot/Junie/AGENTS.md managed blocks embed it inline, because those tools read the file **literally** — they have no reliable import mechanism, so an `@`-reference would appear as plain text and the rules would silently not apply. (AGENTS.md embeds because its main reader, OpenAI Codex, does not yet honour `@` imports.) Embedded blocks are wrapped in `<!-- >>> graph-indexer >>> -->` … `<!-- <<< graph-indexer <<< -->` markers, so your own instructions around them survive re-runs.
|
|
66
|
+
|
|
67
|
+
## Manual setup
|
|
68
|
+
|
|
69
|
+
`init` already wires Claude Code, Cursor, Windsurf, Cline/Roo, Copilot, Junie, the AGENTS.md ecosystem, and Gemini (see the table above). Reach for the steps below only for a legacy format (`.cursorrules`, `.clauderc`), a raw-system-prompt config, or an agent not in that list.
|
|
70
|
+
|
|
71
|
+
### Claude Code (`CLAUDE.md` or `.clauderc`)
|
|
72
|
+
|
|
73
|
+
Claude Code loads `CLAUDE.md` from the project root and supports `@path` imports — reference the assembled files instead of pasting them:
|
|
74
|
+
|
|
75
|
+
```markdown
|
|
76
|
+
# CLAUDE.md
|
|
77
|
+
@GRAPH_INDEXER_PROMPT.md
|
|
78
|
+
@GRAPH_INDEXER_DOMAIN.md
|
|
79
|
+
```
|
|
80
|
+
|
|
81
|
+
If your tooling uses a `.clauderc` (or any agent config that takes a raw system prompt), paste the concatenated layers directly, in layer order.
|
|
82
|
+
|
|
83
|
+
### Cursor (`.cursorrules` or `.cursor/rules/`)
|
|
84
|
+
|
|
85
|
+
For the legacy single-file format, concatenate the layers into `.cursorrules`:
|
|
86
|
+
|
|
87
|
+
```bash
|
|
88
|
+
cat node_modules/graph-indexer/prompts/CORE.md \
|
|
89
|
+
node_modules/graph-indexer/prompts/languages/JAVASCRIPT_TYPESCRIPT.md \
|
|
90
|
+
node_modules/graph-indexer/prompts/frameworks/REACT.md \
|
|
91
|
+
GRAPH_INDEXER_DOMAIN.md > .cursorrules
|
|
92
|
+
```
|
|
93
|
+
|
|
94
|
+
For the modern format, put the same concatenation in `.cursor/rules/graph-indexer.mdc` with this frontmatter:
|
|
95
|
+
|
|
96
|
+
```yaml
|
|
97
|
+
---
|
|
98
|
+
description: graph-indexer usage rules
|
|
99
|
+
alwaysApply: true
|
|
100
|
+
---
|
|
101
|
+
```
|
|
102
|
+
|
|
103
|
+
### Any other agent
|
|
104
|
+
|
|
105
|
+
Concatenate the layers (in order) into whatever field your agent calls "system prompt", "rules", or "custom instructions". The XML structure is self-describing — no other formatting is required.
|
|
106
|
+
|
|
107
|
+
## Worked examples
|
|
108
|
+
|
|
109
|
+
| Stack | Assembly |
|
|
110
|
+
| :--- | :--- |
|
|
111
|
+
| React SPA | `CORE + languages/JAVASCRIPT_TYPESCRIPT + languages/CSS_SCSS + frameworks/REACT + domain` |
|
|
112
|
+
| NestJS API + React front-end (monorepo) | `CORE + languages/JAVASCRIPT_TYPESCRIPT + frameworks/NODE_EXPRESS_NESTJS + frameworks/REACT + domain` |
|
|
113
|
+
| Django service | `CORE + languages/PYTHON + frameworks/FASTAPI_DJANGO + domain` |
|
|
114
|
+
| Spring Boot backend (Java or Kotlin) | `CORE + languages/JAVA` (or `KOTLIN`) `+ frameworks/SPRING_BOOT + domain` |
|
|
115
|
+
| Rails app | `CORE + languages/RUBY + frameworks/RAILS + domain` |
|
|
116
|
+
| Laravel / Symfony app | `CORE + languages/PHP + frameworks/LARAVEL_SYMFONY + domain` |
|
|
117
|
+
| ASP.NET Core API | `CORE + languages/CSHARP + frameworks/ASPNET_CORE + domain` |
|
|
118
|
+
| Android app | `CORE + languages/KOTLIN + frameworks/ANDROID + domain` |
|
|
119
|
+
| Go / Rust service | `CORE + languages/GO` (or `RUST`) `+ domain` |
|
|
120
|
+
|
|
121
|
+
## Layer 3 — make it yours
|
|
122
|
+
|
|
123
|
+
Copy `DOMAIN_TEMPLATE.md` (or let `init` create `GRAPH_INDEXER_DOMAIN.md`), then fill in entry points, domain vocabulary, critical paths, and no-go zones. Keep it under ~40 lines: Layer 3 exists to make the agent's *first* query hit rank-1, not to restate your architecture docs.
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
<environment_prompt layer="2" type="framework" name="android" requires="graph-indexer-core, kotlin" version="1.0">
|
|
2
|
+
|
|
3
|
+
<scope>Android (Jetpack, Compose or XML views, coroutines). Loads after the kotlin layer.</scope>
|
|
4
|
+
|
|
5
|
+
<critical_fact>Activities, Fragments, Services, ViewModels, BroadcastReceivers are instantiated and driven by the OS: lifecycle methods (onCreate, onResume, onCleared...) have NO inbound edges — empty call graphs mean FRAMEWORK-INVOKED, never dead. Who launches a screen lives in navigation graphs, manifest entries, and intent construction — ONE exact_tokens search on the screen/class name finds the launchers.</critical_fact>
|
|
6
|
+
|
|
7
|
+
<rules>
|
|
8
|
+
<rule name="compose-vs-xml">Composables are ordinary Kotlin functions: get_call_graph("OrderScreen") DOES find composition sites — the graph works for Compose UI, unlike JSX. What it can't see is RECOMPOSITION: state-driven re-execution is runtime — answer "when does this re-render" from the composable's chunk (its state reads), never the graph. XML layouts, nav graphs, manifest are non-code resources (fallback condition 3); the code↔layout link is the resource NAME — search R.layout.x-style identifiers or the binding class name.</rule>
|
|
9
|
+
<rule name="generated-artifacts">R, BuildConfig, ViewBinding/DataBinding classes, generated DAO impls have NO chunks — the annotated interface or XML they were generated FROM is the answer; never hunt for generated bodies.</rule>
|
|
10
|
+
<rule name="di-annotations">Hilt/Dagger injection is annotation-driven (@Inject, @Provides, @Module, @HiltViewModel): the container builds the graph, so injection sites carry no caller edges. ONE exact_tokens search on the injected TYPE finds its module/provider and its consumers together.</rule>
|
|
11
|
+
<rule name="coroutines-and-state">viewModelScope.launch and Flow pipelines decouple call sites from execution (kotlin layer). UI state connects ViewModel → UI by the STATE property name — ONE search of that name finds emitter and collectors together. Click listeners and observers are lambdas with no symbols — find them behaviourally inside the owning screen's chunk.</rule>
|
|
12
|
+
<rule name="query-style">Screen names, state property names, resource identifiers, DI types are the highest-precision tokens. Intent actions and navigation route strings trace inter-screen flow better than any graph.</rule>
|
|
13
|
+
</rules>
|
|
14
|
+
|
|
15
|
+
<playbook question="explain screen X" calls="2-4">resolve_symbol("XViewModel" or "XScreen") → get_chunk(the one whose logic is in question — ViewModel for behaviour, composable for UI) → ONE exact_tokens search for the launcher or the state property → answer.</playbook>
|
|
16
|
+
|
|
17
|
+
</environment_prompt>
|