@danielblomma/cortex-mcp 1.4.0 → 1.5.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/bin/cortex.mjs +10 -1
- package/package.json +2 -2
- package/scaffold/scripts/parsers/bash-treesitter.mjs +6 -3
- package/scaffold/scripts/parsers/cpp-treesitter.mjs +57 -8
- package/scaffold/scripts/parsers/dotnet/VbNetParser/Program.cs +13 -2
- package/scaffold/scripts/parsers/dotnet/VbNetParser/VbNetParser.csproj +1 -1
- package/scaffold/scripts/parsers/go-treesitter.mjs +8 -5
- package/scaffold/scripts/parsers/java-treesitter.mjs +8 -5
- package/scaffold/scripts/parsers/python-treesitter.mjs +7 -4
- package/scaffold/scripts/parsers/ruby-treesitter.mjs +7 -4
- package/scaffold/scripts/parsers/rust-treesitter.mjs +16 -4
- package/scaffold/scripts/parsers/tree-sitter/base.mjs +74 -2
- package/scaffold/scripts/parsers/vbnet.mjs +109 -10
package/bin/cortex.mjs
CHANGED
|
@@ -734,8 +734,17 @@ async function run() {
|
|
|
734
734
|
await runContextCommand(process.cwd(), [command, ...rest]);
|
|
735
735
|
}
|
|
736
736
|
|
|
737
|
+
function resolveArgv1() {
|
|
738
|
+
if (!process.argv[1]) return null;
|
|
739
|
+
try {
|
|
740
|
+
return fs.realpathSync(process.argv[1]);
|
|
741
|
+
} catch {
|
|
742
|
+
return process.argv[1];
|
|
743
|
+
}
|
|
744
|
+
}
|
|
745
|
+
|
|
737
746
|
const invokedAsScript =
|
|
738
|
-
process.argv[1] && import.meta.url === pathToFileURL(
|
|
747
|
+
process.argv[1] && import.meta.url === pathToFileURL(resolveArgv1()).href;
|
|
739
748
|
|
|
740
749
|
if (invokedAsScript) {
|
|
741
750
|
run().catch((error) => {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@danielblomma/cortex-mcp",
|
|
3
3
|
"mcpName": "io.github.DanielBlomma/cortex",
|
|
4
|
-
"version": "1.
|
|
4
|
+
"version": "1.5.0",
|
|
5
5
|
"description": "Local, repo-scoped context platform for coding assistants. Semantic search, graph relationships, and architectural rule context.",
|
|
6
6
|
"type": "module",
|
|
7
7
|
"author": "Daniel Blomma",
|
|
@@ -46,7 +46,7 @@
|
|
|
46
46
|
"docs/MCP_MARKETPLACE.md"
|
|
47
47
|
],
|
|
48
48
|
"scripts": {
|
|
49
|
-
"test": "node tests/context-regressions.test.mjs && node --test tests/ingest-units.test.mjs tests/javascript-parser.test.mjs tests/sql-parser.test.mjs tests/config-parser.test.mjs tests/resources-parser.test.mjs tests/vbnet-parser.test.mjs tests/cpp-parser.test.mjs tests/multi-level.test.mjs tests/no-legacy-paths.test.mjs",
|
|
49
|
+
"test": "node tests/context-regressions.test.mjs && node --test tests/ingest-units.test.mjs tests/javascript-parser.test.mjs tests/sql-parser.test.mjs tests/config-parser.test.mjs tests/resources-parser.test.mjs tests/vbnet-parser.test.mjs tests/cpp-parser.test.mjs tests/multi-level.test.mjs tests/no-legacy-paths.test.mjs tests/tree-sitter-error-reporting.test.mjs tests/tree-sitter-body-cap.test.mjs tests/tree-sitter-exported.test.mjs tests/tree-sitter-robustness.test.mjs",
|
|
50
50
|
"release:sync-version": "node scripts/sync-release-version.mjs",
|
|
51
51
|
"release:check-version-sync": "node scripts/sync-release-version.mjs --check",
|
|
52
52
|
"prepublishOnly": "echo 'Ready to publish to npm'"
|
|
@@ -35,6 +35,8 @@ import {
|
|
|
35
35
|
initTreeSitter,
|
|
36
36
|
lineRangeOf,
|
|
37
37
|
loadGrammar,
|
|
38
|
+
bodyOf,
|
|
39
|
+
collectErrors,
|
|
38
40
|
parseSource,
|
|
39
41
|
runQuery
|
|
40
42
|
} from "./tree-sitter/base.mjs";
|
|
@@ -181,7 +183,7 @@ function buildFunctionChunk(node, imports, language) {
|
|
|
181
183
|
name,
|
|
182
184
|
kind: "function",
|
|
183
185
|
signature: signatureOfDecl(node),
|
|
184
|
-
body: node
|
|
186
|
+
body: bodyOf(node),
|
|
185
187
|
startLine,
|
|
186
188
|
endLine,
|
|
187
189
|
language,
|
|
@@ -193,7 +195,8 @@ function buildFunctionChunk(node, imports, language) {
|
|
|
193
195
|
|
|
194
196
|
export async function parseCode(code, filePath, language = "bash") {
|
|
195
197
|
await ensureLanguage();
|
|
196
|
-
const { tree } = parseSource(BASH_LANG, code);
|
|
198
|
+
const { tree, reason } = parseSource(BASH_LANG, code);
|
|
199
|
+
if (!tree) return { chunks: [], errors: [{ message: reason }] };
|
|
197
200
|
const root = tree.rootNode;
|
|
198
201
|
const imports = collectImports(root);
|
|
199
202
|
|
|
@@ -214,7 +217,7 @@ export async function parseCode(code, filePath, language = "bash") {
|
|
|
214
217
|
return true;
|
|
215
218
|
});
|
|
216
219
|
|
|
217
|
-
return { chunks: deduped, errors:
|
|
220
|
+
return { chunks: deduped, errors: collectErrors(tree) };
|
|
218
221
|
}
|
|
219
222
|
|
|
220
223
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
@@ -29,6 +29,8 @@ import {
|
|
|
29
29
|
initTreeSitter,
|
|
30
30
|
lineRangeOf,
|
|
31
31
|
loadGrammar,
|
|
32
|
+
bodyOf,
|
|
33
|
+
collectErrors,
|
|
32
34
|
parseSource,
|
|
33
35
|
runQuery
|
|
34
36
|
} from "./tree-sitter/base.mjs";
|
|
@@ -70,6 +72,52 @@ function normalizeWhitespace(value) {
|
|
|
70
72
|
return String(value).replace(/\s+/g, " ").trim();
|
|
71
73
|
}
|
|
72
74
|
|
|
75
|
+
/**
|
|
76
|
+
* Determine whether a declaration is visible from outside its
|
|
77
|
+
* enclosing class/struct. Walks up the AST and, when the nearest
|
|
78
|
+
* class_specifier/struct_specifier ancestor is found, inspects the
|
|
79
|
+
* preceding access_specifier sibling inside the class body. Defaults:
|
|
80
|
+
* `class` members are private until an `access_specifier` says
|
|
81
|
+
* otherwise; `struct`/`union` members are public.
|
|
82
|
+
*
|
|
83
|
+
* Returns true when the declaration is at namespace scope or under
|
|
84
|
+
* a `public:` access specifier.
|
|
85
|
+
*/
|
|
86
|
+
function isCppVisible(node) {
|
|
87
|
+
let current = node;
|
|
88
|
+
while (current?.parent) {
|
|
89
|
+
const parent = current.parent;
|
|
90
|
+
const parentType = parent.type;
|
|
91
|
+
|
|
92
|
+
if (parentType === "field_declaration_list") {
|
|
93
|
+
// web-tree-sitter returns fresh wrapper objects per call, so compare
|
|
94
|
+
// by source position rather than identity.
|
|
95
|
+
let access = null;
|
|
96
|
+
for (let i = 0; i < parent.namedChildCount; i += 1) {
|
|
97
|
+
const sib = parent.namedChild(i);
|
|
98
|
+
if (sib.startIndex === current.startIndex && sib.endIndex === current.endIndex) break;
|
|
99
|
+
if (sib.type === "access_specifier") access = sib.text.trim();
|
|
100
|
+
}
|
|
101
|
+
const enclosing = parent.parent?.type;
|
|
102
|
+
if (access == null) {
|
|
103
|
+
// No access_specifier yet — use the enclosing type's default.
|
|
104
|
+
return enclosing === "struct_specifier" || enclosing === "union_specifier";
|
|
105
|
+
}
|
|
106
|
+
return access === "public";
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
if (parentType === "class_specifier" || parentType === "struct_specifier" || parentType === "union_specifier") {
|
|
110
|
+
// Direct member of a named type body not wrapped in a field list (rare).
|
|
111
|
+
// Treat as if under the default access.
|
|
112
|
+
return parentType !== "class_specifier";
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
current = parent;
|
|
116
|
+
}
|
|
117
|
+
// No enclosing class/struct body — namespace or file scope: always visible.
|
|
118
|
+
return true;
|
|
119
|
+
}
|
|
120
|
+
|
|
73
121
|
function signatureOfDecl(node) {
|
|
74
122
|
const braceIndex = node.text.indexOf("{");
|
|
75
123
|
const semiIndex = node.text.indexOf(";");
|
|
@@ -228,11 +276,11 @@ function buildFunctionChunk(node, imports, language) {
|
|
|
228
276
|
name: qualifiedName,
|
|
229
277
|
kind,
|
|
230
278
|
signature: signatureOfDecl(node),
|
|
231
|
-
body: node
|
|
279
|
+
body: bodyOf(node),
|
|
232
280
|
startLine,
|
|
233
281
|
endLine,
|
|
234
282
|
language,
|
|
235
|
-
exported:
|
|
283
|
+
exported: isCppVisible(node),
|
|
236
284
|
calls: collectCallsInNode(node),
|
|
237
285
|
imports
|
|
238
286
|
};
|
|
@@ -249,11 +297,11 @@ function buildTypeChunk(node, kind, language) {
|
|
|
249
297
|
name: qualifiedName,
|
|
250
298
|
kind,
|
|
251
299
|
signature: signatureOfDecl(node),
|
|
252
|
-
body: node
|
|
300
|
+
body: bodyOf(node),
|
|
253
301
|
startLine,
|
|
254
302
|
endLine,
|
|
255
303
|
language,
|
|
256
|
-
exported:
|
|
304
|
+
exported: isCppVisible(node),
|
|
257
305
|
calls: [],
|
|
258
306
|
imports: []
|
|
259
307
|
};
|
|
@@ -269,11 +317,11 @@ function buildNamespaceChunk(node, language) {
|
|
|
269
317
|
name: fullPath,
|
|
270
318
|
kind: "namespace",
|
|
271
319
|
signature: signatureOfDecl(node),
|
|
272
|
-
body: node
|
|
320
|
+
body: bodyOf(node),
|
|
273
321
|
startLine,
|
|
274
322
|
endLine,
|
|
275
323
|
language,
|
|
276
|
-
exported:
|
|
324
|
+
exported: isCppVisible(node),
|
|
277
325
|
calls: [],
|
|
278
326
|
imports: []
|
|
279
327
|
};
|
|
@@ -281,7 +329,8 @@ function buildNamespaceChunk(node, language) {
|
|
|
281
329
|
|
|
282
330
|
export async function parseCode(code, filePath, language = "cpp") {
|
|
283
331
|
await ensureLanguage();
|
|
284
|
-
const { tree } = parseSource(CPP_LANG, code);
|
|
332
|
+
const { tree, reason } = parseSource(CPP_LANG, code);
|
|
333
|
+
if (!tree) return { chunks: [], errors: [{ message: reason }] };
|
|
285
334
|
const root = tree.rootNode;
|
|
286
335
|
const imports = collectImports(root);
|
|
287
336
|
|
|
@@ -309,7 +358,7 @@ export async function parseCode(code, filePath, language = "cpp") {
|
|
|
309
358
|
return true;
|
|
310
359
|
});
|
|
311
360
|
|
|
312
|
-
return { chunks: deduped, errors:
|
|
361
|
+
return { chunks: deduped, errors: collectErrors(tree) };
|
|
313
362
|
}
|
|
314
363
|
|
|
315
364
|
export async function isAvailable() {
|
|
@@ -206,7 +206,14 @@ sealed class VbChunkCollector
|
|
|
206
206
|
private void AddMethodChunk(List<ChunkOutput> chunks, MethodBlockSyntax node, string parentTypeName)
|
|
207
207
|
{
|
|
208
208
|
var statement = node.BlockStatement;
|
|
209
|
-
var
|
|
209
|
+
var identifierText = statement switch
|
|
210
|
+
{
|
|
211
|
+
MethodStatementSyntax methodStmt => methodStmt.Identifier.Text,
|
|
212
|
+
SubNewStatementSyntax => "New",
|
|
213
|
+
OperatorStatementSyntax opStmt => opStmt.OperatorToken.Text,
|
|
214
|
+
_ => statement.ToString().Split('(')[0].Trim()
|
|
215
|
+
};
|
|
216
|
+
var name = $"{parentTypeName}.{identifierText}";
|
|
210
217
|
var kind = statement.Kind() == SyntaxKind.SubStatement ? "method" : "function";
|
|
211
218
|
chunks.Add(BuildChunk(
|
|
212
219
|
name,
|
|
@@ -300,7 +307,6 @@ sealed class VbChunkCollector
|
|
|
300
307
|
{
|
|
301
308
|
SimpleImportsClauseSyntax simpleClause => simpleClause.Name.ToString(),
|
|
302
309
|
XmlNamespaceImportsClauseSyntax xmlClause => xmlClause.XmlNamespace.ToString(),
|
|
303
|
-
AliasImportsClauseSyntax aliasClause => aliasClause.Name.ToString(),
|
|
304
310
|
_ => clause.ToString()
|
|
305
311
|
};
|
|
306
312
|
}
|
|
@@ -309,6 +315,10 @@ sealed class VbChunkCollector
|
|
|
309
315
|
{
|
|
310
316
|
SyntaxTokenList modifiers = node switch
|
|
311
317
|
{
|
|
318
|
+
TypeBlockSyntax typeBlock => typeBlock.BlockStatement.Modifiers,
|
|
319
|
+
MethodBlockSyntax methodBlock => methodBlock.BlockStatement.Modifiers,
|
|
320
|
+
PropertyBlockSyntax propertyBlock => propertyBlock.PropertyStatement.Modifiers,
|
|
321
|
+
EventBlockSyntax eventBlock => eventBlock.EventStatement.Modifiers,
|
|
312
322
|
TypeStatementSyntax typeStatement => typeStatement.Modifiers,
|
|
313
323
|
MethodStatementSyntax methodStatement => methodStatement.Modifiers,
|
|
314
324
|
PropertyStatementSyntax propertyStatement => propertyStatement.Modifiers,
|
|
@@ -332,6 +342,7 @@ sealed class VbChunkCollector
|
|
|
332
342
|
.Select(invocation => invocation.Expression)
|
|
333
343
|
.Select(GetInvocationName)
|
|
334
344
|
.Where(name => !string.IsNullOrWhiteSpace(name))
|
|
345
|
+
.Select(name => name!)
|
|
335
346
|
.Distinct(StringComparer.Ordinal)
|
|
336
347
|
.ToArray();
|
|
337
348
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
<Project Sdk="Microsoft.NET.Sdk">
|
|
2
2
|
<PropertyGroup>
|
|
3
3
|
<OutputType>Exe</OutputType>
|
|
4
|
-
<TargetFramework>
|
|
4
|
+
<TargetFramework>net10.0</TargetFramework>
|
|
5
5
|
<ImplicitUsings>enable</ImplicitUsings>
|
|
6
6
|
<Nullable>enable</Nullable>
|
|
7
7
|
<LangVersion>latest</LangVersion>
|
|
@@ -26,6 +26,8 @@ import {
|
|
|
26
26
|
initTreeSitter,
|
|
27
27
|
lineRangeOf,
|
|
28
28
|
loadGrammar,
|
|
29
|
+
bodyOf,
|
|
30
|
+
collectErrors,
|
|
29
31
|
parseSource,
|
|
30
32
|
runQuery
|
|
31
33
|
} from "./tree-sitter/base.mjs";
|
|
@@ -172,7 +174,7 @@ function buildFunctionChunk(node, imports, language) {
|
|
|
172
174
|
name,
|
|
173
175
|
kind: "function",
|
|
174
176
|
signature: signatureOfDecl(node),
|
|
175
|
-
body: node
|
|
177
|
+
body: bodyOf(node),
|
|
176
178
|
startLine,
|
|
177
179
|
endLine,
|
|
178
180
|
language,
|
|
@@ -193,7 +195,7 @@ function buildMethodChunk(node, imports, language) {
|
|
|
193
195
|
name: qualifiedName,
|
|
194
196
|
kind: "method",
|
|
195
197
|
signature: signatureOfDecl(node),
|
|
196
|
-
body: node
|
|
198
|
+
body: bodyOf(node),
|
|
197
199
|
startLine,
|
|
198
200
|
endLine,
|
|
199
201
|
language,
|
|
@@ -211,7 +213,7 @@ function buildTypeChunk(node, nameNode, language) {
|
|
|
211
213
|
name,
|
|
212
214
|
kind,
|
|
213
215
|
signature: signatureOfDecl(node),
|
|
214
|
-
body: node
|
|
216
|
+
body: bodyOf(node),
|
|
215
217
|
startLine,
|
|
216
218
|
endLine,
|
|
217
219
|
language,
|
|
@@ -223,7 +225,8 @@ function buildTypeChunk(node, nameNode, language) {
|
|
|
223
225
|
|
|
224
226
|
export async function parseCode(code, filePath, language = "go") {
|
|
225
227
|
await ensureLanguage();
|
|
226
|
-
const { tree } = parseSource(GO_LANG, code);
|
|
228
|
+
const { tree, reason } = parseSource(GO_LANG, code);
|
|
229
|
+
if (!tree) return { chunks: [], errors: [{ message: reason }] };
|
|
227
230
|
const root = tree.rootNode;
|
|
228
231
|
const imports = collectImports(root);
|
|
229
232
|
|
|
@@ -268,7 +271,7 @@ export async function parseCode(code, filePath, language = "go") {
|
|
|
268
271
|
return true;
|
|
269
272
|
});
|
|
270
273
|
|
|
271
|
-
return { chunks: deduped, errors:
|
|
274
|
+
return { chunks: deduped, errors: collectErrors(tree) };
|
|
272
275
|
}
|
|
273
276
|
|
|
274
277
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
@@ -28,6 +28,8 @@ import {
|
|
|
28
28
|
initTreeSitter,
|
|
29
29
|
lineRangeOf,
|
|
30
30
|
loadGrammar,
|
|
31
|
+
bodyOf,
|
|
32
|
+
collectErrors,
|
|
31
33
|
parseSource,
|
|
32
34
|
runQuery
|
|
33
35
|
} from "./tree-sitter/base.mjs";
|
|
@@ -151,7 +153,7 @@ function buildTypeChunk(node, kind, imports, language) {
|
|
|
151
153
|
name: qualifiedName,
|
|
152
154
|
kind,
|
|
153
155
|
signature: signatureOfDecl(node),
|
|
154
|
-
body: node
|
|
156
|
+
body: bodyOf(node),
|
|
155
157
|
startLine,
|
|
156
158
|
endLine,
|
|
157
159
|
language,
|
|
@@ -174,7 +176,7 @@ function buildMethodChunk(node, imports, language) {
|
|
|
174
176
|
name: qualifiedName,
|
|
175
177
|
kind: "method",
|
|
176
178
|
signature: signatureOfDecl(node),
|
|
177
|
-
body: node
|
|
179
|
+
body: bodyOf(node),
|
|
178
180
|
startLine,
|
|
179
181
|
endLine,
|
|
180
182
|
language,
|
|
@@ -195,7 +197,7 @@ function buildConstructorChunk(node, imports, language) {
|
|
|
195
197
|
name: qualifiedName,
|
|
196
198
|
kind: "constructor",
|
|
197
199
|
signature: signatureOfDecl(node),
|
|
198
|
-
body: node
|
|
200
|
+
body: bodyOf(node),
|
|
199
201
|
startLine,
|
|
200
202
|
endLine,
|
|
201
203
|
language,
|
|
@@ -207,7 +209,8 @@ function buildConstructorChunk(node, imports, language) {
|
|
|
207
209
|
|
|
208
210
|
export async function parseCode(code, filePath, language = "java") {
|
|
209
211
|
await ensureLanguage();
|
|
210
|
-
const { tree } = parseSource(JAVA_LANG, code);
|
|
212
|
+
const { tree, reason } = parseSource(JAVA_LANG, code);
|
|
213
|
+
if (!tree) return { chunks: [], errors: [{ message: reason }] };
|
|
211
214
|
const root = tree.rootNode;
|
|
212
215
|
const imports = collectImports(root);
|
|
213
216
|
|
|
@@ -235,7 +238,7 @@ export async function parseCode(code, filePath, language = "java") {
|
|
|
235
238
|
return true;
|
|
236
239
|
});
|
|
237
240
|
|
|
238
|
-
return { chunks: deduped, errors:
|
|
241
|
+
return { chunks: deduped, errors: collectErrors(tree) };
|
|
239
242
|
}
|
|
240
243
|
|
|
241
244
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
@@ -26,6 +26,8 @@ import {
|
|
|
26
26
|
initTreeSitter,
|
|
27
27
|
lineRangeOf,
|
|
28
28
|
loadGrammar,
|
|
29
|
+
bodyOf,
|
|
30
|
+
collectErrors,
|
|
29
31
|
parseSource,
|
|
30
32
|
runQuery
|
|
31
33
|
} from "./tree-sitter/base.mjs";
|
|
@@ -197,7 +199,7 @@ function buildFunctionChunk(node, imports, language) {
|
|
|
197
199
|
name: qualifiedName,
|
|
198
200
|
kind: isMethod ? "method" : "function",
|
|
199
201
|
signature: signatureOfDef(node),
|
|
200
|
-
body: node
|
|
202
|
+
body: bodyOf(node),
|
|
201
203
|
startLine,
|
|
202
204
|
endLine,
|
|
203
205
|
language,
|
|
@@ -220,7 +222,7 @@ function buildClassChunk(node, language) {
|
|
|
220
222
|
name: qualifiedName,
|
|
221
223
|
kind: "class",
|
|
222
224
|
signature: signatureOfDef(node),
|
|
223
|
-
body: node
|
|
225
|
+
body: bodyOf(node),
|
|
224
226
|
startLine,
|
|
225
227
|
endLine,
|
|
226
228
|
language,
|
|
@@ -232,7 +234,8 @@ function buildClassChunk(node, language) {
|
|
|
232
234
|
|
|
233
235
|
export async function parseCode(code, filePath, language = "python") {
|
|
234
236
|
await ensureLanguage();
|
|
235
|
-
const { tree } = parseSource(PY_LANG, code);
|
|
237
|
+
const { tree, reason } = parseSource(PY_LANG, code);
|
|
238
|
+
if (!tree) return { chunks: [], errors: [{ message: reason }] };
|
|
236
239
|
const root = tree.rootNode;
|
|
237
240
|
const imports = collectImports(root);
|
|
238
241
|
|
|
@@ -256,7 +259,7 @@ export async function parseCode(code, filePath, language = "python") {
|
|
|
256
259
|
return true;
|
|
257
260
|
});
|
|
258
261
|
|
|
259
|
-
return { chunks: deduped, errors:
|
|
262
|
+
return { chunks: deduped, errors: collectErrors(tree) };
|
|
260
263
|
}
|
|
261
264
|
|
|
262
265
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
@@ -33,6 +33,8 @@ import {
|
|
|
33
33
|
initTreeSitter,
|
|
34
34
|
lineRangeOf,
|
|
35
35
|
loadGrammar,
|
|
36
|
+
bodyOf,
|
|
37
|
+
collectErrors,
|
|
36
38
|
parseSource,
|
|
37
39
|
runQuery
|
|
38
40
|
} from "./tree-sitter/base.mjs";
|
|
@@ -195,7 +197,7 @@ function buildTypeChunk(node, kind, language) {
|
|
|
195
197
|
name: qualifiedName,
|
|
196
198
|
kind,
|
|
197
199
|
signature: signatureOfDecl(node),
|
|
198
|
-
body: node
|
|
200
|
+
body: bodyOf(node),
|
|
199
201
|
startLine,
|
|
200
202
|
endLine,
|
|
201
203
|
language,
|
|
@@ -218,7 +220,7 @@ function buildMethodChunk(node, imports, language, isSingleton) {
|
|
|
218
220
|
name: qualifiedName,
|
|
219
221
|
kind: isSingleton ? "class_method" : "method",
|
|
220
222
|
signature: signatureOfDecl(node),
|
|
221
|
-
body: node
|
|
223
|
+
body: bodyOf(node),
|
|
222
224
|
startLine,
|
|
223
225
|
endLine,
|
|
224
226
|
language,
|
|
@@ -230,7 +232,8 @@ function buildMethodChunk(node, imports, language, isSingleton) {
|
|
|
230
232
|
|
|
231
233
|
export async function parseCode(code, filePath, language = "ruby") {
|
|
232
234
|
await ensureLanguage();
|
|
233
|
-
const { tree } = parseSource(RUBY_LANG, code);
|
|
235
|
+
const { tree, reason } = parseSource(RUBY_LANG, code);
|
|
236
|
+
if (!tree) return { chunks: [], errors: [{ message: reason }] };
|
|
234
237
|
const root = tree.rootNode;
|
|
235
238
|
const imports = collectImports(root);
|
|
236
239
|
|
|
@@ -256,7 +259,7 @@ export async function parseCode(code, filePath, language = "ruby") {
|
|
|
256
259
|
return true;
|
|
257
260
|
});
|
|
258
261
|
|
|
259
|
-
return { chunks: deduped, errors:
|
|
262
|
+
return { chunks: deduped, errors: collectErrors(tree) };
|
|
260
263
|
}
|
|
261
264
|
|
|
262
265
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
@@ -19,6 +19,8 @@ import {
|
|
|
19
19
|
initTreeSitter,
|
|
20
20
|
lineRangeOf,
|
|
21
21
|
loadGrammar,
|
|
22
|
+
bodyOf,
|
|
23
|
+
collectErrors,
|
|
22
24
|
parseSource,
|
|
23
25
|
runQuery
|
|
24
26
|
} from "./tree-sitter/base.mjs";
|
|
@@ -159,16 +161,24 @@ function groupDeclarations(rootNode) {
|
|
|
159
161
|
return entries;
|
|
160
162
|
}
|
|
161
163
|
|
|
164
|
+
function isRustPublic(node) {
|
|
165
|
+
for (let i = 0; i < node.namedChildCount; i += 1) {
|
|
166
|
+
if (node.namedChild(i).type === "visibility_modifier") return true;
|
|
167
|
+
}
|
|
168
|
+
return false;
|
|
169
|
+
}
|
|
170
|
+
|
|
162
171
|
function chunkFrom(kind, node, name, signatureOverride, calls, imports, language) {
|
|
163
172
|
const { startLine, endLine } = lineRangeOf(node);
|
|
164
173
|
return {
|
|
165
174
|
name,
|
|
166
175
|
kind,
|
|
167
176
|
signature: signatureOverride ?? buildSignature(node.text),
|
|
168
|
-
body: node
|
|
177
|
+
body: bodyOf(node),
|
|
169
178
|
startLine,
|
|
170
179
|
endLine,
|
|
171
180
|
language,
|
|
181
|
+
exported: isRustPublic(node),
|
|
172
182
|
calls,
|
|
173
183
|
imports
|
|
174
184
|
};
|
|
@@ -191,7 +201,8 @@ function extractFunctionCalls(functionNode) {
|
|
|
191
201
|
|
|
192
202
|
export async function parseCode(code, filePath, language = "rust") {
|
|
193
203
|
await ensureLanguage();
|
|
194
|
-
const { tree } = parseSource(RUST_LANG, code);
|
|
204
|
+
const { tree, reason } = parseSource(RUST_LANG, code);
|
|
205
|
+
if (!tree) return { chunks: [], errors: [{ message: reason }] };
|
|
195
206
|
const root = tree.rootNode;
|
|
196
207
|
|
|
197
208
|
const imports = collectImports(root);
|
|
@@ -257,10 +268,11 @@ export async function parseCode(code, filePath, language = "rust") {
|
|
|
257
268
|
name: qualifiedName,
|
|
258
269
|
kind: "method",
|
|
259
270
|
signature: buildSignature(child.text),
|
|
260
|
-
body: child
|
|
271
|
+
body: bodyOf(child),
|
|
261
272
|
startLine,
|
|
262
273
|
endLine,
|
|
263
274
|
language,
|
|
275
|
+
exported: isRustPublic(child),
|
|
264
276
|
calls: extractFunctionCalls(child),
|
|
265
277
|
imports
|
|
266
278
|
});
|
|
@@ -276,7 +288,7 @@ export async function parseCode(code, filePath, language = "rust") {
|
|
|
276
288
|
return true;
|
|
277
289
|
});
|
|
278
290
|
|
|
279
|
-
return { chunks: deduped, errors:
|
|
291
|
+
return { chunks: deduped, errors: collectErrors(tree) };
|
|
280
292
|
}
|
|
281
293
|
|
|
282
294
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
@@ -77,10 +77,42 @@ export function createParser(language) {
|
|
|
77
77
|
return parser;
|
|
78
78
|
}
|
|
79
79
|
|
|
80
|
+
/**
|
|
81
|
+
* Hard size limit on input passed to tree-sitter. Swift was dropped
|
|
82
|
+
* because its grammar OOM'd on large files (see aa52c93); even
|
|
83
|
+
* supported grammars can exhaust WASM memory on adversarial input.
|
|
84
|
+
* Callers receive { tree: null, reason } when the limit is hit.
|
|
85
|
+
* Override via CORTEX_TREE_SITTER_MAX_BYTES.
|
|
86
|
+
*/
|
|
87
|
+
const DEFAULT_MAX_SOURCE_BYTES = 4 * 1024 * 1024; // 4 MiB
|
|
88
|
+
|
|
89
|
+
function getMaxSourceBytes() {
|
|
90
|
+
const override = process.env.CORTEX_TREE_SITTER_MAX_BYTES;
|
|
91
|
+
if (!override) return DEFAULT_MAX_SOURCE_BYTES;
|
|
92
|
+
const n = Number.parseInt(override, 10);
|
|
93
|
+
return Number.isFinite(n) && n > 0 ? n : DEFAULT_MAX_SOURCE_BYTES;
|
|
94
|
+
}
|
|
95
|
+
|
|
80
96
|
export function parseSource(language, code) {
|
|
97
|
+
const max = getMaxSourceBytes();
|
|
98
|
+
if (typeof code === "string" && code.length > max) {
|
|
99
|
+
return {
|
|
100
|
+
tree: null,
|
|
101
|
+
parser: null,
|
|
102
|
+
reason: `source exceeds CORTEX_TREE_SITTER_MAX_BYTES (${code.length} > ${max})`
|
|
103
|
+
};
|
|
104
|
+
}
|
|
81
105
|
const parser = createParser(language);
|
|
82
|
-
|
|
83
|
-
|
|
106
|
+
try {
|
|
107
|
+
const tree = parser.parse(code);
|
|
108
|
+
return { tree, parser };
|
|
109
|
+
} catch (error) {
|
|
110
|
+
return {
|
|
111
|
+
tree: null,
|
|
112
|
+
parser,
|
|
113
|
+
reason: `tree-sitter parse threw: ${error instanceof Error ? error.message : String(error)}`
|
|
114
|
+
};
|
|
115
|
+
}
|
|
84
116
|
}
|
|
85
117
|
|
|
86
118
|
export function runQuery(language, queryString, node) {
|
|
@@ -142,6 +174,46 @@ export function dedupe(items) {
|
|
|
142
174
|
return [...new Set(items.filter((item) => item != null && item !== ""))];
|
|
143
175
|
}
|
|
144
176
|
|
|
177
|
+
/**
|
|
178
|
+
* Walk the tree collecting syntax errors. Tree-sitter flags MISSING
|
|
179
|
+
* and ERROR nodes during parsing; a clean parse has none. Returns
|
|
180
|
+
* `{message, line, column}` entries compatible with Cortex's existing
|
|
181
|
+
* parser error shape. Limits output to `maxErrors` to keep DB rows
|
|
182
|
+
* small on pathological input. Descends into ERROR subtrees so nested
|
|
183
|
+
* errors are also reported (capped by maxErrors).
|
|
184
|
+
*/
|
|
185
|
+
export function collectErrors(tree, { maxErrors = 32 } = {}) {
|
|
186
|
+
const errors = [];
|
|
187
|
+
if (!tree?.rootNode?.hasError) return errors;
|
|
188
|
+
|
|
189
|
+
const visit = (node) => {
|
|
190
|
+
if (errors.length >= maxErrors) return;
|
|
191
|
+
if (node.isError || node.type === "ERROR") {
|
|
192
|
+
errors.push({
|
|
193
|
+
message: "Syntax error",
|
|
194
|
+
line: node.startPosition.row + 1,
|
|
195
|
+
column: node.startPosition.column + 1
|
|
196
|
+
});
|
|
197
|
+
// fall through — ERROR nodes can contain nested errors we still want to report
|
|
198
|
+
} else if (node.isMissing) {
|
|
199
|
+
errors.push({
|
|
200
|
+
message: `Missing ${node.type || "token"}`,
|
|
201
|
+
line: node.startPosition.row + 1,
|
|
202
|
+
column: node.startPosition.column + 1
|
|
203
|
+
});
|
|
204
|
+
return;
|
|
205
|
+
} else if (!node.hasError) {
|
|
206
|
+
return;
|
|
207
|
+
}
|
|
208
|
+
for (let i = 0; i < node.childCount; i++) {
|
|
209
|
+
visit(node.child(i));
|
|
210
|
+
}
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
visit(tree.rootNode);
|
|
214
|
+
return errors;
|
|
215
|
+
}
|
|
216
|
+
|
|
145
217
|
/**
|
|
146
218
|
* Convenience loader for language modules — initializes tree-sitter and
|
|
147
219
|
* pre-loads a grammar. Returns an object with the grammar handle and
|
|
@@ -2,11 +2,16 @@
|
|
|
2
2
|
/**
|
|
3
3
|
* Conditional VB.NET parser bridge for Cortex.
|
|
4
4
|
*
|
|
5
|
-
* Uses a Roslyn sidecar via
|
|
6
|
-
*
|
|
7
|
-
*
|
|
5
|
+
* Uses a Roslyn sidecar via a pre-published DLL when a .NET SDK is available.
|
|
6
|
+
* On first use the sidecar is published to bin/Release/<tfm>/publish/ and the
|
|
7
|
+
* DLL path is cached; subsequent invocations skip the msbuild cycle and run
|
|
8
|
+
* `dotnet <dll>` directly — roughly 10× faster per call than `dotnet run`.
|
|
9
|
+
*
|
|
10
|
+
* If no runtime/SDK exists, callers should skip structured chunk extraction
|
|
11
|
+
* and fall back to plain file-level indexing.
|
|
8
12
|
*/
|
|
9
13
|
|
|
14
|
+
import fs from "node:fs";
|
|
10
15
|
import path from "node:path";
|
|
11
16
|
import { fileURLToPath } from "node:url";
|
|
12
17
|
import { spawnSync } from "node:child_process";
|
|
@@ -15,8 +20,10 @@ const __filename = fileURLToPath(import.meta.url);
|
|
|
15
20
|
const __dirname = path.dirname(__filename);
|
|
16
21
|
const DEFAULT_DOTNET_COMMAND = "dotnet";
|
|
17
22
|
const DEFAULT_PROJECT_PATH = path.join(__dirname, "dotnet", "VbNetParser", "VbNetParser.csproj");
|
|
23
|
+
const DEFAULT_TARGET_FRAMEWORK = "net10.0";
|
|
18
24
|
|
|
19
25
|
let runtimeCache = null;
|
|
26
|
+
let publishCache = null;
|
|
20
27
|
|
|
21
28
|
function getDotnetCommand() {
|
|
22
29
|
const override = process.env.CORTEX_DOTNET_CMD;
|
|
@@ -28,8 +35,51 @@ function getProjectPath() {
|
|
|
28
35
|
return override && override.trim().length > 0 ? override.trim() : DEFAULT_PROJECT_PATH;
|
|
29
36
|
}
|
|
30
37
|
|
|
38
|
+
function getTargetFramework() {
|
|
39
|
+
const override = process.env.CORTEX_VBNET_PARSER_TFM;
|
|
40
|
+
return override && override.trim().length > 0 ? override.trim() : DEFAULT_TARGET_FRAMEWORK;
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function getPublishDir() {
|
|
44
|
+
const override = process.env.CORTEX_VBNET_PUBLISH_DIR;
|
|
45
|
+
if (override && override.trim().length > 0) return override.trim();
|
|
46
|
+
const projectDir = path.dirname(getProjectPath());
|
|
47
|
+
return path.join(projectDir, "bin", "Release", getTargetFramework(), "publish");
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
function getDllPath() {
|
|
51
|
+
return path.join(getPublishDir(), "VbNetParser.dll");
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
function getMaxSourceMtime() {
|
|
55
|
+
const projectDir = path.dirname(getProjectPath());
|
|
56
|
+
const sources = [getProjectPath(), path.join(projectDir, "Program.cs")];
|
|
57
|
+
let max = 0;
|
|
58
|
+
for (const src of sources) {
|
|
59
|
+
try {
|
|
60
|
+
const mtime = fs.statSync(src).mtimeMs;
|
|
61
|
+
if (mtime > max) max = mtime;
|
|
62
|
+
} catch {
|
|
63
|
+
// missing source — treated as stale below
|
|
64
|
+
}
|
|
65
|
+
}
|
|
66
|
+
return max;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
function needsPublish() {
|
|
70
|
+
const dll = getDllPath();
|
|
71
|
+
let dllMtime;
|
|
72
|
+
try {
|
|
73
|
+
dllMtime = fs.statSync(dll).mtimeMs;
|
|
74
|
+
} catch {
|
|
75
|
+
return true;
|
|
76
|
+
}
|
|
77
|
+
return getMaxSourceMtime() > dllMtime;
|
|
78
|
+
}
|
|
79
|
+
|
|
31
80
|
export function resetVbNetParserRuntimeCache() {
|
|
32
81
|
runtimeCache = null;
|
|
82
|
+
publishCache = null;
|
|
33
83
|
}
|
|
34
84
|
|
|
35
85
|
export function getVbNetParserRuntime() {
|
|
@@ -69,19 +119,69 @@ export function isVbNetParserAvailable() {
|
|
|
69
119
|
return getVbNetParserRuntime().available;
|
|
70
120
|
}
|
|
71
121
|
|
|
122
|
+
export function ensureVbNetParserPublished() {
|
|
123
|
+
if (publishCache) return publishCache;
|
|
124
|
+
|
|
125
|
+
const runtime = getVbNetParserRuntime();
|
|
126
|
+
if (!runtime.available) {
|
|
127
|
+
publishCache = { ok: false, reason: runtime.reason };
|
|
128
|
+
return publishCache;
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
const dllPath = getDllPath();
|
|
132
|
+
if (!needsPublish()) {
|
|
133
|
+
publishCache = { ok: true, dllPath };
|
|
134
|
+
return publishCache;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
if (!process.env.CORTEX_QUIET) {
|
|
138
|
+
process.stderr.write("[cortex] Publishing Roslyn VB.NET parser (one-time, ~15s)...\n");
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const result = spawnSync(
|
|
142
|
+
runtime.command,
|
|
143
|
+
[
|
|
144
|
+
"publish",
|
|
145
|
+
runtime.projectPath,
|
|
146
|
+
"-c", "Release",
|
|
147
|
+
"-o", getPublishDir(),
|
|
148
|
+
"--nologo",
|
|
149
|
+
"-v", "quiet"
|
|
150
|
+
],
|
|
151
|
+
{ encoding: "utf8", timeout: 180000 }
|
|
152
|
+
);
|
|
153
|
+
|
|
154
|
+
if (result.error || result.status !== 0) {
|
|
155
|
+
publishCache = {
|
|
156
|
+
ok: false,
|
|
157
|
+
reason:
|
|
158
|
+
result.error?.message ||
|
|
159
|
+
result.stderr?.trim() ||
|
|
160
|
+
`dotnet publish failed with exit code ${result.status ?? "unknown"}`
|
|
161
|
+
};
|
|
162
|
+
return publishCache;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
publishCache = { ok: true, dllPath };
|
|
166
|
+
return publishCache;
|
|
167
|
+
}
|
|
168
|
+
|
|
72
169
|
export function parseCode(code, filePath, language = "vbnet") {
|
|
73
170
|
const runtime = getVbNetParserRuntime();
|
|
74
171
|
if (!runtime.available) {
|
|
75
172
|
return { chunks: [], errors: [] };
|
|
76
173
|
}
|
|
77
174
|
|
|
175
|
+
const published = ensureVbNetParserPublished();
|
|
176
|
+
if (!published.ok) {
|
|
177
|
+
return {
|
|
178
|
+
chunks: [],
|
|
179
|
+
errors: [{ message: `VB.NET parser publish failed: ${published.reason}` }]
|
|
180
|
+
};
|
|
181
|
+
}
|
|
182
|
+
|
|
78
183
|
const args = [
|
|
79
|
-
|
|
80
|
-
"--project",
|
|
81
|
-
runtime.projectPath,
|
|
82
|
-
"--configuration",
|
|
83
|
-
"Release",
|
|
84
|
-
"--",
|
|
184
|
+
published.dllPath,
|
|
85
185
|
"--stdin",
|
|
86
186
|
"--file",
|
|
87
187
|
filePath,
|
|
@@ -129,7 +229,6 @@ export function parseCode(code, filePath, language = "vbnet") {
|
|
|
129
229
|
}
|
|
130
230
|
|
|
131
231
|
if (import.meta.url === `file://${process.argv[1]}`) {
|
|
132
|
-
const fs = await import("node:fs");
|
|
133
232
|
const filePath = process.argv[2];
|
|
134
233
|
|
|
135
234
|
if (!filePath) {
|