gitnexus 1.2.7 → 1.2.8
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/README.md +186 -186
- package/dist/cli/ai-context.js +71 -71
- package/dist/cli/analyze.d.ts +1 -1
- package/dist/cli/analyze.js +59 -15
- package/dist/cli/index.js +1 -1
- package/dist/core/augmentation/engine.js +20 -20
- package/dist/core/embeddings/embedder.js +1 -0
- package/dist/core/embeddings/embedding-pipeline.js +26 -26
- package/dist/core/ingestion/cluster-enricher.js +16 -16
- package/dist/core/ingestion/tree-sitter-queries.js +282 -282
- package/dist/core/kuzu/kuzu-adapter.js +9 -9
- package/dist/core/kuzu/schema.js +256 -256
- package/dist/core/search/bm25-index.js +5 -5
- package/dist/core/search/hybrid-search.js +3 -3
- package/dist/core/wiki/graph-queries.js +52 -52
- package/dist/core/wiki/html-viewer.js +192 -192
- package/dist/core/wiki/prompts.js +82 -82
- package/dist/mcp/local/local-backend.js +112 -112
- package/dist/mcp/resources.js +42 -42
- package/dist/mcp/server.js +16 -16
- package/dist/mcp/tools.js +77 -77
- package/hooks/claude/gitnexus-hook.cjs +135 -135
- package/hooks/claude/pre-tool-use.sh +78 -78
- package/hooks/claude/session-start.sh +42 -42
- package/package.json +82 -82
- package/skills/debugging.md +85 -85
- package/skills/exploring.md +75 -75
- package/skills/impact-analysis.md +94 -94
- package/skills/refactoring.md +113 -113
- package/vendor/leiden/index.cjs +355 -355
- package/vendor/leiden/utils.cjs +392 -392
|
@@ -5,98 +5,98 @@
|
|
|
5
5
|
* Templates use {{PLACEHOLDER}} substitution.
|
|
6
6
|
*/
|
|
7
7
|
// ─── Grouping Prompt ──────────────────────────────────────────────────
|
|
8
|
-
export const GROUPING_SYSTEM_PROMPT = `You are a documentation architect. Given a list of source files with their exported symbols, group them into logical documentation modules.
|
|
9
|
-
|
|
10
|
-
Rules:
|
|
11
|
-
- Each module should represent a cohesive feature, layer, or domain
|
|
12
|
-
- Every file must appear in exactly one module
|
|
13
|
-
- Module names should be human-readable (e.g. "Authentication", "Database Layer", "API Routes")
|
|
14
|
-
- Aim for 5-15 modules for a typical project. Fewer for small projects, more for large ones
|
|
15
|
-
- Group by functionality, not by file type or directory structure alone
|
|
8
|
+
export const GROUPING_SYSTEM_PROMPT = `You are a documentation architect. Given a list of source files with their exported symbols, group them into logical documentation modules.
|
|
9
|
+
|
|
10
|
+
Rules:
|
|
11
|
+
- Each module should represent a cohesive feature, layer, or domain
|
|
12
|
+
- Every file must appear in exactly one module
|
|
13
|
+
- Module names should be human-readable (e.g. "Authentication", "Database Layer", "API Routes")
|
|
14
|
+
- Aim for 5-15 modules for a typical project. Fewer for small projects, more for large ones
|
|
15
|
+
- Group by functionality, not by file type or directory structure alone
|
|
16
16
|
- Do NOT create modules for tests, configs, or non-source files`;
|
|
17
|
-
export const GROUPING_USER_PROMPT = `Group these source files into documentation modules.
|
|
18
|
-
|
|
19
|
-
**Files and their exports:**
|
|
20
|
-
{{FILE_LIST}}
|
|
21
|
-
|
|
22
|
-
**Directory structure:**
|
|
23
|
-
{{DIRECTORY_TREE}}
|
|
24
|
-
|
|
25
|
-
Respond with ONLY a JSON object mapping module names to file path arrays. No markdown, no explanation.
|
|
26
|
-
Example format:
|
|
27
|
-
{
|
|
28
|
-
"Authentication": ["src/auth/login.ts", "src/auth/session.ts"],
|
|
29
|
-
"Database": ["src/db/connection.ts", "src/db/models.ts"]
|
|
17
|
+
export const GROUPING_USER_PROMPT = `Group these source files into documentation modules.
|
|
18
|
+
|
|
19
|
+
**Files and their exports:**
|
|
20
|
+
{{FILE_LIST}}
|
|
21
|
+
|
|
22
|
+
**Directory structure:**
|
|
23
|
+
{{DIRECTORY_TREE}}
|
|
24
|
+
|
|
25
|
+
Respond with ONLY a JSON object mapping module names to file path arrays. No markdown, no explanation.
|
|
26
|
+
Example format:
|
|
27
|
+
{
|
|
28
|
+
"Authentication": ["src/auth/login.ts", "src/auth/session.ts"],
|
|
29
|
+
"Database": ["src/db/connection.ts", "src/db/models.ts"]
|
|
30
30
|
}`;
|
|
31
31
|
// ─── Leaf Module Prompt ───────────────────────────────────────────────
|
|
32
|
-
export const MODULE_SYSTEM_PROMPT = `You are a technical documentation writer. Write clear, developer-focused documentation for a code module.
|
|
33
|
-
|
|
34
|
-
Rules:
|
|
35
|
-
- Reference actual function names, class names, and code patterns — do NOT invent APIs
|
|
36
|
-
- Use the call graph and execution flow data for accuracy, but do NOT mechanically list every edge
|
|
37
|
-
- Include Mermaid diagrams only when they genuinely help understanding. Keep them small (5-10 nodes max)
|
|
38
|
-
- Structure the document however makes sense for this module — there is no mandatory format
|
|
32
|
+
export const MODULE_SYSTEM_PROMPT = `You are a technical documentation writer. Write clear, developer-focused documentation for a code module.
|
|
33
|
+
|
|
34
|
+
Rules:
|
|
35
|
+
- Reference actual function names, class names, and code patterns — do NOT invent APIs
|
|
36
|
+
- Use the call graph and execution flow data for accuracy, but do NOT mechanically list every edge
|
|
37
|
+
- Include Mermaid diagrams only when they genuinely help understanding. Keep them small (5-10 nodes max)
|
|
38
|
+
- Structure the document however makes sense for this module — there is no mandatory format
|
|
39
39
|
- Write for a developer who needs to understand and contribute to this code`;
|
|
40
|
-
export const MODULE_USER_PROMPT = `Write documentation for the **{{MODULE_NAME}}** module.
|
|
41
|
-
|
|
42
|
-
## Source Code
|
|
43
|
-
|
|
44
|
-
{{SOURCE_CODE}}
|
|
45
|
-
|
|
46
|
-
## Call Graph & Execution Flows (reference for accuracy)
|
|
47
|
-
|
|
48
|
-
Internal calls: {{INTRA_CALLS}}
|
|
49
|
-
Outgoing calls: {{OUTGOING_CALLS}}
|
|
50
|
-
Incoming calls: {{INCOMING_CALLS}}
|
|
51
|
-
Execution flows: {{PROCESSES}}
|
|
52
|
-
|
|
53
|
-
---
|
|
54
|
-
|
|
40
|
+
export const MODULE_USER_PROMPT = `Write documentation for the **{{MODULE_NAME}}** module.
|
|
41
|
+
|
|
42
|
+
## Source Code
|
|
43
|
+
|
|
44
|
+
{{SOURCE_CODE}}
|
|
45
|
+
|
|
46
|
+
## Call Graph & Execution Flows (reference for accuracy)
|
|
47
|
+
|
|
48
|
+
Internal calls: {{INTRA_CALLS}}
|
|
49
|
+
Outgoing calls: {{OUTGOING_CALLS}}
|
|
50
|
+
Incoming calls: {{INCOMING_CALLS}}
|
|
51
|
+
Execution flows: {{PROCESSES}}
|
|
52
|
+
|
|
53
|
+
---
|
|
54
|
+
|
|
55
55
|
Write comprehensive documentation for this module. Cover its purpose, how it works, its key components, and how it connects to the rest of the codebase. Use whatever structure best fits this module — you decide the sections and headings. Include a Mermaid diagram only if it genuinely clarifies the architecture.`;
|
|
56
56
|
// ─── Parent Module Prompt ─────────────────────────────────────────────
|
|
57
|
-
export const PARENT_SYSTEM_PROMPT = `You are a technical documentation writer. Write a summary page for a module that contains sub-modules. Synthesize the children's documentation — do not re-read source code.
|
|
58
|
-
|
|
59
|
-
Rules:
|
|
60
|
-
- Reference actual components from the child modules
|
|
61
|
-
- Focus on how the sub-modules work together, not repeating their individual docs
|
|
62
|
-
- Keep it concise — the reader can click through to child pages for detail
|
|
57
|
+
export const PARENT_SYSTEM_PROMPT = `You are a technical documentation writer. Write a summary page for a module that contains sub-modules. Synthesize the children's documentation — do not re-read source code.
|
|
58
|
+
|
|
59
|
+
Rules:
|
|
60
|
+
- Reference actual components from the child modules
|
|
61
|
+
- Focus on how the sub-modules work together, not repeating their individual docs
|
|
62
|
+
- Keep it concise — the reader can click through to child pages for detail
|
|
63
63
|
- Include a Mermaid diagram only if it genuinely clarifies how the sub-modules relate`;
|
|
64
|
-
export const PARENT_USER_PROMPT = `Write documentation for the **{{MODULE_NAME}}** module, which contains these sub-modules:
|
|
65
|
-
|
|
66
|
-
{{CHILDREN_DOCS}}
|
|
67
|
-
|
|
68
|
-
Cross-module calls: {{CROSS_MODULE_CALLS}}
|
|
69
|
-
Shared execution flows: {{CROSS_PROCESSES}}
|
|
70
|
-
|
|
71
|
-
---
|
|
72
|
-
|
|
64
|
+
export const PARENT_USER_PROMPT = `Write documentation for the **{{MODULE_NAME}}** module, which contains these sub-modules:
|
|
65
|
+
|
|
66
|
+
{{CHILDREN_DOCS}}
|
|
67
|
+
|
|
68
|
+
Cross-module calls: {{CROSS_MODULE_CALLS}}
|
|
69
|
+
Shared execution flows: {{CROSS_PROCESSES}}
|
|
70
|
+
|
|
71
|
+
---
|
|
72
|
+
|
|
73
73
|
Write a concise overview of this module group. Explain its purpose, how the sub-modules fit together, and the key workflows that span them. Link to sub-module pages (e.g. \`[Sub-module Name](sub-module-slug.md)\`) rather than repeating their content. Use whatever structure fits best.`;
|
|
74
74
|
// ─── Overview Prompt ──────────────────────────────────────────────────
|
|
75
|
-
export const OVERVIEW_SYSTEM_PROMPT = `You are a technical documentation writer. Write the top-level overview page for a repository wiki. This is the first page a new developer sees.
|
|
76
|
-
|
|
77
|
-
Rules:
|
|
78
|
-
- Be clear and welcoming — this is the entry point to the entire codebase
|
|
79
|
-
- Reference actual module names so readers can navigate to their docs
|
|
80
|
-
- Include a high-level Mermaid architecture diagram showing only the most important modules and their relationships (max 10 nodes). A new dev should grasp it in 10 seconds
|
|
81
|
-
- Do NOT create module index tables or list every module with descriptions — just link to module pages naturally within the text
|
|
75
|
+
export const OVERVIEW_SYSTEM_PROMPT = `You are a technical documentation writer. Write the top-level overview page for a repository wiki. This is the first page a new developer sees.
|
|
76
|
+
|
|
77
|
+
Rules:
|
|
78
|
+
- Be clear and welcoming — this is the entry point to the entire codebase
|
|
79
|
+
- Reference actual module names so readers can navigate to their docs
|
|
80
|
+
- Include a high-level Mermaid architecture diagram showing only the most important modules and their relationships (max 10 nodes). A new dev should grasp it in 10 seconds
|
|
81
|
+
- Do NOT create module index tables or list every module with descriptions — just link to module pages naturally within the text
|
|
82
82
|
- Use the inter-module edges and execution flow data for accuracy, but do NOT dump them raw`;
|
|
83
|
-
export const OVERVIEW_USER_PROMPT = `Write the overview page for this repository's wiki.
|
|
84
|
-
|
|
85
|
-
## Project Info
|
|
86
|
-
|
|
87
|
-
{{PROJECT_INFO}}
|
|
88
|
-
|
|
89
|
-
## Module Summaries
|
|
90
|
-
|
|
91
|
-
{{MODULE_SUMMARIES}}
|
|
92
|
-
|
|
93
|
-
## Reference Data (for accuracy — do not reproduce verbatim)
|
|
94
|
-
|
|
95
|
-
Inter-module call edges: {{MODULE_EDGES}}
|
|
96
|
-
Key system flows: {{TOP_PROCESSES}}
|
|
97
|
-
|
|
98
|
-
---
|
|
99
|
-
|
|
83
|
+
export const OVERVIEW_USER_PROMPT = `Write the overview page for this repository's wiki.
|
|
84
|
+
|
|
85
|
+
## Project Info
|
|
86
|
+
|
|
87
|
+
{{PROJECT_INFO}}
|
|
88
|
+
|
|
89
|
+
## Module Summaries
|
|
90
|
+
|
|
91
|
+
{{MODULE_SUMMARIES}}
|
|
92
|
+
|
|
93
|
+
## Reference Data (for accuracy — do not reproduce verbatim)
|
|
94
|
+
|
|
95
|
+
Inter-module call edges: {{MODULE_EDGES}}
|
|
96
|
+
Key system flows: {{TOP_PROCESSES}}
|
|
97
|
+
|
|
98
|
+
---
|
|
99
|
+
|
|
100
100
|
Write a clear overview of this project: what it does, how it's architected, and the key end-to-end flows. Include a simple Mermaid architecture diagram (max 10 nodes, big-picture only). Link to module pages (e.g. \`[Module Name](module-slug.md)\`) naturally in the text rather than listing them in a table. If project config was provided, include brief setup instructions. Structure the page however reads best.`;
|
|
101
101
|
// ─── Template Substitution Helper ─────────────────────────────────────
|
|
102
102
|
/**
|
|
@@ -320,19 +320,19 @@ export class LocalBackend {
|
|
|
320
320
|
// Find processes this symbol participates in
|
|
321
321
|
let processRows = [];
|
|
322
322
|
try {
|
|
323
|
-
processRows = await executeQuery(repo.id, `
|
|
324
|
-
MATCH (n {id: '${escaped}'})-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
|
325
|
-
RETURN p.id AS pid, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount, r.step AS step
|
|
323
|
+
processRows = await executeQuery(repo.id, `
|
|
324
|
+
MATCH (n {id: '${escaped}'})-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
|
325
|
+
RETURN p.id AS pid, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount, r.step AS step
|
|
326
326
|
`);
|
|
327
327
|
}
|
|
328
328
|
catch { /* symbol might not be in any process */ }
|
|
329
329
|
// Get cluster cohesion as internal ranking signal (never exposed)
|
|
330
330
|
let cohesion = 0;
|
|
331
331
|
try {
|
|
332
|
-
const cohesionRows = await executeQuery(repo.id, `
|
|
333
|
-
MATCH (n {id: '${escaped}'})-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
|
|
334
|
-
RETURN c.cohesion AS cohesion
|
|
335
|
-
LIMIT 1
|
|
332
|
+
const cohesionRows = await executeQuery(repo.id, `
|
|
333
|
+
MATCH (n {id: '${escaped}'})-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
|
|
334
|
+
RETURN c.cohesion AS cohesion
|
|
335
|
+
LIMIT 1
|
|
336
336
|
`);
|
|
337
337
|
if (cohesionRows.length > 0) {
|
|
338
338
|
cohesion = (cohesionRows[0].cohesion ?? cohesionRows[0][0]) || 0;
|
|
@@ -343,9 +343,9 @@ export class LocalBackend {
|
|
|
343
343
|
let content;
|
|
344
344
|
if (includeContent) {
|
|
345
345
|
try {
|
|
346
|
-
const contentRows = await executeQuery(repo.id, `
|
|
347
|
-
MATCH (n {id: '${escaped}'})
|
|
348
|
-
RETURN n.content AS content
|
|
346
|
+
const contentRows = await executeQuery(repo.id, `
|
|
347
|
+
MATCH (n {id: '${escaped}'})
|
|
348
|
+
RETURN n.content AS content
|
|
349
349
|
`);
|
|
350
350
|
if (contentRows.length > 0) {
|
|
351
351
|
content = contentRows[0].content ?? contentRows[0][0];
|
|
@@ -450,11 +450,11 @@ export class LocalBackend {
|
|
|
450
450
|
for (const bm25Result of bm25Results) {
|
|
451
451
|
const fullPath = bm25Result.filePath;
|
|
452
452
|
try {
|
|
453
|
-
const symbolQuery = `
|
|
454
|
-
MATCH (n)
|
|
455
|
-
WHERE n.filePath = '${fullPath.replace(/'/g, "''")}'
|
|
456
|
-
RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine
|
|
457
|
-
LIMIT 3
|
|
453
|
+
const symbolQuery = `
|
|
454
|
+
MATCH (n)
|
|
455
|
+
WHERE n.filePath = '${fullPath.replace(/'/g, "''")}'
|
|
456
|
+
RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine
|
|
457
|
+
LIMIT 3
|
|
458
458
|
`;
|
|
459
459
|
const symbols = await executeQuery(repo.id, symbolQuery);
|
|
460
460
|
if (symbols.length > 0) {
|
|
@@ -500,14 +500,14 @@ export class LocalBackend {
|
|
|
500
500
|
const queryVec = await embedQuery(query);
|
|
501
501
|
const dims = getEmbeddingDims();
|
|
502
502
|
const queryVecStr = `[${queryVec.join(',')}]`;
|
|
503
|
-
const vectorQuery = `
|
|
504
|
-
CALL QUERY_VECTOR_INDEX('CodeEmbedding', 'code_embedding_idx',
|
|
505
|
-
CAST(${queryVecStr} AS FLOAT[${dims}]), ${limit})
|
|
506
|
-
YIELD node AS emb, distance
|
|
507
|
-
WITH emb, distance
|
|
508
|
-
WHERE distance < 0.6
|
|
509
|
-
RETURN emb.nodeId AS nodeId, distance
|
|
510
|
-
ORDER BY distance
|
|
503
|
+
const vectorQuery = `
|
|
504
|
+
CALL QUERY_VECTOR_INDEX('CodeEmbedding', 'code_embedding_idx',
|
|
505
|
+
CAST(${queryVecStr} AS FLOAT[${dims}]), ${limit})
|
|
506
|
+
YIELD node AS emb, distance
|
|
507
|
+
WITH emb, distance
|
|
508
|
+
WHERE distance < 0.6
|
|
509
|
+
RETURN emb.nodeId AS nodeId, distance
|
|
510
|
+
ORDER BY distance
|
|
511
511
|
`;
|
|
512
512
|
const embResults = await executeQuery(repo.id, vectorQuery);
|
|
513
513
|
if (embResults.length === 0)
|
|
@@ -612,11 +612,11 @@ export class LocalBackend {
|
|
|
612
612
|
try {
|
|
613
613
|
// Fetch more raw communities than the display limit so aggregation has enough data
|
|
614
614
|
const rawLimit = Math.max(limit * 5, 200);
|
|
615
|
-
const clusters = await executeQuery(repo.id, `
|
|
616
|
-
MATCH (c:Community)
|
|
617
|
-
RETURN c.id AS id, c.label AS label, c.heuristicLabel AS heuristicLabel, c.cohesion AS cohesion, c.symbolCount AS symbolCount
|
|
618
|
-
ORDER BY c.symbolCount DESC
|
|
619
|
-
LIMIT ${rawLimit}
|
|
615
|
+
const clusters = await executeQuery(repo.id, `
|
|
616
|
+
MATCH (c:Community)
|
|
617
|
+
RETURN c.id AS id, c.label AS label, c.heuristicLabel AS heuristicLabel, c.cohesion AS cohesion, c.symbolCount AS symbolCount
|
|
618
|
+
ORDER BY c.symbolCount DESC
|
|
619
|
+
LIMIT ${rawLimit}
|
|
620
620
|
`);
|
|
621
621
|
const rawClusters = clusters.map((c) => ({
|
|
622
622
|
id: c.id || c[0],
|
|
@@ -633,11 +633,11 @@ export class LocalBackend {
|
|
|
633
633
|
}
|
|
634
634
|
if (params.showProcesses !== false) {
|
|
635
635
|
try {
|
|
636
|
-
const processes = await executeQuery(repo.id, `
|
|
637
|
-
MATCH (p:Process)
|
|
638
|
-
RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount
|
|
639
|
-
ORDER BY p.stepCount DESC
|
|
640
|
-
LIMIT ${limit}
|
|
636
|
+
const processes = await executeQuery(repo.id, `
|
|
637
|
+
MATCH (p:Process)
|
|
638
|
+
RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount
|
|
639
|
+
ORDER BY p.stepCount DESC
|
|
640
|
+
LIMIT ${limit}
|
|
641
641
|
`);
|
|
642
642
|
result.processes = processes.map((p) => ({
|
|
643
643
|
id: p.id || p[0],
|
|
@@ -668,10 +668,10 @@ export class LocalBackend {
|
|
|
668
668
|
let symbols;
|
|
669
669
|
if (uid) {
|
|
670
670
|
const escaped = uid.replace(/'/g, "''");
|
|
671
|
-
symbols = await executeQuery(repo.id, `
|
|
672
|
-
MATCH (n {id: '${escaped}'})
|
|
673
|
-
RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine${include_content ? ', n.content AS content' : ''}
|
|
674
|
-
LIMIT 1
|
|
671
|
+
symbols = await executeQuery(repo.id, `
|
|
672
|
+
MATCH (n {id: '${escaped}'})
|
|
673
|
+
RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine${include_content ? ', n.content AS content' : ''}
|
|
674
|
+
LIMIT 1
|
|
675
675
|
`);
|
|
676
676
|
}
|
|
677
677
|
else {
|
|
@@ -688,10 +688,10 @@ export class LocalBackend {
|
|
|
688
688
|
else {
|
|
689
689
|
whereClause = `WHERE n.name = '${escaped}'`;
|
|
690
690
|
}
|
|
691
|
-
symbols = await executeQuery(repo.id, `
|
|
692
|
-
MATCH (n) ${whereClause}
|
|
693
|
-
RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine${include_content ? ', n.content AS content' : ''}
|
|
694
|
-
LIMIT 10
|
|
691
|
+
symbols = await executeQuery(repo.id, `
|
|
692
|
+
MATCH (n) ${whereClause}
|
|
693
|
+
RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, n.startLine AS startLine, n.endLine AS endLine${include_content ? ', n.content AS content' : ''}
|
|
694
|
+
LIMIT 10
|
|
695
695
|
`);
|
|
696
696
|
}
|
|
697
697
|
if (symbols.length === 0) {
|
|
@@ -715,25 +715,25 @@ export class LocalBackend {
|
|
|
715
715
|
const sym = symbols[0];
|
|
716
716
|
const symId = (sym.id || sym[0]).replace(/'/g, "''");
|
|
717
717
|
// Categorized incoming refs
|
|
718
|
-
const incomingRows = await executeQuery(repo.id, `
|
|
719
|
-
MATCH (caller)-[r:CodeRelation]->(n {id: '${symId}'})
|
|
720
|
-
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS']
|
|
721
|
-
RETURN r.type AS relType, caller.id AS uid, caller.name AS name, caller.filePath AS filePath, labels(caller)[0] AS kind
|
|
722
|
-
LIMIT 30
|
|
718
|
+
const incomingRows = await executeQuery(repo.id, `
|
|
719
|
+
MATCH (caller)-[r:CodeRelation]->(n {id: '${symId}'})
|
|
720
|
+
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS']
|
|
721
|
+
RETURN r.type AS relType, caller.id AS uid, caller.name AS name, caller.filePath AS filePath, labels(caller)[0] AS kind
|
|
722
|
+
LIMIT 30
|
|
723
723
|
`);
|
|
724
724
|
// Categorized outgoing refs
|
|
725
|
-
const outgoingRows = await executeQuery(repo.id, `
|
|
726
|
-
MATCH (n {id: '${symId}'})-[r:CodeRelation]->(target)
|
|
727
|
-
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS']
|
|
728
|
-
RETURN r.type AS relType, target.id AS uid, target.name AS name, target.filePath AS filePath, labels(target)[0] AS kind
|
|
729
|
-
LIMIT 30
|
|
725
|
+
const outgoingRows = await executeQuery(repo.id, `
|
|
726
|
+
MATCH (n {id: '${symId}'})-[r:CodeRelation]->(target)
|
|
727
|
+
WHERE r.type IN ['CALLS', 'IMPORTS', 'EXTENDS', 'IMPLEMENTS']
|
|
728
|
+
RETURN r.type AS relType, target.id AS uid, target.name AS name, target.filePath AS filePath, labels(target)[0] AS kind
|
|
729
|
+
LIMIT 30
|
|
730
730
|
`);
|
|
731
731
|
// Process participation
|
|
732
732
|
let processRows = [];
|
|
733
733
|
try {
|
|
734
|
-
processRows = await executeQuery(repo.id, `
|
|
735
|
-
MATCH (n {id: '${symId}'})-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
|
736
|
-
RETURN p.id AS pid, p.heuristicLabel AS label, r.step AS step, p.stepCount AS stepCount
|
|
734
|
+
processRows = await executeQuery(repo.id, `
|
|
735
|
+
MATCH (n {id: '${symId}'})-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
|
736
|
+
RETURN p.id AS pid, p.heuristicLabel AS label, r.step AS step, p.stepCount AS stepCount
|
|
737
737
|
`);
|
|
738
738
|
}
|
|
739
739
|
catch { /* no process info */ }
|
|
@@ -787,10 +787,10 @@ export class LocalBackend {
|
|
|
787
787
|
}
|
|
788
788
|
if (type === 'cluster') {
|
|
789
789
|
const escaped = name.replace(/'/g, "''");
|
|
790
|
-
const clusterQuery = `
|
|
791
|
-
MATCH (c:Community)
|
|
792
|
-
WHERE c.label = '${escaped}' OR c.heuristicLabel = '${escaped}'
|
|
793
|
-
RETURN c.id AS id, c.label AS label, c.heuristicLabel AS heuristicLabel, c.cohesion AS cohesion, c.symbolCount AS symbolCount
|
|
790
|
+
const clusterQuery = `
|
|
791
|
+
MATCH (c:Community)
|
|
792
|
+
WHERE c.label = '${escaped}' OR c.heuristicLabel = '${escaped}'
|
|
793
|
+
RETURN c.id AS id, c.label AS label, c.heuristicLabel AS heuristicLabel, c.cohesion AS cohesion, c.symbolCount AS symbolCount
|
|
794
794
|
`;
|
|
795
795
|
const clusters = await executeQuery(repo.id, clusterQuery);
|
|
796
796
|
if (clusters.length === 0)
|
|
@@ -805,11 +805,11 @@ export class LocalBackend {
|
|
|
805
805
|
totalSymbols += s;
|
|
806
806
|
weightedCohesion += (c.cohesion || 0) * s;
|
|
807
807
|
}
|
|
808
|
-
const members = await executeQuery(repo.id, `
|
|
809
|
-
MATCH (n)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
|
|
810
|
-
WHERE c.label = '${escaped}' OR c.heuristicLabel = '${escaped}'
|
|
811
|
-
RETURN DISTINCT n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
|
|
812
|
-
LIMIT 30
|
|
808
|
+
const members = await executeQuery(repo.id, `
|
|
809
|
+
MATCH (n)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
|
|
810
|
+
WHERE c.label = '${escaped}' OR c.heuristicLabel = '${escaped}'
|
|
811
|
+
RETURN DISTINCT n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
|
|
812
|
+
LIMIT 30
|
|
813
813
|
`);
|
|
814
814
|
return {
|
|
815
815
|
cluster: {
|
|
@@ -826,20 +826,20 @@ export class LocalBackend {
|
|
|
826
826
|
};
|
|
827
827
|
}
|
|
828
828
|
if (type === 'process') {
|
|
829
|
-
const processes = await executeQuery(repo.id, `
|
|
830
|
-
MATCH (p:Process)
|
|
831
|
-
WHERE p.label = '${name.replace(/'/g, "''")}' OR p.heuristicLabel = '${name.replace(/'/g, "''")}'
|
|
832
|
-
RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount
|
|
833
|
-
LIMIT 1
|
|
829
|
+
const processes = await executeQuery(repo.id, `
|
|
830
|
+
MATCH (p:Process)
|
|
831
|
+
WHERE p.label = '${name.replace(/'/g, "''")}' OR p.heuristicLabel = '${name.replace(/'/g, "''")}'
|
|
832
|
+
RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount
|
|
833
|
+
LIMIT 1
|
|
834
834
|
`);
|
|
835
835
|
if (processes.length === 0)
|
|
836
836
|
return { error: `Process '${name}' not found` };
|
|
837
837
|
const proc = processes[0];
|
|
838
838
|
const procId = proc.id || proc[0];
|
|
839
|
-
const steps = await executeQuery(repo.id, `
|
|
840
|
-
MATCH (n)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p {id: '${procId}'})
|
|
841
|
-
RETURN n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, r.step AS step
|
|
842
|
-
ORDER BY r.step
|
|
839
|
+
const steps = await executeQuery(repo.id, `
|
|
840
|
+
MATCH (n)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p {id: '${procId}'})
|
|
841
|
+
RETURN n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, r.step AS step
|
|
842
|
+
ORDER BY r.step
|
|
843
843
|
`);
|
|
844
844
|
return {
|
|
845
845
|
process: {
|
|
@@ -900,10 +900,10 @@ export class LocalBackend {
|
|
|
900
900
|
for (const file of changedFiles) {
|
|
901
901
|
const escaped = file.replace(/\\/g, '/').replace(/'/g, "''");
|
|
902
902
|
try {
|
|
903
|
-
const symbols = await executeQuery(repo.id, `
|
|
904
|
-
MATCH (n) WHERE n.filePath CONTAINS '${escaped}'
|
|
905
|
-
RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
|
|
906
|
-
LIMIT 20
|
|
903
|
+
const symbols = await executeQuery(repo.id, `
|
|
904
|
+
MATCH (n) WHERE n.filePath CONTAINS '${escaped}'
|
|
905
|
+
RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
|
|
906
|
+
LIMIT 20
|
|
907
907
|
`);
|
|
908
908
|
for (const sym of symbols) {
|
|
909
909
|
changedSymbols.push({
|
|
@@ -922,9 +922,9 @@ export class LocalBackend {
|
|
|
922
922
|
for (const sym of changedSymbols) {
|
|
923
923
|
const escaped = sym.id.replace(/'/g, "''");
|
|
924
924
|
try {
|
|
925
|
-
const procs = await executeQuery(repo.id, `
|
|
926
|
-
MATCH (n {id: '${escaped}'})-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
|
927
|
-
RETURN p.id AS pid, p.heuristicLabel AS label, p.processType AS processType, p.stepCount AS stepCount, r.step AS step
|
|
925
|
+
const procs = await executeQuery(repo.id, `
|
|
926
|
+
MATCH (n {id: '${escaped}'})-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
|
927
|
+
RETURN p.id AS pid, p.heuristicLabel AS label, p.processType AS processType, p.stepCount AS stepCount, r.step AS step
|
|
928
928
|
`);
|
|
929
929
|
for (const proc of procs) {
|
|
930
930
|
const pid = proc.pid || proc[0];
|
|
@@ -1099,11 +1099,11 @@ export class LocalBackend {
|
|
|
1099
1099
|
const minConfidence = params.minConfidence ?? 0;
|
|
1100
1100
|
const relTypeFilter = relationTypes.map(t => `'${t}'`).join(', ');
|
|
1101
1101
|
const confidenceFilter = minConfidence > 0 ? ` AND r.confidence >= ${minConfidence}` : '';
|
|
1102
|
-
const targetQuery = `
|
|
1103
|
-
MATCH (n)
|
|
1104
|
-
WHERE n.name = '${target.replace(/'/g, "''")}'
|
|
1105
|
-
RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
|
|
1106
|
-
LIMIT 1
|
|
1102
|
+
const targetQuery = `
|
|
1103
|
+
MATCH (n)
|
|
1104
|
+
WHERE n.name = '${target.replace(/'/g, "''")}'
|
|
1105
|
+
RETURN n.id AS id, n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
|
|
1106
|
+
LIMIT 1
|
|
1107
1107
|
`;
|
|
1108
1108
|
const targets = await executeQuery(repo.id, targetQuery);
|
|
1109
1109
|
if (targets.length === 0)
|
|
@@ -1173,11 +1173,11 @@ export class LocalBackend {
|
|
|
1173
1173
|
await this.ensureInitialized(repo.id);
|
|
1174
1174
|
try {
|
|
1175
1175
|
const rawLimit = Math.max(limit * 5, 200);
|
|
1176
|
-
const clusters = await executeQuery(repo.id, `
|
|
1177
|
-
MATCH (c:Community)
|
|
1178
|
-
RETURN c.id AS id, c.label AS label, c.heuristicLabel AS heuristicLabel, c.cohesion AS cohesion, c.symbolCount AS symbolCount
|
|
1179
|
-
ORDER BY c.symbolCount DESC
|
|
1180
|
-
LIMIT ${rawLimit}
|
|
1176
|
+
const clusters = await executeQuery(repo.id, `
|
|
1177
|
+
MATCH (c:Community)
|
|
1178
|
+
RETURN c.id AS id, c.label AS label, c.heuristicLabel AS heuristicLabel, c.cohesion AS cohesion, c.symbolCount AS symbolCount
|
|
1179
|
+
ORDER BY c.symbolCount DESC
|
|
1180
|
+
LIMIT ${rawLimit}
|
|
1181
1181
|
`);
|
|
1182
1182
|
const rawClusters = clusters.map((c) => ({
|
|
1183
1183
|
id: c.id || c[0],
|
|
@@ -1200,11 +1200,11 @@ export class LocalBackend {
|
|
|
1200
1200
|
const repo = await this.resolveRepo(repoName);
|
|
1201
1201
|
await this.ensureInitialized(repo.id);
|
|
1202
1202
|
try {
|
|
1203
|
-
const processes = await executeQuery(repo.id, `
|
|
1204
|
-
MATCH (p:Process)
|
|
1205
|
-
RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount
|
|
1206
|
-
ORDER BY p.stepCount DESC
|
|
1207
|
-
LIMIT ${limit}
|
|
1203
|
+
const processes = await executeQuery(repo.id, `
|
|
1204
|
+
MATCH (p:Process)
|
|
1205
|
+
RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount
|
|
1206
|
+
ORDER BY p.stepCount DESC
|
|
1207
|
+
LIMIT ${limit}
|
|
1208
1208
|
`);
|
|
1209
1209
|
return {
|
|
1210
1210
|
processes: processes.map((p) => ({
|
|
@@ -1228,10 +1228,10 @@ export class LocalBackend {
|
|
|
1228
1228
|
const repo = await this.resolveRepo(repoName);
|
|
1229
1229
|
await this.ensureInitialized(repo.id);
|
|
1230
1230
|
const escaped = name.replace(/'/g, "''");
|
|
1231
|
-
const clusterQuery = `
|
|
1232
|
-
MATCH (c:Community)
|
|
1233
|
-
WHERE c.label = '${escaped}' OR c.heuristicLabel = '${escaped}'
|
|
1234
|
-
RETURN c.id AS id, c.label AS label, c.heuristicLabel AS heuristicLabel, c.cohesion AS cohesion, c.symbolCount AS symbolCount
|
|
1231
|
+
const clusterQuery = `
|
|
1232
|
+
MATCH (c:Community)
|
|
1233
|
+
WHERE c.label = '${escaped}' OR c.heuristicLabel = '${escaped}'
|
|
1234
|
+
RETURN c.id AS id, c.label AS label, c.heuristicLabel AS heuristicLabel, c.cohesion AS cohesion, c.symbolCount AS symbolCount
|
|
1235
1235
|
`;
|
|
1236
1236
|
const clusters = await executeQuery(repo.id, clusterQuery);
|
|
1237
1237
|
if (clusters.length === 0)
|
|
@@ -1246,11 +1246,11 @@ export class LocalBackend {
|
|
|
1246
1246
|
totalSymbols += s;
|
|
1247
1247
|
weightedCohesion += (c.cohesion || 0) * s;
|
|
1248
1248
|
}
|
|
1249
|
-
const members = await executeQuery(repo.id, `
|
|
1250
|
-
MATCH (n)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
|
|
1251
|
-
WHERE c.label = '${escaped}' OR c.heuristicLabel = '${escaped}'
|
|
1252
|
-
RETURN DISTINCT n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
|
|
1253
|
-
LIMIT 30
|
|
1249
|
+
const members = await executeQuery(repo.id, `
|
|
1250
|
+
MATCH (n)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
|
|
1251
|
+
WHERE c.label = '${escaped}' OR c.heuristicLabel = '${escaped}'
|
|
1252
|
+
RETURN DISTINCT n.name AS name, labels(n)[0] AS type, n.filePath AS filePath
|
|
1253
|
+
LIMIT 30
|
|
1254
1254
|
`);
|
|
1255
1255
|
return {
|
|
1256
1256
|
cluster: {
|
|
@@ -1274,20 +1274,20 @@ export class LocalBackend {
|
|
|
1274
1274
|
const repo = await this.resolveRepo(repoName);
|
|
1275
1275
|
await this.ensureInitialized(repo.id);
|
|
1276
1276
|
const escaped = name.replace(/'/g, "''");
|
|
1277
|
-
const processes = await executeQuery(repo.id, `
|
|
1278
|
-
MATCH (p:Process)
|
|
1279
|
-
WHERE p.label = '${escaped}' OR p.heuristicLabel = '${escaped}'
|
|
1280
|
-
RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount
|
|
1281
|
-
LIMIT 1
|
|
1277
|
+
const processes = await executeQuery(repo.id, `
|
|
1278
|
+
MATCH (p:Process)
|
|
1279
|
+
WHERE p.label = '${escaped}' OR p.heuristicLabel = '${escaped}'
|
|
1280
|
+
RETURN p.id AS id, p.label AS label, p.heuristicLabel AS heuristicLabel, p.processType AS processType, p.stepCount AS stepCount
|
|
1281
|
+
LIMIT 1
|
|
1282
1282
|
`);
|
|
1283
1283
|
if (processes.length === 0)
|
|
1284
1284
|
return { error: `Process '${name}' not found` };
|
|
1285
1285
|
const proc = processes[0];
|
|
1286
1286
|
const procId = proc.id || proc[0];
|
|
1287
|
-
const steps = await executeQuery(repo.id, `
|
|
1288
|
-
MATCH (n)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p {id: '${procId}'})
|
|
1289
|
-
RETURN n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, r.step AS step
|
|
1290
|
-
ORDER BY r.step
|
|
1287
|
+
const steps = await executeQuery(repo.id, `
|
|
1288
|
+
MATCH (n)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p {id: '${procId}'})
|
|
1289
|
+
RETURN n.name AS name, labels(n)[0] AS type, n.filePath AS filePath, r.step AS step
|
|
1290
|
+
ORDER BY r.step
|
|
1291
1291
|
`);
|
|
1292
1292
|
return {
|
|
1293
1293
|
process: {
|
package/dist/mcp/resources.js
CHANGED
|
@@ -256,48 +256,48 @@ async function getProcessesResource(backend, repoName) {
|
|
|
256
256
|
* Schema resource — graph structure for Cypher queries
|
|
257
257
|
*/
|
|
258
258
|
function getSchemaResource() {
|
|
259
|
-
return `# GitNexus Graph Schema
|
|
260
|
-
|
|
261
|
-
nodes:
|
|
262
|
-
- File: Source code files
|
|
263
|
-
- Folder: Directory containers
|
|
264
|
-
- Function: Functions and arrow functions
|
|
265
|
-
- Class: Class definitions
|
|
266
|
-
- Interface: Interface/type definitions
|
|
267
|
-
- Method: Class methods
|
|
268
|
-
- CodeElement: Catch-all for other code elements
|
|
269
|
-
- Community: Auto-detected functional area (Leiden algorithm)
|
|
270
|
-
- Process: Execution flow trace
|
|
271
|
-
|
|
272
|
-
additional_node_types: "Multi-language: Struct, Enum, Macro, Typedef, Union, Namespace, Trait, Impl, TypeAlias, Const, Static, Property, Record, Delegate, Annotation, Constructor, Template, Module (use backticks in queries: \`Struct\`, \`Enum\`, etc.)"
|
|
273
|
-
|
|
274
|
-
relationships:
|
|
275
|
-
- CONTAINS: File/Folder contains child
|
|
276
|
-
- DEFINES: File defines a symbol
|
|
277
|
-
- CALLS: Function/method invocation
|
|
278
|
-
- IMPORTS: Module imports
|
|
279
|
-
- EXTENDS: Class inheritance
|
|
280
|
-
- IMPLEMENTS: Interface implementation
|
|
281
|
-
- MEMBER_OF: Symbol belongs to community
|
|
282
|
-
- STEP_IN_PROCESS: Symbol is step N in process
|
|
283
|
-
|
|
284
|
-
relationship_table: "All relationships use a single CodeRelation table with a 'type' property. Properties: type (STRING), confidence (DOUBLE), reason (STRING), step (INT32)"
|
|
285
|
-
|
|
286
|
-
example_queries:
|
|
287
|
-
find_callers: |
|
|
288
|
-
MATCH (caller)-[:CodeRelation {type: 'CALLS'}]->(f:Function {name: "myFunc"})
|
|
289
|
-
RETURN caller.name, caller.filePath
|
|
290
|
-
|
|
291
|
-
find_community_members: |
|
|
292
|
-
MATCH (s)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
|
|
293
|
-
WHERE c.heuristicLabel = "Auth"
|
|
294
|
-
RETURN s.name, labels(s)[0] AS type
|
|
295
|
-
|
|
296
|
-
trace_process: |
|
|
297
|
-
MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
|
298
|
-
WHERE p.heuristicLabel = "LoginFlow"
|
|
299
|
-
RETURN s.name, r.step
|
|
300
|
-
ORDER BY r.step
|
|
259
|
+
return `# GitNexus Graph Schema
|
|
260
|
+
|
|
261
|
+
nodes:
|
|
262
|
+
- File: Source code files
|
|
263
|
+
- Folder: Directory containers
|
|
264
|
+
- Function: Functions and arrow functions
|
|
265
|
+
- Class: Class definitions
|
|
266
|
+
- Interface: Interface/type definitions
|
|
267
|
+
- Method: Class methods
|
|
268
|
+
- CodeElement: Catch-all for other code elements
|
|
269
|
+
- Community: Auto-detected functional area (Leiden algorithm)
|
|
270
|
+
- Process: Execution flow trace
|
|
271
|
+
|
|
272
|
+
additional_node_types: "Multi-language: Struct, Enum, Macro, Typedef, Union, Namespace, Trait, Impl, TypeAlias, Const, Static, Property, Record, Delegate, Annotation, Constructor, Template, Module (use backticks in queries: \`Struct\`, \`Enum\`, etc.)"
|
|
273
|
+
|
|
274
|
+
relationships:
|
|
275
|
+
- CONTAINS: File/Folder contains child
|
|
276
|
+
- DEFINES: File defines a symbol
|
|
277
|
+
- CALLS: Function/method invocation
|
|
278
|
+
- IMPORTS: Module imports
|
|
279
|
+
- EXTENDS: Class inheritance
|
|
280
|
+
- IMPLEMENTS: Interface implementation
|
|
281
|
+
- MEMBER_OF: Symbol belongs to community
|
|
282
|
+
- STEP_IN_PROCESS: Symbol is step N in process
|
|
283
|
+
|
|
284
|
+
relationship_table: "All relationships use a single CodeRelation table with a 'type' property. Properties: type (STRING), confidence (DOUBLE), reason (STRING), step (INT32)"
|
|
285
|
+
|
|
286
|
+
example_queries:
|
|
287
|
+
find_callers: |
|
|
288
|
+
MATCH (caller)-[:CodeRelation {type: 'CALLS'}]->(f:Function {name: "myFunc"})
|
|
289
|
+
RETURN caller.name, caller.filePath
|
|
290
|
+
|
|
291
|
+
find_community_members: |
|
|
292
|
+
MATCH (s)-[:CodeRelation {type: 'MEMBER_OF'}]->(c:Community)
|
|
293
|
+
WHERE c.heuristicLabel = "Auth"
|
|
294
|
+
RETURN s.name, labels(s)[0] AS type
|
|
295
|
+
|
|
296
|
+
trace_process: |
|
|
297
|
+
MATCH (s)-[r:CodeRelation {type: 'STEP_IN_PROCESS'}]->(p:Process)
|
|
298
|
+
WHERE p.heuristicLabel = "LoginFlow"
|
|
299
|
+
RETURN s.name, r.step
|
|
300
|
+
ORDER BY r.step
|
|
301
301
|
`;
|
|
302
302
|
}
|
|
303
303
|
/**
|