@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 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(process.argv[1]).href;
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.0",
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.text,
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.text,
279
+ body: bodyOf(node),
232
280
  startLine,
233
281
  endLine,
234
282
  language,
235
- exported: true,
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.text,
300
+ body: bodyOf(node),
253
301
  startLine,
254
302
  endLine,
255
303
  language,
256
- exported: true,
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.text,
320
+ body: bodyOf(node),
273
321
  startLine,
274
322
  endLine,
275
323
  language,
276
- exported: true,
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 name = $"{parentTypeName}.{statement.Identifier.Text}";
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>net8.0</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.text,
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.text,
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.text,
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.text,
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.text,
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.text,
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.text,
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.text,
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.text,
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.text,
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.text,
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.text,
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
- const tree = parser.parse(code);
83
- return { tree, parser };
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 `dotnet run` when a .NET runtime is available.
6
- * If no runtime exists, callers should skip structured chunk extraction and
7
- * fall back to plain file-level indexing.
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
- "run",
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) {