purecontext-mcp 1.2.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.
Files changed (177) hide show
  1. package/AGENT_INSTRUCTIONS.md +110 -784
  2. package/AGENT_REFERENCE.md +561 -0
  3. package/CHANGELOG.md +177 -6
  4. package/FRAMEWORK-ADAPTERS.md +351 -0
  5. package/LANGUAGE-SUPPORT.md +144 -0
  6. package/README.md +92 -12
  7. package/USER-GUIDE.md +8 -0
  8. package/dist/cli/hooks.d.ts +28 -0
  9. package/dist/cli/hooks.d.ts.map +1 -0
  10. package/dist/cli/hooks.js +570 -0
  11. package/dist/cli/hooks.js.map +1 -0
  12. package/dist/cli/install-detect.d.ts +16 -0
  13. package/dist/cli/install-detect.d.ts.map +1 -0
  14. package/dist/cli/install-detect.js +70 -0
  15. package/dist/cli/install-detect.js.map +1 -0
  16. package/dist/cli/install-writers.d.ts +59 -0
  17. package/dist/cli/install-writers.d.ts.map +1 -0
  18. package/dist/cli/install-writers.js +292 -0
  19. package/dist/cli/install-writers.js.map +1 -0
  20. package/dist/cli/install.d.ts +14 -0
  21. package/dist/cli/install.d.ts.map +1 -0
  22. package/dist/cli/install.js +150 -0
  23. package/dist/cli/install.js.map +1 -0
  24. package/dist/config/config-loader.js +3 -0
  25. package/dist/config/config-loader.js.map +1 -1
  26. package/dist/config/config-schema.d.ts +11 -0
  27. package/dist/config/config-schema.d.ts.map +1 -1
  28. package/dist/config/config-schema.js +15 -0
  29. package/dist/config/config-schema.js.map +1 -1
  30. package/dist/core/db/symbol-store.d.ts +1 -0
  31. package/dist/core/db/symbol-store.d.ts.map +1 -1
  32. package/dist/core/db/symbol-store.js +120 -6
  33. package/dist/core/db/symbol-store.js.map +1 -1
  34. package/dist/core/file-discovery.d.ts +6 -0
  35. package/dist/core/file-discovery.d.ts.map +1 -1
  36. package/dist/core/file-discovery.js +20 -13
  37. package/dist/core/file-discovery.js.map +1 -1
  38. package/dist/core/file-processor.d.ts.map +1 -1
  39. package/dist/core/file-processor.js +26 -1
  40. package/dist/core/file-processor.js.map +1 -1
  41. package/dist/core/git-log-reader.d.ts.map +1 -1
  42. package/dist/core/git-log-reader.js +21 -0
  43. package/dist/core/git-log-reader.js.map +1 -1
  44. package/dist/core/index-manager.d.ts.map +1 -1
  45. package/dist/core/index-manager.js +21 -7
  46. package/dist/core/index-manager.js.map +1 -1
  47. package/dist/core/indexing-worker.d.ts.map +1 -1
  48. package/dist/core/indexing-worker.js +14 -0
  49. package/dist/core/indexing-worker.js.map +1 -1
  50. package/dist/core/parse-dispatcher.d.ts.map +1 -1
  51. package/dist/core/parse-dispatcher.js +20 -5
  52. package/dist/core/parse-dispatcher.js.map +1 -1
  53. package/dist/core/search/query-preprocessor.d.ts +69 -3
  54. package/dist/core/search/query-preprocessor.d.ts.map +1 -1
  55. package/dist/core/search/query-preprocessor.js +450 -17
  56. package/dist/core/search/query-preprocessor.js.map +1 -1
  57. package/dist/core/search/relevance-ranker.d.ts +60 -5
  58. package/dist/core/search/relevance-ranker.d.ts.map +1 -1
  59. package/dist/core/search/relevance-ranker.js +931 -33
  60. package/dist/core/search/relevance-ranker.js.map +1 -1
  61. package/dist/core/test-mapper.d.ts.map +1 -1
  62. package/dist/core/test-mapper.js +7 -1
  63. package/dist/core/test-mapper.js.map +1 -1
  64. package/dist/core/types.d.ts +28 -1
  65. package/dist/core/types.d.ts.map +1 -1
  66. package/dist/handlers/angular-html.d.ts +3 -0
  67. package/dist/handlers/angular-html.d.ts.map +1 -0
  68. package/dist/handlers/angular-html.js +215 -0
  69. package/dist/handlers/angular-html.js.map +1 -0
  70. package/dist/handlers/c.d.ts.map +1 -1
  71. package/dist/handlers/c.js +19 -0
  72. package/dist/handlers/c.js.map +1 -1
  73. package/dist/handlers/cpp-macro-registry.d.ts +21 -0
  74. package/dist/handlers/cpp-macro-registry.d.ts.map +1 -0
  75. package/dist/handlers/cpp-macro-registry.js +44 -0
  76. package/dist/handlers/cpp-macro-registry.js.map +1 -0
  77. package/dist/handlers/cpp.d.ts.map +1 -1
  78. package/dist/handlers/cpp.js +579 -10
  79. package/dist/handlers/cpp.js.map +1 -1
  80. package/dist/handlers/csharp.d.ts.map +1 -1
  81. package/dist/handlers/csharp.js +39 -2
  82. package/dist/handlers/csharp.js.map +1 -1
  83. package/dist/handlers/css.d.ts +3 -0
  84. package/dist/handlers/css.d.ts.map +1 -0
  85. package/dist/handlers/css.js +154 -0
  86. package/dist/handlers/css.js.map +1 -0
  87. package/dist/handlers/erlang.d.ts.map +1 -1
  88. package/dist/handlers/erlang.js +8 -1
  89. package/dist/handlers/erlang.js.map +1 -1
  90. package/dist/handlers/fortran.js +1 -1
  91. package/dist/handlers/fortran.js.map +1 -1
  92. package/dist/handlers/go.d.ts.map +1 -1
  93. package/dist/handlers/go.js +87 -2
  94. package/dist/handlers/go.js.map +1 -1
  95. package/dist/handlers/handler-registry.d.ts.map +1 -1
  96. package/dist/handlers/handler-registry.js +4 -0
  97. package/dist/handlers/handler-registry.js.map +1 -1
  98. package/dist/handlers/hcl.d.ts +3 -0
  99. package/dist/handlers/hcl.d.ts.map +1 -0
  100. package/dist/handlers/hcl.js +193 -0
  101. package/dist/handlers/hcl.js.map +1 -0
  102. package/dist/handlers/java.d.ts.map +1 -1
  103. package/dist/handlers/java.js +33 -16
  104. package/dist/handlers/java.js.map +1 -1
  105. package/dist/handlers/kotlin.d.ts.map +1 -1
  106. package/dist/handlers/kotlin.js +48 -3
  107. package/dist/handlers/kotlin.js.map +1 -1
  108. package/dist/handlers/less.d.ts +3 -0
  109. package/dist/handlers/less.d.ts.map +1 -0
  110. package/dist/handlers/less.js +255 -0
  111. package/dist/handlers/less.js.map +1 -0
  112. package/dist/handlers/objective-c.d.ts.map +1 -1
  113. package/dist/handlers/objective-c.js +122 -64
  114. package/dist/handlers/objective-c.js.map +1 -1
  115. package/dist/handlers/openapi.d.ts.map +1 -1
  116. package/dist/handlers/openapi.js +30 -5
  117. package/dist/handlers/openapi.js.map +1 -1
  118. package/dist/handlers/php.d.ts.map +1 -1
  119. package/dist/handlers/php.js +287 -41
  120. package/dist/handlers/php.js.map +1 -1
  121. package/dist/handlers/protobuf.d.ts.map +1 -1
  122. package/dist/handlers/protobuf.js +1 -0
  123. package/dist/handlers/protobuf.js.map +1 -1
  124. package/dist/handlers/python.d.ts.map +1 -1
  125. package/dist/handlers/python.js +1 -3
  126. package/dist/handlers/python.js.map +1 -1
  127. package/dist/handlers/ruby-dsl.d.ts +23 -0
  128. package/dist/handlers/ruby-dsl.d.ts.map +1 -0
  129. package/dist/handlers/ruby-dsl.js +251 -0
  130. package/dist/handlers/ruby-dsl.js.map +1 -0
  131. package/dist/handlers/ruby.d.ts.map +1 -1
  132. package/dist/handlers/ruby.js +29 -4
  133. package/dist/handlers/ruby.js.map +1 -1
  134. package/dist/handlers/rust.d.ts.map +1 -1
  135. package/dist/handlers/rust.js +98 -2
  136. package/dist/handlers/rust.js.map +1 -1
  137. package/dist/handlers/scss.d.ts +3 -0
  138. package/dist/handlers/scss.d.ts.map +1 -0
  139. package/dist/handlers/scss.js +290 -0
  140. package/dist/handlers/scss.js.map +1 -0
  141. package/dist/handlers/sql.d.ts.map +1 -1
  142. package/dist/handlers/sql.js +37 -18
  143. package/dist/handlers/sql.js.map +1 -1
  144. package/dist/handlers/typescript.d.ts.map +1 -1
  145. package/dist/handlers/typescript.js +65 -17
  146. package/dist/handlers/typescript.js.map +1 -1
  147. package/dist/handlers/xml.d.ts.map +1 -1
  148. package/dist/handlers/xml.js +35 -2
  149. package/dist/handlers/xml.js.map +1 -1
  150. package/dist/index.d.ts.map +1 -1
  151. package/dist/index.js +91 -0
  152. package/dist/index.js.map +1 -1
  153. package/dist/server/mcp-server.d.ts.map +1 -1
  154. package/dist/server/mcp-server.js +10 -0
  155. package/dist/server/mcp-server.js.map +1 -1
  156. package/dist/server/tools/detect-antipatterns.d.ts +1 -1
  157. package/dist/server/tools/get-architecture-snapshot.d.ts +1 -1
  158. package/dist/server/tools/get-entry-points.d.ts +1 -1
  159. package/dist/server/tools/get-lexical-scope-matches.d.ts +54 -0
  160. package/dist/server/tools/get-lexical-scope-matches.d.ts.map +1 -0
  161. package/dist/server/tools/get-lexical-scope-matches.js +470 -0
  162. package/dist/server/tools/get-lexical-scope-matches.js.map +1 -0
  163. package/dist/server/tools/search-symbols.d.ts +10 -0
  164. package/dist/server/tools/search-symbols.d.ts.map +1 -1
  165. package/dist/server/tools/search-symbols.js +353 -8
  166. package/dist/server/tools/search-symbols.js.map +1 -1
  167. package/dist/server/tools/trace-invocation-chain.d.ts +53 -0
  168. package/dist/server/tools/trace-invocation-chain.d.ts.map +1 -0
  169. package/dist/server/tools/trace-invocation-chain.js +280 -0
  170. package/dist/server/tools/trace-invocation-chain.js.map +1 -0
  171. package/dist/version.d.ts +1 -1
  172. package/dist/version.js +1 -1
  173. package/docs/02-installation.md +89 -17
  174. package/docs/05-cli-reference.md +89 -0
  175. package/docs/dev/benchmark-findings-eu-za-tebe.md +210 -0
  176. package/docs/dev/phase-35-coverage-audit.md +469 -0
  177. package/package.json +4 -1
@@ -1,6 +1,7 @@
1
1
  import { createHash } from 'crypto';
2
2
  import { dirname, resolve } from 'path';
3
3
  import { fileURLToPath } from 'url';
4
+ import { findMacroEntry } from './cpp-macro-registry.js';
4
5
  const __dirname = dirname(fileURLToPath(import.meta.url));
5
6
  const GRAMMARS_DIR = resolve(__dirname, '../../grammars');
6
7
  // ─── Symbol ID ────────────────────────────────────────────────────────────────
@@ -18,7 +19,53 @@ function trunc(s, max = 120) {
18
19
  s = s.replace(/\s+/g, ' ').trim();
19
20
  return s.length > max ? s.slice(0, max - 3) + '...' : s;
20
21
  }
22
+ // ─── Body snippet ─────────────────────────────────────────────────────────────
23
+ /**
24
+ * Extract a short text snippet from a class/struct body or function body.
25
+ * Normalises whitespace and caps at 200 characters.
26
+ */
27
+ function extractBodySnippet(bodyNode, src) {
28
+ // Skip the opening '{' (+1 char); grab up to 600 raw chars
29
+ const start = bodyNode.startIndex + 1;
30
+ const end = Math.min(start + 600, bodyNode.endIndex - 1);
31
+ if (start >= end)
32
+ return '';
33
+ return src
34
+ .slice(start, end)
35
+ .replace(/\s+/g, ' ')
36
+ .trim()
37
+ .slice(0, 200);
38
+ }
21
39
  // ─── Declarator name extraction ───────────────────────────────────────────────
40
+ /**
41
+ * Strip template arguments from a qualified identifier scope part.
42
+ * Handles: `Scene<Float, Spectrum>::render` → `Scene::render`
43
+ * `Outer::Inner<T>::method` → `Outer::Inner::method`
44
+ * `Foo::bar` → `Foo::bar` (unchanged)
45
+ */
46
+ function stripTemplateArgsFromQualified(node, src) {
47
+ // template_type like `Scene<Float, Spectrum>` → return just the base name
48
+ if (node.type === 'template_type') {
49
+ const nameNode = node.children.find((c) => c.isNamed && (c.type === 'type_identifier' || c.type === 'namespace_identifier'));
50
+ return nameNode ? nodeText(nameNode, src) : nodeText(node, src);
51
+ }
52
+ // qualified_identifier: recurse to strip template args at each scope level
53
+ if (node.type === 'qualified_identifier') {
54
+ // Find the '::' separator (first unnamed '::' child)
55
+ const dcolonIdx = node.children.findIndex((c) => !c.isNamed && nodeText(c, src) === '::');
56
+ if (dcolonIdx < 0)
57
+ return nodeText(node, src);
58
+ // Scope is the last named child before '::'
59
+ const scopeChild = node.children.slice(0, dcolonIdx).findLast((c) => c.isNamed);
60
+ // Name is the first named child after '::'
61
+ const nameChild = node.children.slice(dcolonIdx + 1).find((c) => c.isNamed);
62
+ const scope = scopeChild ? stripTemplateArgsFromQualified(scopeChild, src) : '';
63
+ // Also recurse on the name part to handle template_type / qualified_identifier names
64
+ const name = nameChild ? stripTemplateArgsFromQualified(nameChild, src) : '';
65
+ return scope ? `${scope}::${name}` : name;
66
+ }
67
+ return nodeText(node, src);
68
+ }
22
69
  /**
23
70
  * Recursively unwrap C++ declarator nodes to find the innermost name.
24
71
  * Handles qualified identifiers, destructor names, operator names,
@@ -30,8 +77,9 @@ function extractDeclaratorName(declarator, src) {
30
77
  case 'field_identifier':
31
78
  return nodeText(declarator, src);
32
79
  case 'qualified_identifier':
33
- // Out-of-class definition like `Foo::bar` or `Foo::~Foo` — return full text
34
- return nodeText(declarator, src);
80
+ // Out-of-class definition like `Foo::bar`, `Foo::~Foo`, or
81
+ // `Scene<Float, Spectrum>::render` — strip template args from scope parts.
82
+ return stripTemplateArgsFromQualified(declarator, src);
35
83
  case 'destructor_name':
36
84
  // `~Foo` — return as-is
37
85
  return nodeText(declarator, src);
@@ -142,8 +190,7 @@ function extractDocstring(node) {
142
190
  }
143
191
  if (lineComments.length > 0) {
144
192
  const joined = lineComments.join(' ');
145
- const match = joined.match(/^([^.!?]*[.!?]?)/);
146
- return (match ? match[1].trim() : joined) || null;
193
+ return joined.slice(0, 400) || null;
147
194
  }
148
195
  // Block comment /* ... */ or /** ... */
149
196
  if (prev && prev.type === 'comment') {
@@ -158,8 +205,7 @@ function extractDocstring(node) {
158
205
  .join(' ');
159
206
  if (!inner)
160
207
  return null;
161
- const match = inner.match(/^([^.!?]*[.!?]?)/);
162
- return (match ? match[1].trim() : inner) || null;
208
+ return inner.slice(0, 400) || null;
163
209
  }
164
210
  }
165
211
  return null;
@@ -173,8 +219,83 @@ function extractSymbols(tree, source, filePath) {
173
219
  return symbols;
174
220
  }
175
221
  function walkNodes(nodes, ctx, filePath, src, symbols, templatePrefix) {
176
- for (const node of nodes) {
222
+ let i = 0;
223
+ while (i < nodes.length) {
224
+ const node = nodes[i];
225
+ // ── Detect sibling-level class/struct declaration (NAMESPACE_BEGIN pattern) ──
226
+ // When NAMESPACE_BEGIN macros (or other constructs) prevent proper parsing,
227
+ // tree-sitter may scatter the class declaration across consecutive sibling nodes:
228
+ // [i] class_specifier (no field_declaration_list body — e.g. "class MI_EXPORT_LIB")
229
+ // [i+1] ERROR (contains the class name as first identifier/type_identifier)
230
+ // [i+k] { (opening brace)
231
+ // [i+k+1..j-1] body children
232
+ // [j] } (closing brace)
233
+ //
234
+ // This fires only when the class_specifier has no body (otherwise walkNode handles it).
235
+ if ((node.type === 'class_specifier' || node.type === 'struct_specifier') &&
236
+ !node.children.some((c) => c.type === 'field_declaration_list')) {
237
+ const nextSib = nodes[i + 1];
238
+ if (nextSib && nextSib.type === 'ERROR') {
239
+ const nameIdent = nextSib.children.find((c) => c.isNamed && (c.type === 'identifier' || c.type === 'type_identifier'));
240
+ const localName = nameIdent ? nodeText(nameIdent, src) : null;
241
+ if (localName && localName !== 'final' && localName !== 'override') {
242
+ // Find the opening brace in subsequent siblings (skipping base-class / template args)
243
+ let braceAbsIdx = -1;
244
+ for (let j = i + 2; j < nodes.length && j < i + 10; j++) {
245
+ if (nodes[j].type === '{') {
246
+ braceAbsIdx = j;
247
+ break;
248
+ }
249
+ }
250
+ if (braceAbsIdx >= 0) {
251
+ const isStruct = node.type === 'struct_specifier';
252
+ // Find closing brace
253
+ let closeAbsIdx = nodes.length;
254
+ for (let j = braceAbsIdx + 1; j < nodes.length; j++) {
255
+ if (nodes[j].type === '}') {
256
+ closeAbsIdx = j;
257
+ break;
258
+ }
259
+ }
260
+ const bodyChildren = nodes.slice(braceAbsIdx + 1, closeAbsIdx);
261
+ // Body snippet
262
+ const braceNode = nodes[braceAbsIdx];
263
+ const closeNode = closeAbsIdx < nodes.length ? nodes[closeAbsIdx] : null;
264
+ const bodyRaw = src.slice(braceNode.endIndex, Math.min(braceNode.endIndex + 600, closeNode ? closeNode.startIndex : node.endIndex));
265
+ const bodySnippet = bodyRaw.replace(/\s+/g, ' ').trim().slice(0, 200);
266
+ const nsPart = ctx.nsStack.length > 0 ? ctx.nsStack.join('::') + '::' : '';
267
+ const qualName = nsPart + localName;
268
+ const kind = isStruct ? 'struct' : 'class';
269
+ // Look back for template_parameter_list among preceding siblings
270
+ const tmplParamsNode = nodes.slice(0, i).findLast((c) => c.type === 'template_parameter_list');
271
+ const tmplParamText = tmplParamsNode ? nodeText(tmplParamsNode, src) : '';
272
+ const tmplPrefixLocal = tmplParamText
273
+ ? `template${tmplParamText}`
274
+ : (templatePrefix ?? null);
275
+ let sig = `${isStruct ? 'struct' : 'class'} ${qualName}`;
276
+ if (tmplPrefixLocal)
277
+ sig = `${tmplPrefixLocal} ${sig}`;
278
+ symbols.push({
279
+ id: makeId(filePath, qualName, kind),
280
+ name: qualName,
281
+ kind,
282
+ filePath,
283
+ startByte: node.startIndex,
284
+ endByte: closeNode ? closeNode.endIndex : node.endIndex,
285
+ signature: trunc(sig),
286
+ summary: extractDocstring(node) ?? `C++ ${kind}: ${qualName}`,
287
+ ...(bodySnippet ? { bodySnippet } : {}),
288
+ });
289
+ walkErrorClassBody(bodyChildren, { nsStack: ctx.nsStack, className: qualName }, filePath, src, symbols, isStruct);
290
+ // Advance past all consumed siblings (ERROR, template_function, {, body, })
291
+ i = closeAbsIdx + 1;
292
+ continue;
293
+ }
294
+ }
295
+ }
296
+ }
177
297
  walkNode(node, ctx, filePath, src, symbols, templatePrefix);
298
+ i++;
178
299
  }
179
300
  }
180
301
  function walkNode(node, ctx, filePath, src, symbols, templatePrefix) {
@@ -219,7 +340,16 @@ function walkNode(node, ctx, filePath, src, symbols, templatePrefix) {
219
340
  }
220
341
  // ── class_specifier ──────────────────────────────────────────────────────
221
342
  case 'class_specifier': {
222
- const nameNode = node.children.find((c) => c.type === 'type_identifier');
343
+ // Use the LAST type_identifier before the base clause or body.
344
+ // This handles `class MI_EXPORT_LIB ClassName` patterns correctly —
345
+ // the export macro appears as the first type_identifier, the real name last.
346
+ let nameNode;
347
+ for (const child of node.children) {
348
+ if (child.type === 'base_class_clause' || child.type === 'field_declaration_list')
349
+ break;
350
+ if (child.type === 'type_identifier')
351
+ nameNode = child;
352
+ }
223
353
  if (!nameNode)
224
354
  break; // anonymous class
225
355
  const localName = nodeText(nameNode, src);
@@ -237,6 +367,7 @@ function walkNode(node, ctx, filePath, src, symbols, templatePrefix) {
237
367
  endByte: node.endIndex,
238
368
  signature: buildClassSignature(node, src, qualName, false, templatePrefix ?? null),
239
369
  summary: extractDocstring(node) ?? `C++ class: ${qualName}`,
370
+ bodySnippet: extractBodySnippet(bodyNode, src),
240
371
  });
241
372
  // Walk class body with private as default access
242
373
  walkClassBody(bodyNode, { nsStack: ctx.nsStack, className: qualName }, filePath, src, symbols, false);
@@ -244,7 +375,15 @@ function walkNode(node, ctx, filePath, src, symbols, templatePrefix) {
244
375
  }
245
376
  // ── struct_specifier ─────────────────────────────────────────────────────
246
377
  case 'struct_specifier': {
247
- const nameNode = node.children.find((c) => c.type === 'type_identifier');
378
+ // Use the LAST type_identifier before the base clause or body.
379
+ // Handles `struct MI_EXPORT_LIB StructName` patterns correctly.
380
+ let nameNode;
381
+ for (const child of node.children) {
382
+ if (child.type === 'base_class_clause' || child.type === 'field_declaration_list')
383
+ break;
384
+ if (child.type === 'type_identifier')
385
+ nameNode = child;
386
+ }
248
387
  if (!nameNode)
249
388
  break;
250
389
  const localName = nodeText(nameNode, src);
@@ -262,6 +401,7 @@ function walkNode(node, ctx, filePath, src, symbols, templatePrefix) {
262
401
  endByte: node.endIndex,
263
402
  signature: buildClassSignature(node, src, qualName, true, templatePrefix ?? null),
264
403
  summary: extractDocstring(node) ?? `C++ struct: ${qualName}`,
404
+ bodySnippet: extractBodySnippet(bodyNode, src),
265
405
  });
266
406
  // Walk struct body with public as default access
267
407
  walkClassBody(bodyNode, { nsStack: ctx.nsStack, className: qualName }, filePath, src, symbols, true);
@@ -326,6 +466,65 @@ function walkNode(node, ctx, filePath, src, symbols, templatePrefix) {
326
466
  }
327
467
  // ── function_definition at namespace / global scope ──────────────────────
328
468
  case 'function_definition': {
469
+ // ── Detect `class/struct MACRO ClassName [: Base] { ... }` misparse ───
470
+ // tree-sitter-cpp misparsed these as function_definition where the
471
+ // "return type" is class_specifier(MACRO) and body is compound_statement.
472
+ // e.g. `class MI_EXPORT_LIB Scene : public Base { ... }`.
473
+ const exportMacroType = node.children.find((c) => c.isNamed && (c.type === 'class_specifier' || c.type === 'struct_specifier'));
474
+ if (exportMacroType) {
475
+ const isStruct = exportMacroType.type === 'struct_specifier';
476
+ const typeIdx = node.children.indexOf(exportMacroType);
477
+ // Find real class name: identifier after specifier, or inside ERROR node
478
+ let localName = null;
479
+ for (let i = typeIdx + 1; i < node.children.length; i++) {
480
+ const c = node.children[i];
481
+ if (c.type === 'compound_statement')
482
+ break; // hit the body
483
+ if (c.isNamed && c.type === 'identifier') {
484
+ localName = nodeText(c, src);
485
+ break;
486
+ }
487
+ if (c.type === 'ERROR') {
488
+ // Class name lands in ERROR when there's a base-class clause after it
489
+ const inner = c.children.find((ec) => ec.isNamed && ec.type === 'identifier');
490
+ if (inner) {
491
+ localName = nodeText(inner, src);
492
+ break;
493
+ }
494
+ }
495
+ }
496
+ // Guard: if no class name found, this is NOT an export macro class —
497
+ // it's a regular C function whose return type happens to be a
498
+ // struct/class specifier (e.g. `struct AP_info *get_ap_info(...)`).
499
+ // Fall through to the normal function_definition extraction below.
500
+ if (localName) {
501
+ const nsPart = ctx.nsStack.length > 0 ? ctx.nsStack.join('::') + '::' : '';
502
+ const qualName = nsPart + localName;
503
+ const kind = isStruct ? 'struct' : 'class';
504
+ const keyword = isStruct ? 'struct' : 'class';
505
+ let sig = `${keyword} ${qualName}`;
506
+ if (templatePrefix)
507
+ sig = trunc(`${templatePrefix} ${sig}`);
508
+ const body = node.children.find((c) => c.type === 'compound_statement');
509
+ symbols.push({
510
+ id: makeId(filePath, qualName, kind),
511
+ name: qualName,
512
+ kind,
513
+ filePath,
514
+ startByte: node.startIndex,
515
+ endByte: node.endIndex,
516
+ signature: trunc(sig),
517
+ summary: extractDocstring(node) ?? `C++ ${kind}: ${qualName}`,
518
+ ...(body ? { bodySnippet: extractBodySnippet(body, src) } : {}),
519
+ });
520
+ // Walk the compound_statement to extract public method declarations
521
+ if (body) {
522
+ walkExportMacroClassBody(body, { nsStack: ctx.nsStack, className: qualName }, filePath, src, symbols, isStruct);
523
+ }
524
+ break;
525
+ }
526
+ // localName was null → fall through to normal function extraction
527
+ }
329
528
  const declaratorChild = node.children.find((c) => c.isNamed &&
330
529
  [
331
530
  'function_declarator',
@@ -381,6 +580,175 @@ function walkNode(node, ctx, filePath, src, symbols, templatePrefix) {
381
580
  });
382
581
  break;
383
582
  }
583
+ // ── type_definition: C-style typedef struct/enum/union ───────────────────
584
+ // Handles: typedef struct { ... } Name; and typedef enum { ... } Name;
585
+ // Important for C headers (.h) included in C++ projects.
586
+ case 'type_definition': {
587
+ const typeChild = node.children.find((c) => c.isNamed && (c.type === 'struct_specifier' ||
588
+ c.type === 'union_specifier' ||
589
+ c.type === 'enum_specifier'));
590
+ if (!typeChild)
591
+ break;
592
+ // The typedef alias is the last type_identifier child of the type_definition
593
+ // (not inside the type specifier), e.g. `typedef struct { ... } AuthSession;`
594
+ const declaratorNode = node.children.findLast((c) => c.type === 'type_identifier');
595
+ if (!declaratorNode)
596
+ break;
597
+ const alias = nodeText(declaratorNode, src);
598
+ const nsPart = ctx.nsStack.length > 0 ? ctx.nsStack.join('::') + '::' : '';
599
+ const qualName = nsPart + alias;
600
+ if (typeChild.type === 'enum_specifier') {
601
+ const enumeratorList = typeChild.children.find((c) => c.type === 'enumerator_list');
602
+ let sig;
603
+ if (enumeratorList) {
604
+ const vals = enumeratorList.children
605
+ .filter((c) => c.isNamed && c.type === 'enumerator')
606
+ .slice(0, 3)
607
+ .map((c) => {
608
+ const id = c.children.find((cc) => cc.type === 'identifier');
609
+ return id ? nodeText(id, src) : nodeText(c, src);
610
+ });
611
+ const suffix = enumeratorList.children.filter((c) => c.isNamed && c.type === 'enumerator').length > 3 ? ', ...' : '';
612
+ sig = trunc(`typedef enum ${alias} { ${vals.join(', ')}${suffix} }`);
613
+ }
614
+ else {
615
+ sig = trunc(`typedef enum ${alias}`);
616
+ }
617
+ symbols.push({
618
+ id: makeId(filePath, qualName, 'enum'),
619
+ name: qualName,
620
+ kind: 'enum',
621
+ filePath,
622
+ startByte: node.startIndex,
623
+ endByte: node.endIndex,
624
+ signature: sig,
625
+ summary: extractDocstring(node) ?? `C enum: ${qualName}`,
626
+ });
627
+ }
628
+ else {
629
+ // struct or union
630
+ const isUnion = typeChild.type === 'union_specifier';
631
+ symbols.push({
632
+ id: makeId(filePath, qualName, 'struct'),
633
+ name: qualName,
634
+ kind: 'struct',
635
+ filePath,
636
+ startByte: node.startIndex,
637
+ endByte: node.endIndex,
638
+ signature: trunc(`typedef ${isUnion ? 'union' : 'struct'} ${alias}`),
639
+ summary: extractDocstring(node) ?? `C ${isUnion ? 'union' : 'struct'}: ${qualName}`,
640
+ });
641
+ }
642
+ break;
643
+ }
644
+ // ── ERROR recovery: template class declarations misparsed by tree-sitter ──
645
+ // Fires for mitsuba3-style headers where NAMESPACE_BEGIN(mitsuba) macros
646
+ // (or preceding #include directives) confuse tree-sitter so the whole
647
+ // `template <Ts...> class MI_EXPORT_LIB ClassName final : public Base { ... }`
648
+ // becomes an ERROR node instead of a proper template_declaration.
649
+ //
650
+ // Two child patterns observed:
651
+ //
652
+ // Pattern 1 — export macro (scene.h, MI_EXPORT_LIB):
653
+ // template
654
+ // template_parameter_list <typename Float, typename Spectrum>
655
+ // class_specifier "class MI_EXPORT_LIB"
656
+ // ERROR "Scene final : public ..."
657
+ // identifier "Scene" ← class name (clean)
658
+ // {
659
+ // ... labeled_statement / ERROR / declaration ... (body)
660
+ //
661
+ // Pattern 2 — no export macro (microfacet.h, MicrofacetDistribution):
662
+ // template
663
+ // template_parameter_list <typename Float, typename Spectrum>
664
+ // class (bare keyword, unnamed)
665
+ // type_identifier "MicrofacetDistribution" ← class name directly
666
+ // base_class_clause
667
+ // {
668
+ // access_specifier public
669
+ // ... declaration / function_definition ... (body)
670
+ case 'ERROR': {
671
+ const children = node.children;
672
+ // Must have 'template' keyword child
673
+ if (!children.some((c) => c.type === 'template'))
674
+ break;
675
+ const tmplParams = children.find((c) => c.type === 'template_parameter_list');
676
+ if (!tmplParams)
677
+ break;
678
+ let isStruct = false;
679
+ let localName = null;
680
+ // Pattern 1: class_specifier (export macro: "class MI_EXPORT_LIB")
681
+ const classSpec = children.find((c) => c.type === 'class_specifier' || c.type === 'struct_specifier');
682
+ if (classSpec) {
683
+ isStruct = classSpec.type === 'struct_specifier';
684
+ const classSpecIdx = children.indexOf(classSpec);
685
+ for (let i = classSpecIdx + 1; i < children.length; i++) {
686
+ const c = children[i];
687
+ if (c.type === '{')
688
+ break;
689
+ if (c.isNamed && c.type === 'identifier') {
690
+ localName = nodeText(c, src);
691
+ break;
692
+ }
693
+ if (c.type === 'ERROR') {
694
+ const inner = c.children.find((ec) => ec.isNamed && ec.type === 'identifier');
695
+ if (inner) {
696
+ localName = nodeText(inner, src);
697
+ break;
698
+ }
699
+ }
700
+ }
701
+ }
702
+ else {
703
+ // Pattern 2: bare 'class'/'struct' keyword followed by type_identifier
704
+ const classKwIdx = children.findIndex((c) => c.type === 'class' || c.type === 'struct');
705
+ if (classKwIdx < 0)
706
+ break;
707
+ isStruct = children[classKwIdx].type === 'struct';
708
+ for (let i = classKwIdx + 1; i < children.length; i++) {
709
+ const c = children[i];
710
+ if (c.type === '{')
711
+ break;
712
+ if (c.isNamed && (c.type === 'type_identifier' || c.type === 'identifier')) {
713
+ localName = nodeText(c, src);
714
+ break;
715
+ }
716
+ }
717
+ }
718
+ if (!localName || localName === 'final' || localName === 'override')
719
+ break;
720
+ const nsPart = ctx.nsStack.length > 0 ? ctx.nsStack.join('::') + '::' : '';
721
+ const qualName = nsPart + localName;
722
+ const kind = isStruct ? 'struct' : 'class';
723
+ const tmplParamText = nodeText(tmplParams, src);
724
+ const tmplPrefix = `template${tmplParamText}`;
725
+ // Build body snippet from the content between '{' and the node end
726
+ const braceIdx = children.findIndex((c) => c.type === '{');
727
+ let bodySnippet = '';
728
+ if (braceIdx >= 0) {
729
+ const afterBrace = children[braceIdx].endIndex;
730
+ const closingBrace = children.findLast((c) => c.type === '}');
731
+ const bodyEnd = closingBrace ? closingBrace.startIndex : node.endIndex;
732
+ const rawBody = src.slice(afterBrace, Math.min(afterBrace + 600, bodyEnd));
733
+ bodySnippet = rawBody.replace(/\s+/g, ' ').trim().slice(0, 200);
734
+ }
735
+ symbols.push({
736
+ id: makeId(filePath, qualName, kind),
737
+ name: qualName,
738
+ kind,
739
+ filePath,
740
+ startByte: node.startIndex,
741
+ endByte: node.endIndex,
742
+ signature: trunc(`${tmplPrefix} ${isStruct ? 'struct' : 'class'} ${qualName}`),
743
+ summary: extractDocstring(node) ?? `C++ ${kind}: ${qualName}`,
744
+ ...(bodySnippet ? { bodySnippet } : {}),
745
+ });
746
+ // Walk body children for method symbols
747
+ if (braceIdx >= 0) {
748
+ walkErrorClassBody(children.slice(braceIdx + 1), { nsStack: ctx.nsStack, className: qualName }, filePath, src, symbols, isStruct);
749
+ }
750
+ break;
751
+ }
384
752
  // ── preproc_def: object-like #define macros ──────────────────────────────
385
753
  case 'preproc_def': {
386
754
  const nameNode = node.children.find((c) => c.type === 'identifier');
@@ -405,10 +773,211 @@ function walkNode(node, ctx, filePath, src, symbols, templatePrefix) {
405
773
  });
406
774
  break;
407
775
  }
776
+ // ── expression_statement: may wrap a registration macro call_expression ────
777
+ // tree-sitter-cpp parses REGISTER_OP("Abs") and FUNCTION_REGISTER(Foo) as
778
+ // expression_statement > call_expression, NOT as macro_invocation nodes.
779
+ case 'expression_statement': {
780
+ const callExpr = node.children.find((c) => c.type === 'call_expression');
781
+ if (!callExpr)
782
+ break;
783
+ const macroNameNode = callExpr.children.find((c) => c.type === 'identifier');
784
+ if (!macroNameNode)
785
+ break;
786
+ const macroName = nodeText(macroNameNode, src);
787
+ const entry = findMacroEntry(macroName);
788
+ if (!entry)
789
+ break;
790
+ const argList = callExpr.children.find((c) => c.type === 'argument_list');
791
+ if (!argList)
792
+ break;
793
+ let symbolName = null;
794
+ if (entry.argKind === 'identifier' || entry.argKind === 'any') {
795
+ const identArg = argList.children.find((c) => c.type === 'identifier');
796
+ if (identArg)
797
+ symbolName = nodeText(identArg, src);
798
+ }
799
+ if (!symbolName && (entry.argKind === 'string' || entry.argKind === 'any')) {
800
+ const strArg = argList.children.find((c) => c.type === 'string_literal' || c.type === 'raw_string_literal');
801
+ if (strArg) {
802
+ const content = strArg.children.find((c) => c.type === 'string_content');
803
+ symbolName = content
804
+ ? nodeText(content, src)
805
+ : nodeText(strArg, src).replace(/^["']|["']$/g, '').trim();
806
+ }
807
+ }
808
+ if (!symbolName)
809
+ break;
810
+ const nsPart = ctx.nsStack.length > 0 ? ctx.nsStack.join('::') + '::' : '';
811
+ const qualName = nsPart + symbolName;
812
+ symbols.push({
813
+ id: makeId(filePath, qualName, entry.symbolKind),
814
+ name: qualName,
815
+ kind: entry.symbolKind,
816
+ filePath,
817
+ startByte: node.startIndex,
818
+ endByte: node.endIndex,
819
+ signature: trunc(`${macroName}(${symbolName})`),
820
+ summary: extractDocstring(node) ?? `C++ ${entry.symbolKind} registered via ${macroName}`,
821
+ frameworkMeta: { registeredViaMacro: macroName },
822
+ });
823
+ break;
824
+ }
408
825
  default:
409
826
  break;
410
827
  }
411
828
  }
829
+ /**
830
+ * Walk a `compound_statement` that was misparsed as a function body but is
831
+ * actually the body of a class/struct declared with an export macro.
832
+ * e.g. `class MI_EXPORT_LIB Scene { public: void render(); };`
833
+ *
834
+ * tree-sitter wraps `public:` as `labeled_statement` nodes inside the compound.
835
+ */
836
+ function walkExportMacroClassBody(bodyNode, ctx, filePath, src, symbols, isStruct) {
837
+ let accessLevel = isStruct ? 'public' : 'private';
838
+ for (const child of bodyNode.children) {
839
+ if (!child.isNamed)
840
+ continue;
841
+ if (child.type === 'labeled_statement') {
842
+ // `public: decl...` or `private: decl...`
843
+ const labelNode = child.children.find((c) => c.type === 'statement_identifier');
844
+ if (labelNode) {
845
+ const label = nodeText(labelNode, src);
846
+ if (label === 'public')
847
+ accessLevel = 'public';
848
+ else if (label === 'protected')
849
+ accessLevel = 'protected';
850
+ else if (label === 'private')
851
+ accessLevel = 'private';
852
+ }
853
+ if (accessLevel === 'private')
854
+ continue;
855
+ // Walk the statement body (declarations / function definitions after label)
856
+ for (const stmt of child.children) {
857
+ if (!stmt.isNamed || stmt.type === 'statement_identifier')
858
+ continue;
859
+ if (stmt.type === 'function_definition') {
860
+ emitMethod(stmt, ctx, filePath, src, symbols, null);
861
+ }
862
+ else if (stmt.type === 'declaration') {
863
+ emitDeclMethod(stmt, ctx, filePath, src, symbols);
864
+ }
865
+ }
866
+ }
867
+ else if (child.type === 'function_definition' && accessLevel !== 'private') {
868
+ emitMethod(child, ctx, filePath, src, symbols, null);
869
+ }
870
+ else if (child.type === 'declaration' && accessLevel !== 'private') {
871
+ emitDeclMethod(child, ctx, filePath, src, symbols);
872
+ }
873
+ }
874
+ }
875
+ /**
876
+ * Walk body children of a class/struct that tree-sitter parsed inside an ERROR node.
877
+ * Body children are direct children of the ERROR node that come after the `{` token.
878
+ * Handles labeled_statement (access specifiers), ERROR (misparsed method stubs),
879
+ * function_definition, and declaration nodes.
880
+ */
881
+ function walkErrorClassBody(bodyChildren, ctx, filePath, src, symbols, isStruct) {
882
+ let accessLevel = isStruct ? 'public' : 'private';
883
+ for (const child of bodyChildren) {
884
+ // Handle direct access_specifier nodes (Pattern 2: microfacet.h style)
885
+ if (child.type === 'access_specifier') {
886
+ const text = nodeText(child, src);
887
+ if (text.startsWith('public'))
888
+ accessLevel = 'public';
889
+ else if (text.startsWith('protected'))
890
+ accessLevel = 'protected';
891
+ else if (text.startsWith('private'))
892
+ accessLevel = 'private';
893
+ continue;
894
+ }
895
+ if (!child.isNamed)
896
+ continue;
897
+ if (child.type === 'labeled_statement') {
898
+ const labelNode = child.children.find((c) => c.type === 'statement_identifier');
899
+ if (labelNode) {
900
+ const label = nodeText(labelNode, src);
901
+ if (label === 'public')
902
+ accessLevel = 'public';
903
+ else if (label === 'protected')
904
+ accessLevel = 'protected';
905
+ else if (label === 'private')
906
+ accessLevel = 'private';
907
+ }
908
+ if (accessLevel === 'private')
909
+ continue;
910
+ // Walk statement body (declarations / definitions after the access label)
911
+ for (const stmt of child.children) {
912
+ if (!stmt.isNamed || stmt.type === 'statement_identifier')
913
+ continue;
914
+ if (stmt.type === 'function_definition') {
915
+ emitMethod(stmt, ctx, filePath, src, symbols, null);
916
+ }
917
+ else if (stmt.type === 'declaration') {
918
+ emitDeclMethod(stmt, ctx, filePath, src, symbols);
919
+ }
920
+ }
921
+ }
922
+ else if (child.type === 'function_definition' && accessLevel !== 'private') {
923
+ emitMethod(child, ctx, filePath, src, symbols, null);
924
+ }
925
+ else if (child.type === 'declaration' && accessLevel !== 'private') {
926
+ emitDeclMethod(child, ctx, filePath, src, symbols);
927
+ }
928
+ else if (child.type === 'ERROR' && accessLevel !== 'private') {
929
+ // Method declarations misparsed as ERROR — look for function_declarator child
930
+ const funcDecl = child.children.find((c) => c.type === 'function_declarator');
931
+ if (funcDecl) {
932
+ const rawName = extractDeclaratorName(funcDecl, src);
933
+ if (rawName) {
934
+ const methodName = ctx.className ? ctx.className + '::' + rawName : rawName;
935
+ symbols.push({
936
+ id: makeId(filePath, methodName, 'method'),
937
+ name: methodName,
938
+ kind: 'method',
939
+ filePath,
940
+ startByte: child.startIndex,
941
+ endByte: child.endIndex,
942
+ signature: trunc(nodeText(child, src).replace(/\s+/g, ' ')),
943
+ summary: extractDocstring(child) ?? `C++ method: ${methodName}`,
944
+ });
945
+ }
946
+ }
947
+ }
948
+ }
949
+ }
950
+ /** Extract a method symbol from a `declaration` node (header-only method stub). */
951
+ function emitDeclMethod(node, ctx, filePath, src, symbols) {
952
+ const hasFuncDecl = node.children.some((c) => c.isNamed &&
953
+ (c.type === 'function_declarator' || findFunctionDeclarator(c) !== null));
954
+ if (!hasFuncDecl)
955
+ return;
956
+ const declaratorChild = node.children.find((c) => c.isNamed &&
957
+ [
958
+ 'function_declarator',
959
+ 'pointer_declarator',
960
+ 'reference_declarator',
961
+ 'parenthesized_declarator',
962
+ ].includes(c.type));
963
+ if (!declaratorChild)
964
+ return;
965
+ const rawName = extractDeclaratorName(declaratorChild, src);
966
+ if (!rawName)
967
+ return;
968
+ const methodName = ctx.className ? ctx.className + '::' + rawName : rawName;
969
+ const declText = nodeText(node, src).replace(/;\s*$/, '').trim();
970
+ symbols.push({
971
+ id: makeId(filePath, methodName, 'method'),
972
+ name: methodName,
973
+ kind: 'method',
974
+ filePath,
975
+ startByte: node.startIndex,
976
+ endByte: node.endIndex,
977
+ signature: trunc(declText),
978
+ summary: extractDocstring(node) ?? `C++ method: ${methodName}`,
979
+ });
980
+ }
412
981
  /**
413
982
  * Walk the body of a class or struct, tracking the current access level.
414
983
  * Emits methods (definitions and declarations), nested classes/structs.
@@ -673,7 +1242,7 @@ function collectImportNodes(nodes, src, imports) {
673
1242
  }
674
1243
  // ─── Handler export ───────────────────────────────────────────────────────────
675
1244
  export const cppHandler = {
676
- extensions: () => ['.cpp', '.cxx', '.cc', '.c++', '.hpp', '.hxx', '.hh', '.h++'],
1245
+ extensions: () => ['.cpp', '.cxx', '.cc', '.c++', '.hpp', '.hxx', '.hh', '.h++', '.h'],
677
1246
  grammarPath: () => resolve(GRAMMARS_DIR, 'tree-sitter-cpp.wasm'),
678
1247
  extractSymbols,
679
1248
  extractImports,