@optave/codegraph 3.11.1 → 3.12.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 (176) hide show
  1. package/README.md +8 -8
  2. package/dist/db/migrations.d.ts.map +1 -1
  3. package/dist/db/migrations.js +7 -0
  4. package/dist/db/migrations.js.map +1 -1
  5. package/dist/domain/analysis/module-map.d.ts +2 -0
  6. package/dist/domain/analysis/module-map.d.ts.map +1 -1
  7. package/dist/domain/analysis/module-map.js +24 -2
  8. package/dist/domain/analysis/module-map.js.map +1 -1
  9. package/dist/domain/graph/builder/call-resolver.d.ts +73 -0
  10. package/dist/domain/graph/builder/call-resolver.d.ts.map +1 -0
  11. package/dist/domain/graph/builder/call-resolver.js +292 -0
  12. package/dist/domain/graph/builder/call-resolver.js.map +1 -0
  13. package/dist/domain/graph/builder/cha.d.ts +61 -0
  14. package/dist/domain/graph/builder/cha.d.ts.map +1 -0
  15. package/dist/domain/graph/builder/cha.js +143 -0
  16. package/dist/domain/graph/builder/cha.js.map +1 -0
  17. package/dist/domain/graph/builder/context.d.ts +3 -0
  18. package/dist/domain/graph/builder/context.d.ts.map +1 -1
  19. package/dist/domain/graph/builder/context.js +2 -0
  20. package/dist/domain/graph/builder/context.js.map +1 -1
  21. package/dist/domain/graph/builder/helpers.d.ts +17 -1
  22. package/dist/domain/graph/builder/helpers.d.ts.map +1 -1
  23. package/dist/domain/graph/builder/helpers.js +159 -5
  24. package/dist/domain/graph/builder/helpers.js.map +1 -1
  25. package/dist/domain/graph/builder/incremental.d.ts.map +1 -1
  26. package/dist/domain/graph/builder/incremental.js +147 -54
  27. package/dist/domain/graph/builder/incremental.js.map +1 -1
  28. package/dist/domain/graph/builder/stages/build-edges.d.ts +2 -0
  29. package/dist/domain/graph/builder/stages/build-edges.d.ts.map +1 -1
  30. package/dist/domain/graph/builder/stages/build-edges.js +932 -110
  31. package/dist/domain/graph/builder/stages/build-edges.js.map +1 -1
  32. package/dist/domain/graph/builder/stages/detect-changes.d.ts.map +1 -1
  33. package/dist/domain/graph/builder/stages/detect-changes.js +2 -1
  34. package/dist/domain/graph/builder/stages/detect-changes.js.map +1 -1
  35. package/dist/domain/graph/builder/stages/native-orchestrator.d.ts.map +1 -1
  36. package/dist/domain/graph/builder/stages/native-orchestrator.js +501 -14
  37. package/dist/domain/graph/builder/stages/native-orchestrator.js.map +1 -1
  38. package/dist/domain/graph/builder/stages/resolve-imports.d.ts +1 -0
  39. package/dist/domain/graph/builder/stages/resolve-imports.d.ts.map +1 -1
  40. package/dist/domain/graph/builder/stages/resolve-imports.js +9 -0
  41. package/dist/domain/graph/builder/stages/resolve-imports.js.map +1 -1
  42. package/dist/domain/graph/journal.js +1 -1
  43. package/dist/domain/graph/journal.js.map +1 -1
  44. package/dist/domain/graph/resolver/points-to.d.ts +53 -0
  45. package/dist/domain/graph/resolver/points-to.d.ts.map +1 -0
  46. package/dist/domain/graph/resolver/points-to.js +213 -0
  47. package/dist/domain/graph/resolver/points-to.js.map +1 -0
  48. package/dist/domain/graph/resolver/ts-resolver.d.ts +9 -0
  49. package/dist/domain/graph/resolver/ts-resolver.d.ts.map +1 -0
  50. package/dist/domain/graph/resolver/ts-resolver.js +476 -0
  51. package/dist/domain/graph/resolver/ts-resolver.js.map +1 -0
  52. package/dist/domain/graph/watcher.d.ts.map +1 -1
  53. package/dist/domain/graph/watcher.js +5 -2
  54. package/dist/domain/graph/watcher.js.map +1 -1
  55. package/dist/domain/parser.d.ts +10 -1
  56. package/dist/domain/parser.d.ts.map +1 -1
  57. package/dist/domain/parser.js +39 -7
  58. package/dist/domain/parser.js.map +1 -1
  59. package/dist/domain/wasm-worker-entry.js +25 -0
  60. package/dist/domain/wasm-worker-entry.js.map +1 -1
  61. package/dist/domain/wasm-worker-pool.d.ts.map +1 -1
  62. package/dist/domain/wasm-worker-pool.js +32 -0
  63. package/dist/domain/wasm-worker-pool.js.map +1 -1
  64. package/dist/domain/wasm-worker-protocol.d.ts +14 -1
  65. package/dist/domain/wasm-worker-protocol.d.ts.map +1 -1
  66. package/dist/extractors/c.js +3 -3
  67. package/dist/extractors/c.js.map +1 -1
  68. package/dist/extractors/clojure.js +1 -1
  69. package/dist/extractors/clojure.js.map +1 -1
  70. package/dist/extractors/cpp.js +3 -3
  71. package/dist/extractors/cpp.js.map +1 -1
  72. package/dist/extractors/csharp.d.ts.map +1 -1
  73. package/dist/extractors/csharp.js +37 -8
  74. package/dist/extractors/csharp.js.map +1 -1
  75. package/dist/extractors/cuda.js +3 -3
  76. package/dist/extractors/cuda.js.map +1 -1
  77. package/dist/extractors/elixir.js +6 -6
  78. package/dist/extractors/elixir.js.map +1 -1
  79. package/dist/extractors/fsharp.js +1 -1
  80. package/dist/extractors/fsharp.js.map +1 -1
  81. package/dist/extractors/go.js +5 -5
  82. package/dist/extractors/go.js.map +1 -1
  83. package/dist/extractors/haskell.js +1 -1
  84. package/dist/extractors/haskell.js.map +1 -1
  85. package/dist/extractors/java.js +2 -2
  86. package/dist/extractors/java.js.map +1 -1
  87. package/dist/extractors/javascript.d.ts +2 -0
  88. package/dist/extractors/javascript.d.ts.map +1 -1
  89. package/dist/extractors/javascript.js +1674 -64
  90. package/dist/extractors/javascript.js.map +1 -1
  91. package/dist/extractors/kotlin.js +5 -5
  92. package/dist/extractors/kotlin.js.map +1 -1
  93. package/dist/extractors/lua.js +1 -1
  94. package/dist/extractors/lua.js.map +1 -1
  95. package/dist/extractors/objc.js +3 -3
  96. package/dist/extractors/objc.js.map +1 -1
  97. package/dist/extractors/ocaml.js +1 -1
  98. package/dist/extractors/ocaml.js.map +1 -1
  99. package/dist/extractors/php.js +2 -2
  100. package/dist/extractors/php.js.map +1 -1
  101. package/dist/extractors/python.js +7 -7
  102. package/dist/extractors/python.js.map +1 -1
  103. package/dist/extractors/ruby.js +2 -2
  104. package/dist/extractors/ruby.js.map +1 -1
  105. package/dist/extractors/scala.js +1 -1
  106. package/dist/extractors/scala.js.map +1 -1
  107. package/dist/extractors/solidity.js +1 -1
  108. package/dist/extractors/solidity.js.map +1 -1
  109. package/dist/extractors/swift.js +4 -4
  110. package/dist/extractors/swift.js.map +1 -1
  111. package/dist/extractors/zig.js +4 -4
  112. package/dist/extractors/zig.js.map +1 -1
  113. package/dist/features/structure.d.ts.map +1 -1
  114. package/dist/features/structure.js +121 -16
  115. package/dist/features/structure.js.map +1 -1
  116. package/dist/infrastructure/config.d.ts +10 -0
  117. package/dist/infrastructure/config.d.ts.map +1 -1
  118. package/dist/infrastructure/config.js +15 -0
  119. package/dist/infrastructure/config.js.map +1 -1
  120. package/dist/infrastructure/native.d.ts +11 -0
  121. package/dist/infrastructure/native.d.ts.map +1 -1
  122. package/dist/infrastructure/native.js +78 -5
  123. package/dist/infrastructure/native.js.map +1 -1
  124. package/dist/presentation/queries-cli/overview.d.ts.map +1 -1
  125. package/dist/presentation/queries-cli/overview.js +5 -0
  126. package/dist/presentation/queries-cli/overview.js.map +1 -1
  127. package/dist/types.d.ts +184 -0
  128. package/dist/types.d.ts.map +1 -1
  129. package/grammars/tree-sitter-erlang.wasm +0 -0
  130. package/package.json +9 -9
  131. package/src/db/migrations.ts +7 -0
  132. package/src/domain/analysis/module-map.ts +29 -1
  133. package/src/domain/graph/builder/call-resolver.ts +351 -0
  134. package/src/domain/graph/builder/cha.ts +175 -0
  135. package/src/domain/graph/builder/context.ts +3 -0
  136. package/src/domain/graph/builder/helpers.ts +175 -5
  137. package/src/domain/graph/builder/incremental.ts +186 -66
  138. package/src/domain/graph/builder/stages/build-edges.ts +1146 -146
  139. package/src/domain/graph/builder/stages/detect-changes.ts +3 -1
  140. package/src/domain/graph/builder/stages/native-orchestrator.ts +583 -20
  141. package/src/domain/graph/builder/stages/resolve-imports.ts +14 -0
  142. package/src/domain/graph/journal.ts +1 -1
  143. package/src/domain/graph/resolver/points-to.ts +254 -0
  144. package/src/domain/graph/resolver/ts-resolver.ts +536 -0
  145. package/src/domain/graph/watcher.ts +4 -2
  146. package/src/domain/parser.ts +43 -5
  147. package/src/domain/wasm-worker-entry.ts +25 -0
  148. package/src/domain/wasm-worker-pool.ts +21 -0
  149. package/src/domain/wasm-worker-protocol.ts +14 -0
  150. package/src/extractors/c.ts +3 -3
  151. package/src/extractors/clojure.ts +1 -1
  152. package/src/extractors/cpp.ts +3 -3
  153. package/src/extractors/csharp.ts +33 -9
  154. package/src/extractors/cuda.ts +3 -3
  155. package/src/extractors/elixir.ts +6 -6
  156. package/src/extractors/fsharp.ts +1 -1
  157. package/src/extractors/go.ts +5 -5
  158. package/src/extractors/haskell.ts +1 -1
  159. package/src/extractors/java.ts +2 -2
  160. package/src/extractors/javascript.ts +1802 -66
  161. package/src/extractors/kotlin.ts +5 -5
  162. package/src/extractors/lua.ts +1 -1
  163. package/src/extractors/objc.ts +3 -3
  164. package/src/extractors/ocaml.ts +1 -1
  165. package/src/extractors/php.ts +2 -2
  166. package/src/extractors/python.ts +7 -7
  167. package/src/extractors/ruby.ts +2 -2
  168. package/src/extractors/scala.ts +1 -1
  169. package/src/extractors/solidity.ts +1 -1
  170. package/src/extractors/swift.ts +4 -4
  171. package/src/extractors/zig.ts +4 -4
  172. package/src/features/structure.ts +143 -23
  173. package/src/infrastructure/config.ts +15 -0
  174. package/src/infrastructure/native.ts +87 -5
  175. package/src/presentation/queries-cli/overview.ts +15 -1
  176. package/src/types.ts +194 -0
@@ -102,7 +102,7 @@ function collectKotlinEnumEntries(node: TreeSitterNode): SubDeclaration[] {
102
102
  if (!body) return entries;
103
103
  for (let i = 0; i < body.childCount; i++) {
104
104
  const child = body.child(i);
105
- if (!child || child.type !== 'enum_entry') continue;
105
+ if (child?.type !== 'enum_entry') continue;
106
106
  const entryName = findChild(child, 'simple_identifier');
107
107
  if (entryName) {
108
108
  entries.push({
@@ -122,7 +122,7 @@ function collectKotlinProperties(node: TreeSitterNode): SubDeclaration[] {
122
122
  if (!body) return props;
123
123
  for (let i = 0; i < body.childCount; i++) {
124
124
  const child = body.child(i);
125
- if (!child || child.type !== 'property_declaration') continue;
125
+ if (child?.type !== 'property_declaration') continue;
126
126
  const varDecl = findChild(child, 'variable_declaration');
127
127
  if (!varDecl) continue;
128
128
  const id = findChild(varDecl, 'simple_identifier');
@@ -144,7 +144,7 @@ function collectKotlinMethods(node: TreeSitterNode, className: string, ctx: Extr
144
144
  if (!body) return;
145
145
  for (let i = 0; i < body.childCount; i++) {
146
146
  const child = body.child(i);
147
- if (!child || child.type !== 'function_declaration') continue;
147
+ if (child?.type !== 'function_declaration') continue;
148
148
  const methName = findChild(child, 'simple_identifier');
149
149
  if (methName) {
150
150
  const params = extractKotlinParameters(child);
@@ -168,7 +168,7 @@ function collectKotlinInheritance(
168
168
  ): void {
169
169
  for (let i = 0; i < node.childCount; i++) {
170
170
  const child = node.child(i);
171
- if (!child || child.type !== 'delegation_specifier') continue;
171
+ if (child?.type !== 'delegation_specifier') continue;
172
172
 
173
173
  // constructor_invocation > user_type > type_identifier (extends)
174
174
  const ctorInvocation = findChild(child, 'constructor_invocation');
@@ -294,7 +294,7 @@ function extractKotlinParameters(funcNode: TreeSitterNode): SubDeclaration[] {
294
294
  if (!paramList) return params;
295
295
  for (let i = 0; i < paramList.childCount; i++) {
296
296
  const param = paramList.child(i);
297
- if (!param || param.type !== 'parameter') continue;
297
+ if (param?.type !== 'parameter') continue;
298
298
  const nameNode = findChild(param, 'simple_identifier');
299
299
  if (nameNode) {
300
300
  params.push({ name: nameNode.text, kind: 'parameter', line: param.startPosition.row + 1 });
@@ -90,7 +90,7 @@ function extractLuaParams(funcNode: TreeSitterNode): SubDeclaration[] {
90
90
 
91
91
  for (let i = 0; i < paramList.childCount; i++) {
92
92
  const param = paramList.child(i);
93
- if (!param || param.type !== 'identifier') continue;
93
+ if (param?.type !== 'identifier') continue;
94
94
  params.push({ name: param.text, kind: 'parameter', line: param.startPosition.row + 1 });
95
95
  }
96
96
  return params;
@@ -468,7 +468,7 @@ function extractPropertyName(propNode: TreeSitterNode): string | null {
468
468
  if (!structDecl) return null;
469
469
  for (let i = 0; i < structDecl.childCount; i++) {
470
470
  const child = structDecl.child(i);
471
- if (!child || child.type !== 'struct_declarator') continue;
471
+ if (child?.type !== 'struct_declarator') continue;
472
472
  const id = findIdentifierDeep(child);
473
473
  if (id) return id.text;
474
474
  }
@@ -494,7 +494,7 @@ function extractMethodParams(methodNode: TreeSitterNode): SubDeclaration[] {
494
494
  const params: SubDeclaration[] = [];
495
495
  for (let i = 0; i < methodNode.childCount; i++) {
496
496
  const child = methodNode.child(i);
497
- if (!child || child.type !== 'method_parameter') continue;
497
+ if (child?.type !== 'method_parameter') continue;
498
498
  let nameNode: TreeSitterNode | null = null;
499
499
  for (let j = 0; j < child.childCount; j++) {
500
500
  const inner = child.child(j);
@@ -516,7 +516,7 @@ function extractCParams(paramListNode: TreeSitterNode | null): SubDeclaration[]
516
516
  if (!paramListNode) return params;
517
517
  for (let i = 0; i < paramListNode.childCount; i++) {
518
518
  const param = paramListNode.child(i);
519
- if (!param || param.type !== 'parameter_declaration') continue;
519
+ if (param?.type !== 'parameter_declaration') continue;
520
520
  const nameNode = param.childForFieldName('declarator');
521
521
  if (nameNode) {
522
522
  const name = unwrapObjCDeclaratorName(nameNode);
@@ -168,7 +168,7 @@ function handleOCamlTypeDef(node: TreeSitterNode, ctx: ExtractorOutput): void {
168
168
  // type_definition contains one or more type_bindings
169
169
  for (let i = 0; i < node.childCount; i++) {
170
170
  const child = node.child(i);
171
- if (!child || child.type !== 'type_binding') continue;
171
+ if (child?.type !== 'type_binding') continue;
172
172
 
173
173
  const nameNode =
174
174
  child.childForFieldName('name') ||
@@ -36,7 +36,7 @@ function extractPhpParameters(fnNode: TreeSitterNode): SubDeclaration[] {
36
36
  function extractPhpProperties(member: TreeSitterNode, children: SubDeclaration[]): void {
37
37
  for (let j = 0; j < member.childCount; j++) {
38
38
  const el = member.child(j);
39
- if (!el || el.type !== 'property_element') continue;
39
+ if (el?.type !== 'property_element') continue;
40
40
  const varNode = findChild(el, 'variable_name');
41
41
  if (varNode) {
42
42
  children.push({
@@ -53,7 +53,7 @@ function extractPhpProperties(member: TreeSitterNode, children: SubDeclaration[]
53
53
  function extractPhpConstants(member: TreeSitterNode, children: SubDeclaration[]): void {
54
54
  for (let j = 0; j < member.childCount; j++) {
55
55
  const el = member.child(j);
56
- if (!el || el.type !== 'const_element') continue;
56
+ if (el?.type !== 'const_element') continue;
57
57
  const nameNode = el.childForFieldName('name') || findChild(el, 'name');
58
58
  if (nameNode) {
59
59
  children.push({
@@ -291,7 +291,7 @@ function extractClassAssignment(
291
291
  const assignment = findChild(child, 'assignment');
292
292
  if (!assignment) return;
293
293
  const left = assignment.childForFieldName('left');
294
- if (!left || left.type !== 'identifier' || seen.has(left.text)) return;
294
+ if (left?.type !== 'identifier' || seen.has(left.text)) return;
295
295
  seen.add(left.text);
296
296
  props.push({
297
297
  name: left.text,
@@ -308,7 +308,7 @@ function extractInitProperties(
308
308
  props: SubDeclaration[],
309
309
  ): void {
310
310
  const fnName = node.childForFieldName('name');
311
- if (!fnName || fnName.text !== '__init__') return;
311
+ if (fnName?.text !== '__init__') return;
312
312
  const initBody = node.childForFieldName('body') || findChild(node, 'block');
313
313
  if (initBody) walkInitBody(initBody, seen, props);
314
314
  }
@@ -342,11 +342,11 @@ function extractPythonClassProperties(classNode: TreeSitterNode): SubDeclaration
342
342
  function walkInitBody(bodyNode: TreeSitterNode, seen: Set<string>, props: SubDeclaration[]): void {
343
343
  for (let i = 0; i < bodyNode.childCount; i++) {
344
344
  const stmt = bodyNode.child(i);
345
- if (!stmt || stmt.type !== 'expression_statement') continue;
345
+ if (stmt?.type !== 'expression_statement') continue;
346
346
  const assignment = findChild(stmt, 'assignment');
347
347
  if (!assignment) continue;
348
348
  const left = assignment.childForFieldName('left');
349
- if (!left || left.type !== 'attribute') continue;
349
+ if (left?.type !== 'attribute') continue;
350
350
  const obj = left.childForFieldName('object');
351
351
  const attr = left.childForFieldName('attribute');
352
352
  if (obj && obj.text === 'self' && attr && attr.type === 'identifier' && !seen.has(attr.text)) {
@@ -370,7 +370,7 @@ function handlePyTypedParam(node: TreeSitterNode, ctx: ExtractorOutput): void {
370
370
  const isDefault = node.type === 'typed_default_parameter';
371
371
  const nameNode = isDefault ? node.childForFieldName('name') : node.child(0);
372
372
  const typeNode = node.childForFieldName('type');
373
- if (!nameNode || nameNode.type !== 'identifier' || !typeNode) return;
373
+ if (nameNode?.type !== 'identifier' || !typeNode) return;
374
374
  if (nameNode.text === 'self' || nameNode.text === 'cls') return;
375
375
  const typeName = extractPythonTypeName(typeNode);
376
376
  if (typeName && ctx.typeMap) setTypeMapEntry(ctx.typeMap, nameNode.text, typeName, 0.9);
@@ -380,7 +380,7 @@ function handlePyTypedParam(node: TreeSitterNode, ctx: ExtractorOutput): void {
380
380
  function handlePyAssignmentType(node: TreeSitterNode, ctx: ExtractorOutput): void {
381
381
  const left = node.childForFieldName('left');
382
382
  const right = node.childForFieldName('right');
383
- if (!left || left.type !== 'identifier' || !right || right.type !== 'call') return;
383
+ if (left?.type !== 'identifier' || !right || right.type !== 'call') return;
384
384
 
385
385
  const fn = right.childForFieldName('function');
386
386
  if (!fn) return;
@@ -391,7 +391,7 @@ function handlePyAssignmentType(node: TreeSitterNode, ctx: ExtractorOutput): voi
391
391
  }
392
392
  } else if (fn.type === 'attribute') {
393
393
  const obj = fn.childForFieldName('object');
394
- if (!obj || obj.type !== 'identifier') return;
394
+ if (obj?.type !== 'identifier') return;
395
395
  const objName = obj.text;
396
396
  if (objName[0] && objName[0] !== objName[0].toLowerCase() && !BUILTIN_GLOBALS_PY.has(objName)) {
397
397
  if (ctx.typeMap) setTypeMapEntry(ctx.typeMap, left.text, objName, 0.7);
@@ -264,7 +264,7 @@ function extractRubyBodyConstants(containerNode: TreeSitterNode): SubDeclaration
264
264
  if (!body) return children;
265
265
  for (let i = 0; i < body.childCount; i++) {
266
266
  const child = body.child(i);
267
- if (!child || child.type !== 'assignment') continue;
267
+ if (child?.type !== 'assignment') continue;
268
268
  const left = child.childForFieldName('left');
269
269
  if (left && left.type === 'constant') {
270
270
  children.push({ name: left.text, kind: 'constant', line: child.startPosition.row + 1 });
@@ -279,7 +279,7 @@ function extractRubyClassChildren(classNode: TreeSitterNode): SubDeclaration[] {
279
279
  if (!body) return children;
280
280
  for (let i = 0; i < body.childCount; i++) {
281
281
  const child = body.child(i);
282
- if (!child || child.type !== 'assignment') continue;
282
+ if (child?.type !== 'assignment') continue;
283
283
  const left = child.childForFieldName('left');
284
284
  if (!left) continue;
285
285
  if (left.type === 'instance_variable') {
@@ -263,7 +263,7 @@ function extractScalaParameters(funcNode: TreeSitterNode): SubDeclaration[] {
263
263
  if (!paramList) return params;
264
264
  for (let i = 0; i < paramList.childCount; i++) {
265
265
  const param = paramList.child(i);
266
- if (!param || param.type !== 'parameter') continue;
266
+ if (param?.type !== 'parameter') continue;
267
267
  const nameNode = findChild(param, 'identifier');
268
268
  if (nameNode) {
269
269
  params.push({ name: nameNode.text, kind: 'parameter', line: param.startPosition.row + 1 });
@@ -166,7 +166,7 @@ function extractContractMember(child: TreeSitterNode): SubDeclaration | null {
166
166
  function extractInheritance(node: TreeSitterNode, name: string, ctx: ExtractorOutput): void {
167
167
  for (let i = 0; i < node.childCount; i++) {
168
168
  const inheritance = node.child(i);
169
- if (!inheritance || inheritance.type !== 'inheritance_specifier') continue;
169
+ if (inheritance?.type !== 'inheritance_specifier') continue;
170
170
  for (let j = 0; j < inheritance.childCount; j++) {
171
171
  const child = inheritance.child(j);
172
172
  if (!child) continue;
@@ -94,7 +94,7 @@ function collectSwiftEnumEntries(node: TreeSitterNode): SubDeclaration[] {
94
94
  if (!body) return entries;
95
95
  for (let i = 0; i < body.childCount; i++) {
96
96
  const child = body.child(i);
97
- if (!child || child.type !== 'enum_entry') continue;
97
+ if (child?.type !== 'enum_entry') continue;
98
98
  const entryName = findChild(child, 'simple_identifier');
99
99
  if (entryName) {
100
100
  entries.push({
@@ -114,7 +114,7 @@ function collectSwiftProperties(node: TreeSitterNode): SubDeclaration[] {
114
114
  if (!body) return props;
115
115
  for (let i = 0; i < body.childCount; i++) {
116
116
  const child = body.child(i);
117
- if (!child || child.type !== 'property_declaration') continue;
117
+ if (child?.type !== 'property_declaration') continue;
118
118
  const pattern = findChild(child, 'pattern');
119
119
  if (!pattern) continue;
120
120
  const propName = findChild(pattern, 'simple_identifier');
@@ -136,7 +136,7 @@ function collectSwiftMethods(node: TreeSitterNode, className: string, ctx: Extra
136
136
  if (!body) return;
137
137
  for (let i = 0; i < body.childCount; i++) {
138
138
  const child = body.child(i);
139
- if (!child || child.type !== 'function_declaration') continue;
139
+ if (child?.type !== 'function_declaration') continue;
140
140
  const methName = findChild(child, 'simple_identifier');
141
141
  if (methName) {
142
142
  ctx.definitions.push({
@@ -159,7 +159,7 @@ function collectSwiftInheritance(
159
159
  let first = true;
160
160
  for (let i = 0; i < node.childCount; i++) {
161
161
  const child = node.child(i);
162
- if (!child || child.type !== 'inheritance_specifier') continue;
162
+ if (child?.type !== 'inheritance_specifier') continue;
163
163
  const userType = findChild(child, 'user_type');
164
164
  const typeId = userType ? findChild(userType, 'type_identifier') : null;
165
165
  if (!typeId) continue;
@@ -86,7 +86,7 @@ function extractZigParams(funcNode: TreeSitterNode): SubDeclaration[] {
86
86
 
87
87
  for (let i = 0; i < paramList.childCount; i++) {
88
88
  const param = paramList.child(i);
89
- if (!param || param.type !== 'parameter') continue;
89
+ if (param?.type !== 'parameter') continue;
90
90
  const nameNode = findChild(param, 'identifier');
91
91
  if (nameNode) {
92
92
  params.push({ name: nameNode.text, kind: 'parameter', line: param.startPosition.row + 1 });
@@ -151,7 +151,7 @@ function zigContainerKind(nodeType: string): 'struct' | 'enum' | undefined {
151
151
  function tryHandleZigImportDecl(node: TreeSitterNode, name: string, ctx: ExtractorOutput): boolean {
152
152
  for (let i = 0; i < node.childCount; i++) {
153
153
  const child = node.child(i);
154
- if (!child || child.type !== 'builtin_function') continue;
154
+ if (child?.type !== 'builtin_function') continue;
155
155
  const source = extractZigImportSource(child);
156
156
  if (source) {
157
157
  ctx.imports.push({ source, names: [name], line: node.startPosition.row + 1 });
@@ -175,7 +175,7 @@ function extractZigContainerFields(container: TreeSitterNode): SubDeclaration[]
175
175
  const fields: SubDeclaration[] = [];
176
176
  for (let i = 0; i < container.childCount; i++) {
177
177
  const child = container.child(i);
178
- if (!child || child.type !== 'container_field') continue;
178
+ if (child?.type !== 'container_field') continue;
179
179
  const nameNode = child.childForFieldName('name') || findChild(child, 'identifier');
180
180
  if (nameNode) {
181
181
  fields.push({ name: nameNode.text, kind: 'property', line: child.startPosition.row + 1 });
@@ -191,7 +191,7 @@ function extractZigContainerMethods(
191
191
  ): void {
192
192
  for (let i = 0; i < container.childCount; i++) {
193
193
  const child = container.child(i);
194
- if (!child || child.type !== 'function_declaration') continue;
194
+ if (child?.type !== 'function_declaration') continue;
195
195
  const nameNode = child.childForFieldName('name');
196
196
  if (nameNode) {
197
197
  ctx.definitions.push({
@@ -1,5 +1,5 @@
1
1
  import path from 'node:path';
2
- import { getNodeId, testFilterSQL } from '../db/index.js';
2
+ import { getBuildMeta, getNodeId, setBuildMeta, testFilterSQL } from '../db/index.js';
3
3
  import { debug } from '../infrastructure/logger.js';
4
4
  import { normalizePath } from '../shared/constants.js';
5
5
  import type { BetterSqlite3Database } from '../types.js';
@@ -582,6 +582,111 @@ function buildClassifierInput(
582
582
  }));
583
583
  }
584
584
 
585
+ // ─── Median cache helpers ─────────────────────────────────────────────────────
586
+
587
+ const ROLES_MEDIANS_KEY = 'roles_medians';
588
+
589
+ // Invalidate cached medians when the edge count drifts past this threshold.
590
+ // A 1-file rebuild adds/removes < 100 edges — well within the margin.
591
+ const MEDIAN_INVALIDATION_DELTA = 500;
592
+
593
+ /**
594
+ * Full edge-table GROUP BY scan — O(M). Only runs on cache miss.
595
+ *
596
+ * Joins `nodes` to restrict to the same non-leaf kinds that
597
+ * `classifyNodeRolesFull` uses when computing medians from in-memory rows
598
+ * (excludes 'file', 'directory', 'parameter', 'property'). This keeps the
599
+ * two paths consistent so a cold-cache fallback produces the same distribution
600
+ * as the full-build cached value.
601
+ *
602
+ * Also returns the filtered edge count used for computing the medians so the
603
+ * caller can pass it directly to `writeMedianCache` without a second query.
604
+ */
605
+ function computeGlobalMediansFromEdges(db: BetterSqlite3Database): {
606
+ fanIn: number;
607
+ fanOut: number;
608
+ edgeCount: number;
609
+ } {
610
+ const excludedKinds = `('file', 'directory', 'parameter', 'property')`;
611
+ const fanInRows = db
612
+ .prepare(
613
+ `SELECT COUNT(*) AS cnt FROM edges e
614
+ JOIN nodes t ON e.target_id = t.id
615
+ WHERE e.kind IN ('calls', 'imports-type')
616
+ AND t.kind NOT IN ${excludedKinds}
617
+ GROUP BY e.target_id`,
618
+ )
619
+ .all() as { cnt: number }[];
620
+ const fanOutRows = db
621
+ .prepare(
622
+ `SELECT COUNT(*) AS cnt FROM edges e
623
+ JOIN nodes s ON e.source_id = s.id
624
+ WHERE e.kind = 'calls'
625
+ AND s.kind NOT IN ${excludedKinds}
626
+ GROUP BY e.source_id`,
627
+ )
628
+ .all() as { cnt: number }[];
629
+ const fanInDist = fanInRows.map((r) => r.cnt).sort((a, b) => a - b);
630
+ const fanOutDist = fanOutRows.map((r) => r.cnt).sort((a, b) => a - b);
631
+ // Sum of fanInRows[*].cnt equals the total edge count for the relevant
632
+ // edge subset — no extra COUNT query needed.
633
+ const edgeCount = fanInRows.reduce((acc, r) => acc + r.cnt, 0);
634
+ return { fanIn: median(fanInDist), fanOut: median(fanOutDist), edgeCount };
635
+ }
636
+
637
+ /**
638
+ * Read cached role medians from build_meta. Returns null when absent or stale
639
+ * (edge count moved beyond MEDIAN_INVALIDATION_DELTA from the cached value).
640
+ *
641
+ * The staleness check uses the same edge subset (calls + imports-type) that
642
+ * the medians are derived from, so only changes to the edges that actually
643
+ * influence fan-in/fan-out can evict the cache.
644
+ */
645
+ function readCachedMedians(db: BetterSqlite3Database): { fanIn: number; fanOut: number } | null {
646
+ const raw = getBuildMeta(db, ROLES_MEDIANS_KEY);
647
+ if (!raw) return null;
648
+ try {
649
+ const cached = JSON.parse(raw) as { fanIn: number; fanOut: number; edgeCount: number };
650
+ // Count only the edge kinds that drive median computation — same subset
651
+ // used by computeGlobalMediansFromEdges and classifyNodeRolesFull.
652
+ const currentCount = (
653
+ db
654
+ .prepare(`SELECT COUNT(*) AS cnt FROM edges WHERE kind IN ('calls', 'imports-type')`)
655
+ .get() as { cnt: number }
656
+ ).cnt;
657
+ if (
658
+ Math.abs(currentCount - cached.edgeCount) >
659
+ Math.max(MEDIAN_INVALIDATION_DELTA, cached.edgeCount * 0.1)
660
+ )
661
+ return null;
662
+ return { fanIn: cached.fanIn, fanOut: cached.fanOut };
663
+ } catch {
664
+ return null;
665
+ }
666
+ }
667
+
668
+ /**
669
+ * Persist global role medians + current edge count to build_meta.
670
+ *
671
+ * @param edgeCount - pre-computed calls+imports-type edge count. When provided,
672
+ * the function skips the COUNT query entirely. Pass when the count is already
673
+ * known at the call site (e.g. from `computeGlobalMediansFromEdges`).
674
+ */
675
+ function writeMedianCache(
676
+ db: BetterSqlite3Database,
677
+ medians: { fanIn: number; fanOut: number },
678
+ edgeCount?: number,
679
+ ): void {
680
+ const cnt =
681
+ edgeCount ??
682
+ (
683
+ db
684
+ .prepare(`SELECT COUNT(*) AS cnt FROM edges WHERE kind IN ('calls', 'imports-type')`)
685
+ .get() as { cnt: number }
686
+ ).cnt;
687
+ setBuildMeta(db, { [ROLES_MEDIANS_KEY]: JSON.stringify({ ...medians, edgeCount: cnt }) });
688
+ }
689
+
585
690
  function classifyNodeRolesFull(db: BetterSqlite3Database, emptySummary: RoleSummary): RoleSummary {
586
691
  // Leaf kinds (parameter, property) can never have callers/callees.
587
692
  // Classify them directly as dead-leaf without the expensive fan-in/fan-out JOINs.
@@ -672,10 +777,28 @@ function classifyNodeRolesFull(db: BetterSqlite3Database, emptySummary: RoleSumm
672
777
  prodFanInMap.set(r.target_id, r.cnt);
673
778
  }
674
779
 
675
- // Delegate classification to the pure-logic classifier
780
+ // Delegate classification to the pure-logic classifier.
781
+ // Compute medians from the already-loaded rows (no extra DB round-trip),
782
+ // pass them as overrides to avoid recomputing inside classifyRoles,
783
+ // and cache them for subsequent incremental builds.
676
784
  const activeFiles = buildActiveFilesSet(rows);
677
785
  const classifierInput = buildClassifierInput(rows, exportedIds, prodFanInMap, activeFiles);
678
- const roleMap = classifyRoles(classifierInput);
786
+ const nonZeroFanIn = classifierInput
787
+ .filter((n) => n.fanIn > 0)
788
+ .map((n) => n.fanIn)
789
+ .sort((a, b) => a - b);
790
+ const nonZeroFanOut = classifierInput
791
+ .filter((n) => n.fanOut > 0)
792
+ .map((n) => n.fanOut)
793
+ .sort((a, b) => a - b);
794
+ const globalMedians = { fanIn: median(nonZeroFanIn), fanOut: median(nonZeroFanOut) };
795
+ const roleMap = classifyRoles(classifierInput, globalMedians);
796
+ // Derive the edge count from already-loaded in-memory rows: summing fan_in
797
+ // across all nodes equals COUNT(*) FROM edges WHERE kind IN ('calls','imports-type'),
798
+ // since the full-build query left-joins every matching edge exactly once per target.
799
+ // Passing this avoids an extra COUNT query on the full-build path.
800
+ const inMemoryEdgeCount = rows.reduce((acc, r) => acc + r.fan_in, 0);
801
+ writeMedianCache(db, globalMedians, inMemoryEdgeCount);
679
802
 
680
803
  const { summary, idsByRole } = buildRoleSummary(rows, leafRows, roleMap, emptySummary);
681
804
 
@@ -691,7 +814,9 @@ function classifyNodeRolesFull(db: BetterSqlite3Database, emptySummary: RoleSumm
691
814
  * plus their immediate edge neighbours (callers and callees in other files).
692
815
  *
693
816
  * Uses indexed point lookups for fan-in/fan-out instead of full table scans.
694
- * Global medians are computed from edge distribution (fast GROUP BY on index).
817
+ * Global medians are read from the build_meta cache written by the last full
818
+ * classification; the cache is only recomputed when the edge count drifts
819
+ * beyond MEDIAN_INVALIDATION_DELTA (i.e. large structural changes).
695
820
  * Unchanged files not connected to changed files keep their roles from the
696
821
  * previous build.
697
822
  */
@@ -718,25 +843,20 @@ function classifyNodeRolesIncremental(
718
843
  const allAffectedFiles = [...changedFiles, ...neighbourFiles.map((r) => r.file)];
719
844
  const placeholders = allAffectedFiles.map(() => '?').join(',');
720
845
 
721
- // 1. Compute global medians from edge distribution (fast: scans edge index, no node join)
722
- const fanInDist = (
723
- db
724
- .prepare(
725
- `SELECT COUNT(*) AS cnt FROM edges WHERE kind IN ('calls', 'imports-type') GROUP BY target_id`,
726
- )
727
- .all() as { cnt: number }[]
728
- )
729
- .map((r) => r.cnt)
730
- .sort((a, b) => a - b);
731
- const fanOutDist = (
732
- db
733
- .prepare(`SELECT COUNT(*) AS cnt FROM edges WHERE kind = 'calls' GROUP BY source_id`)
734
- .all() as { cnt: number }[]
735
- )
736
- .map((r) => r.cnt)
737
- .sort((a, b) => a - b);
738
-
739
- const globalMedians = { fanIn: median(fanInDist), fanOut: median(fanOutDist) };
846
+ // 1. Read global medians from cache; fall back to full edge scan only on miss.
847
+ // The median barely moves for a 1-file change, so the cache is almost always
848
+ // valid, eliminating 2× full edge-table GROUP BY queries (~10-15 ms on large graphs).
849
+ const cachedMedians = readCachedMedians(db);
850
+ let globalMedians: { fanIn: number; fanOut: number };
851
+ if (cachedMedians) {
852
+ globalMedians = cachedMedians;
853
+ } else {
854
+ const computed = computeGlobalMediansFromEdges(db);
855
+ // Pass the edgeCount returned by computeGlobalMediansFromEdges so
856
+ // writeMedianCache does not issue a second COUNT query.
857
+ writeMedianCache(db, computed, computed.edgeCount);
858
+ globalMedians = computed;
859
+ }
740
860
 
741
861
  // 2a. Leaf kinds (parameter, property) in affected files — always dead-leaf
742
862
  const leafRows = db
@@ -24,6 +24,7 @@ export const DEFAULTS = {
24
24
  dbPath: '.codegraph/graph.db',
25
25
  driftThreshold: 0.2,
26
26
  smallFilesThreshold: 5,
27
+ typescriptResolver: false,
27
28
  },
28
29
  query: {
29
30
  defaultDepth: 3,
@@ -79,6 +80,20 @@ export const DEFAULTS = {
79
80
  briefImporterDepth: 5,
80
81
  briefHighRiskCallers: 10,
81
82
  briefMediumRiskCallers: 3,
83
+ // TODO(Phase 8.3): wire these into the points-to solver and type-propagation path
84
+ // once config is threaded through to extractSymbols / buildPointsToMap. Currently
85
+ // controlled by hardcoded constants in src/extractors/javascript.ts
86
+ // (MAX_PROPAGATION_DEPTH, PROPAGATION_HOP_PENALTY) and in
87
+ // src/domain/graph/resolver/points-to.ts (MAX_SOLVER_ITERATIONS).
88
+ typePropagationDepth: 3,
89
+ /**
90
+ * Maximum fixed-point iterations for the Phase 8.3 points-to solver.
91
+ * @reserved — currently not wired to either the WASM solver
92
+ * (`MAX_SOLVER_ITERATIONS` in `points-to.ts`) or the native Rust solver
93
+ * (`MAX_SOLVER_ITERATIONS` in `edge_builder.rs`), both of which use the
94
+ * same hardcoded value of 50. See the TODO comment above.
95
+ */
96
+ pointsToMaxIterations: 50,
82
97
  },
83
98
  community: {
84
99
  resolution: 1.0,
@@ -6,11 +6,13 @@
6
6
  * to the existing WASM pipeline.
7
7
  */
8
8
 
9
+ import { existsSync } from 'node:fs';
9
10
  import { createRequire } from 'node:module';
10
11
  import os from 'node:os';
12
+ import { fileURLToPath } from 'node:url';
11
13
  import { EngineError, toErrorMessage } from '../shared/errors.js';
12
14
  import type { NativeAddon } from '../types.js';
13
- import { debug } from './logger.js';
15
+ import { debug, warn } from './logger.js';
14
16
 
15
17
  let _cached: NativeAddon | null | undefined; // undefined = not yet tried, null = failed, NativeAddon = module
16
18
  let _loadError: Error | null = null;
@@ -44,31 +46,107 @@ const PLATFORM_PACKAGES: Record<string, string> = {
44
46
  'win32-x64': '@optave/codegraph-win32-x64-msvc',
45
47
  };
46
48
 
49
+ /**
50
+ * Map of (platform-arch[-libc]) → locally compiled binary filename.
51
+ * Checked before the npm package so that locally compiled Rust changes
52
+ * are picked up immediately in development without publishing a new release.
53
+ */
54
+ const PLATFORM_LOCAL_BINARIES: Record<string, string> = {
55
+ 'linux-x64-gnu': 'codegraph-core.linux-x64-gnu.node',
56
+ 'linux-x64-musl': 'codegraph-core.linux-x64-musl.node',
57
+ 'linux-arm64-gnu': 'codegraph-core.linux-arm64-gnu.node',
58
+ 'linux-arm64-musl': 'codegraph-core.linux-arm64-musl.node',
59
+ 'darwin-arm64': 'codegraph-core.darwin-arm64.node',
60
+ 'darwin-x64': 'codegraph-core.darwin-x64.node',
61
+ 'win32-x64': 'codegraph-core.win32-x64-msvc.node',
62
+ };
63
+
64
+ /** Compute the platform key used to index PLATFORM_PACKAGES / PLATFORM_LOCAL_BINARIES. */
65
+ function resolvePlatformKey(): string {
66
+ const platform = os.platform();
67
+ const arch = os.arch();
68
+ return platform === 'linux' ? `${platform}-${arch}-${detectLibc()}` : `${platform}-${arch}`;
69
+ }
70
+
47
71
  /**
48
72
  * Resolve the platform-specific npm package name for the native addon.
49
73
  * Returns null if the current platform is not supported.
50
74
  */
51
75
  function resolvePlatformPackage(): string | null {
52
- const platform = os.platform();
53
- const arch = os.arch();
54
- const key = platform === 'linux' ? `${platform}-${arch}-${detectLibc()}` : `${platform}-${arch}`;
55
- return PLATFORM_PACKAGES[key] || null;
76
+ return PLATFORM_PACKAGES[resolvePlatformKey()] ?? null;
56
77
  }
57
78
 
58
79
  /**
59
80
  * Try to load the native napi addon.
60
81
  * Returns the module on success, null on failure.
82
+ *
83
+ * Load order:
84
+ * 1. NAPI_RS_NATIVE_LIBRARY_PATH env var (explicit override)
85
+ * 2. locally compiled binary in crates/codegraph-core/ (dev mode — preferred
86
+ * over the npm package so that Rust changes are picked up immediately
87
+ * without publishing a new release)
88
+ * 3. npm platform package (production path)
61
89
  */
62
90
  export function loadNative(): NativeAddon | null {
63
91
  if (_cached !== undefined) return _cached;
64
92
 
93
+ const platformKey = resolvePlatformKey();
94
+
95
+ // 1. Explicit path override — highest priority. Failure is fatal: if the
96
+ // operator set this variable, silently loading a different binary would
97
+ // be harder to diagnose than an explicit error.
98
+ const envPath = process.env.NAPI_RS_NATIVE_LIBRARY_PATH;
99
+ if (envPath) {
100
+ try {
101
+ _cached = _require(envPath) as NativeAddon;
102
+ debug(`loadNative: loaded from NAPI_RS_NATIVE_LIBRARY_PATH: ${envPath}`);
103
+ return _cached;
104
+ } catch (err) {
105
+ _loadError = err as Error;
106
+ warn(
107
+ `loadNative: NAPI_RS_NATIVE_LIBRARY_PATH is set but failed to load "${envPath}": ${toErrorMessage(err as Error)}`,
108
+ );
109
+ _cached = null;
110
+ return null;
111
+ }
112
+ }
113
+
114
+ // 2. Locally compiled dev binary — preferred over npm package so that Rust
115
+ // changes are visible without publishing. Only used when the file exists.
116
+ // If the file exists but fails to load (e.g. stale ABI), we warn and halt
117
+ // rather than silently falling through to the npm package — that would
118
+ // defeat the purpose of this priority order.
119
+ const localFile = PLATFORM_LOCAL_BINARIES[platformKey];
120
+ if (localFile) {
121
+ const localPath = fileURLToPath(
122
+ new URL(`../../crates/codegraph-core/${localFile}`, import.meta.url),
123
+ );
124
+ if (existsSync(localPath)) {
125
+ try {
126
+ _cached = _require(localPath) as NativeAddon;
127
+ debug(`loadNative: loaded local dev binary: ${localPath}`);
128
+ return _cached;
129
+ } catch (err) {
130
+ _loadError = err as Error;
131
+ warn(
132
+ `loadNative: local dev binary exists but failed to load "${localPath}": ${toErrorMessage(err as Error)}`,
133
+ );
134
+ _cached = null;
135
+ return null;
136
+ }
137
+ }
138
+ }
139
+
140
+ // 3. Published npm platform package — production path.
65
141
  const pkg = resolvePlatformPackage();
66
142
  if (pkg) {
67
143
  try {
68
144
  _cached = _require(pkg) as NativeAddon;
145
+ debug(`loadNative: loaded npm package: ${pkg}`);
69
146
  return _cached;
70
147
  } catch (err) {
71
148
  _loadError = err as Error;
149
+ debug(`loadNative: npm package ${pkg} not available: ${toErrorMessage(err as Error)}`);
72
150
  }
73
151
  } else {
74
152
  _loadError = new Error(`Unsupported platform: ${os.platform()}-${os.arch()}`);
@@ -88,6 +166,10 @@ export function isNativeAvailable(): boolean {
88
166
  /**
89
167
  * Read the version from the platform-specific npm package.json.
90
168
  * Returns null if the package is not installed or has no version.
169
+ *
170
+ * Note: always reports the npm package version. When the local dev binary or
171
+ * NAPI_RS_NATIVE_LIBRARY_PATH is loaded instead, this version may not match
172
+ * the running binary.
91
173
  */
92
174
  export function getNativePackageVersion(): string | null {
93
175
  const pkg = resolvePlatformPackage();