openlore 2.0.2 → 2.0.4

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 (145) hide show
  1. package/README.md +32 -2
  2. package/dist/cli/commands/analyze.js +36 -23
  3. package/dist/cli/commands/analyze.js.map +1 -1
  4. package/dist/cli/commands/mcp.d.ts.map +1 -1
  5. package/dist/cli/commands/mcp.js +18 -1
  6. package/dist/cli/commands/mcp.js.map +1 -1
  7. package/dist/cli/export/index.d.ts +10 -0
  8. package/dist/cli/export/index.d.ts.map +1 -0
  9. package/dist/cli/export/index.js +27 -0
  10. package/dist/cli/export/index.js.map +1 -0
  11. package/dist/cli/export/scip.d.ts +22 -0
  12. package/dist/cli/export/scip.d.ts.map +1 -0
  13. package/dist/cli/export/scip.js +95 -0
  14. package/dist/cli/export/scip.js.map +1 -0
  15. package/dist/cli/index.js +4 -0
  16. package/dist/cli/index.js.map +1 -1
  17. package/dist/cli/manifest/detect/events.d.ts +31 -0
  18. package/dist/cli/manifest/detect/events.d.ts.map +1 -0
  19. package/dist/cli/manifest/detect/events.js +22 -0
  20. package/dist/cli/manifest/detect/events.js.map +1 -0
  21. package/dist/cli/manifest/detect/http-routes.d.ts +22 -0
  22. package/dist/cli/manifest/detect/http-routes.d.ts.map +1 -0
  23. package/dist/cli/manifest/detect/http-routes.js +18 -0
  24. package/dist/cli/manifest/detect/http-routes.js.map +1 -0
  25. package/dist/cli/manifest/detect/public-symbols.d.ts +46 -0
  26. package/dist/cli/manifest/detect/public-symbols.d.ts.map +1 -0
  27. package/dist/cli/manifest/detect/public-symbols.js +144 -0
  28. package/dist/cli/manifest/detect/public-symbols.js.map +1 -0
  29. package/dist/cli/manifest/emit.d.ts +103 -0
  30. package/dist/cli/manifest/emit.d.ts.map +1 -0
  31. package/dist/cli/manifest/emit.js +272 -0
  32. package/dist/cli/manifest/emit.js.map +1 -0
  33. package/dist/cli/manifest/index.d.ts +11 -0
  34. package/dist/cli/manifest/index.d.ts.map +1 -0
  35. package/dist/cli/manifest/index.js +31 -0
  36. package/dist/cli/manifest/index.js.map +1 -0
  37. package/dist/cli/manifest/schema-validator.d.ts +18 -0
  38. package/dist/cli/manifest/schema-validator.d.ts.map +1 -0
  39. package/dist/cli/manifest/schema-validator.js +77 -0
  40. package/dist/cli/manifest/schema-validator.js.map +1 -0
  41. package/dist/cli/manifest/validate.d.ts +15 -0
  42. package/dist/cli/manifest/validate.d.ts.map +1 -0
  43. package/dist/cli/manifest/validate.js +51 -0
  44. package/dist/cli/manifest/validate.js.map +1 -0
  45. package/dist/core/analyzer/artifact-generator.d.ts.map +1 -1
  46. package/dist/core/analyzer/artifact-generator.js +37 -3
  47. package/dist/core/analyzer/artifact-generator.js.map +1 -1
  48. package/dist/core/analyzer/call-graph.d.ts +9 -2
  49. package/dist/core/analyzer/call-graph.d.ts.map +1 -1
  50. package/dist/core/analyzer/call-graph.js +534 -1
  51. package/dist/core/analyzer/call-graph.js.map +1 -1
  52. package/dist/core/analyzer/embedding-service.d.ts +2 -0
  53. package/dist/core/analyzer/embedding-service.d.ts.map +1 -1
  54. package/dist/core/analyzer/embedding-service.js +4 -0
  55. package/dist/core/analyzer/embedding-service.js.map +1 -1
  56. package/dist/core/analyzer/fixtures/regression/sample.d.ts +9 -0
  57. package/dist/core/analyzer/fixtures/regression/sample.d.ts.map +1 -0
  58. package/dist/core/analyzer/fixtures/regression/sample.js +26 -0
  59. package/dist/core/analyzer/fixtures/regression/sample.js.map +1 -0
  60. package/dist/core/analyzer/iac/ansible.d.ts +18 -0
  61. package/dist/core/analyzer/iac/ansible.d.ts.map +1 -0
  62. package/dist/core/analyzer/iac/ansible.js +219 -0
  63. package/dist/core/analyzer/iac/ansible.js.map +1 -0
  64. package/dist/core/analyzer/iac/cdk.d.ts +26 -0
  65. package/dist/core/analyzer/iac/cdk.d.ts.map +1 -0
  66. package/dist/core/analyzer/iac/cdk.js +204 -0
  67. package/dist/core/analyzer/iac/cdk.js.map +1 -0
  68. package/dist/core/analyzer/iac/classify-yaml.d.ts +20 -0
  69. package/dist/core/analyzer/iac/classify-yaml.d.ts.map +1 -0
  70. package/dist/core/analyzer/iac/classify-yaml.js +81 -0
  71. package/dist/core/analyzer/iac/classify-yaml.js.map +1 -0
  72. package/dist/core/analyzer/iac/cloudformation.d.ts +15 -0
  73. package/dist/core/analyzer/iac/cloudformation.d.ts.map +1 -0
  74. package/dist/core/analyzer/iac/cloudformation.js +189 -0
  75. package/dist/core/analyzer/iac/cloudformation.js.map +1 -0
  76. package/dist/core/analyzer/iac/helm.d.ts +20 -0
  77. package/dist/core/analyzer/iac/helm.d.ts.map +1 -0
  78. package/dist/core/analyzer/iac/helm.js +229 -0
  79. package/dist/core/analyzer/iac/helm.js.map +1 -0
  80. package/dist/core/analyzer/iac/index.d.ts +23 -0
  81. package/dist/core/analyzer/iac/index.d.ts.map +1 -0
  82. package/dist/core/analyzer/iac/index.js +40 -0
  83. package/dist/core/analyzer/iac/index.js.map +1 -0
  84. package/dist/core/analyzer/iac/kubernetes.d.ts +14 -0
  85. package/dist/core/analyzer/iac/kubernetes.d.ts.map +1 -0
  86. package/dist/core/analyzer/iac/kubernetes.js +226 -0
  87. package/dist/core/analyzer/iac/kubernetes.js.map +1 -0
  88. package/dist/core/analyzer/iac/project.d.ts +16 -0
  89. package/dist/core/analyzer/iac/project.d.ts.map +1 -0
  90. package/dist/core/analyzer/iac/project.js +97 -0
  91. package/dist/core/analyzer/iac/project.js.map +1 -0
  92. package/dist/core/analyzer/iac/pulumi.d.ts +21 -0
  93. package/dist/core/analyzer/iac/pulumi.d.ts.map +1 -0
  94. package/dist/core/analyzer/iac/pulumi.js +190 -0
  95. package/dist/core/analyzer/iac/pulumi.js.map +1 -0
  96. package/dist/core/analyzer/iac/terraform.d.ts +19 -0
  97. package/dist/core/analyzer/iac/terraform.d.ts.map +1 -0
  98. package/dist/core/analyzer/iac/terraform.js +546 -0
  99. package/dist/core/analyzer/iac/terraform.js.map +1 -0
  100. package/dist/core/analyzer/iac/types.d.ts +67 -0
  101. package/dist/core/analyzer/iac/types.d.ts.map +1 -0
  102. package/dist/core/analyzer/iac/types.js +39 -0
  103. package/dist/core/analyzer/iac/types.js.map +1 -0
  104. package/dist/core/analyzer/signature-extractor.d.ts +6 -0
  105. package/dist/core/analyzer/signature-extractor.d.ts.map +1 -1
  106. package/dist/core/analyzer/signature-extractor.js +112 -2
  107. package/dist/core/analyzer/signature-extractor.js.map +1 -1
  108. package/dist/core/analyzer/spec-vector-index.d.ts +9 -2
  109. package/dist/core/analyzer/spec-vector-index.d.ts.map +1 -1
  110. package/dist/core/analyzer/spec-vector-index.js +111 -10
  111. package/dist/core/analyzer/spec-vector-index.js.map +1 -1
  112. package/dist/core/analyzer/vector-index.d.ts +33 -1
  113. package/dist/core/analyzer/vector-index.d.ts.map +1 -1
  114. package/dist/core/analyzer/vector-index.js +99 -10
  115. package/dist/core/analyzer/vector-index.js.map +1 -1
  116. package/dist/core/scip/index.d.ts +51 -0
  117. package/dist/core/scip/index.d.ts.map +1 -0
  118. package/dist/core/scip/index.js +210 -0
  119. package/dist/core/scip/index.js.map +1 -0
  120. package/dist/core/scip/moniker.d.ts +48 -0
  121. package/dist/core/scip/moniker.d.ts.map +1 -0
  122. package/dist/core/scip/moniker.js +109 -0
  123. package/dist/core/scip/moniker.js.map +1 -0
  124. package/dist/core/scip/schema.d.ts +34 -0
  125. package/dist/core/scip/schema.d.ts.map +1 -0
  126. package/dist/core/scip/schema.js +48 -0
  127. package/dist/core/scip/schema.js.map +1 -0
  128. package/dist/core/scip/vendor/scip.proto +910 -0
  129. package/dist/core/services/edge-store.d.ts +6 -0
  130. package/dist/core/services/edge-store.d.ts.map +1 -1
  131. package/dist/core/services/edge-store.js +8 -0
  132. package/dist/core/services/edge-store.js.map +1 -1
  133. package/dist/core/services/mcp-handlers/graph.d.ts.map +1 -1
  134. package/dist/core/services/mcp-handlers/graph.js +12 -0
  135. package/dist/core/services/mcp-handlers/graph.js.map +1 -1
  136. package/dist/core/services/mcp-handlers/orient.js +2 -2
  137. package/dist/core/services/mcp-handlers/orient.js.map +1 -1
  138. package/dist/core/services/mcp-handlers/semantic.d.ts.map +1 -1
  139. package/dist/core/services/mcp-handlers/semantic.js +23 -30
  140. package/dist/core/services/mcp-handlers/semantic.js.map +1 -1
  141. package/dist/core/services/mcp-watcher.d.ts.map +1 -1
  142. package/dist/core/services/mcp-watcher.js +14 -10
  143. package/dist/core/services/mcp-watcher.js.map +1 -1
  144. package/package.json +12 -1
  145. package/schemas/openlore-manifest-v1.json +191 -0
@@ -16,6 +16,8 @@ import Parser from 'tree-sitter';
16
16
  import { FunctionRegistryTrie } from './function-registry-trie.js';
17
17
  import { inferTypesFromSource, resolveViaTypeInference } from './type-inference-engine.js';
18
18
  import { extractAllHttpEdges } from './http-route-parser.js';
19
+ import { buildProjectedIac } from './iac/index.js';
20
+ import { logger } from '../../utils/logger.js';
19
21
  // ============================================================================
20
22
  // CONSTANTS
21
23
  // ============================================================================
@@ -1167,6 +1169,494 @@ async function extractSwiftGraph(filePath, content) {
1167
1169
  return { nodes, rawEdges };
1168
1170
  }
1169
1171
  // ============================================================================
1172
+ // ADDITIONAL GENERAL-PURPOSE LANGUAGES (spec-08)
1173
+ // ============================================================================
1174
+ //
1175
+ // C#, Kotlin, PHP, C, Scala, Dart, Lua, Elixir, Bash. Each follows the existing
1176
+ // extractor pattern (lazy soft-loaded grammar + FN/CALL queries + dispatch).
1177
+ // Grammars are native modules; loaders fail SOFT (graceful degradation): a
1178
+ // missing/ABI-incompatible grammar logs one warning and skips graphing for that
1179
+ // language without aborting analyze or any other language.
1180
+ const _warnedUnavailable = new Set();
1181
+ const _grammarHandleCache = new Map();
1182
+ function warnUnavailable(language, err) {
1183
+ if (!_warnedUnavailable.has(language)) {
1184
+ _warnedUnavailable.add(language);
1185
+ logger.warning(`language ${language} grammar unavailable — files will be indexed for search but not graphed (${err.message})`);
1186
+ }
1187
+ return null;
1188
+ }
1189
+ /** Native tree-sitter loader. Returns a uniform handle, or null when unavailable. */
1190
+ async function loadGrammarSoft(language, importer, pick) {
1191
+ if (_grammarHandleCache.has(language))
1192
+ return _grammarHandleCache.get(language);
1193
+ try {
1194
+ const mod = (await importer());
1195
+ const lang = pick(mod);
1196
+ if (!lang)
1197
+ throw new Error('grammar export resolved to undefined');
1198
+ const parser = new Parser();
1199
+ parser.setLanguage(lang);
1200
+ const handle = {
1201
+ withTree: (content, fn) => {
1202
+ const tree = parser.parse(content);
1203
+ const root = tree.rootNode;
1204
+ const runQuery = (src) => {
1205
+ try {
1206
+ const q = new Parser.Query(lang, src);
1207
+ return q.matches(tree.rootNode);
1208
+ }
1209
+ catch {
1210
+ return [];
1211
+ }
1212
+ };
1213
+ return fn(root, runQuery);
1214
+ },
1215
+ };
1216
+ _grammarHandleCache.set(language, handle);
1217
+ return handle;
1218
+ }
1219
+ catch (err) {
1220
+ _grammarHandleCache.set(language, warnUnavailable(language, err));
1221
+ return null;
1222
+ }
1223
+ }
1224
+ /**
1225
+ * WASM grammar loader via web-tree-sitter (ABI-agnostic, portable). Used for
1226
+ * grammars with no host-ABI-compatible native build (Dart, Lua). Soft-fails.
1227
+ */
1228
+ async function loadWasmGrammarSoft(language, wasmSpecifier) {
1229
+ const cacheKey = `wasm:${language}`;
1230
+ if (_grammarHandleCache.has(cacheKey))
1231
+ return _grammarHandleCache.get(cacheKey);
1232
+ try {
1233
+ const { createRequire } = await import('node:module');
1234
+ const { readFile } = await import('node:fs/promises');
1235
+ const req = createRequire(import.meta.url);
1236
+ const wasmPath = req.resolve(wasmSpecifier);
1237
+ // Load the wasm bytes ourselves and hand web-tree-sitter a Uint8Array, so it
1238
+ // never does its own `require("fs/promises")` (which breaks under ESM/vitest).
1239
+ const wasmBytes = new Uint8Array(await readFile(wasmPath));
1240
+ // CRITICAL: each WASM grammar gets its OWN web-tree-sitter module instance.
1241
+ // web-tree-sitter is a singleton emscripten module with a shared heap; loading
1242
+ // two different grammars into one instance corrupts parsing (a Dart parse
1243
+ // silently breaks subsequent Lua parses). Busting the require cache before each
1244
+ // grammar yields an isolated runtime + heap per grammar, so they never interfere.
1245
+ for (const k of Object.keys(req.cache)) {
1246
+ if (k.includes('web-tree-sitter'))
1247
+ delete req.cache[k];
1248
+ }
1249
+ const TS = req('web-tree-sitter');
1250
+ const ParserCtor = (TS.default ?? TS.Parser ?? TS);
1251
+ if (typeof ParserCtor.init === 'function')
1252
+ await ParserCtor.init();
1253
+ const LanguageNs = (TS.Language ?? ParserCtor.Language);
1254
+ const lang = await LanguageNs.load(wasmBytes);
1255
+ const handle = {
1256
+ withTree: (content, fn) => {
1257
+ // Fresh parser + explicit tree/query disposal: web-tree-sitter holds the
1258
+ // parse tree in WASM heap, which corrupts the next parse if not freed.
1259
+ const p = new ParserCtor();
1260
+ p.setLanguage(lang);
1261
+ const tree = p.parse(content);
1262
+ const queries = [];
1263
+ const runQuery = (src) => {
1264
+ try {
1265
+ const q = lang.query(src);
1266
+ queries.push(q);
1267
+ return q.matches(tree.rootNode);
1268
+ }
1269
+ catch {
1270
+ return [];
1271
+ }
1272
+ };
1273
+ try {
1274
+ return fn(tree.rootNode, runQuery);
1275
+ }
1276
+ finally {
1277
+ for (const q of queries)
1278
+ q.delete?.();
1279
+ tree.delete?.();
1280
+ p.delete?.();
1281
+ }
1282
+ },
1283
+ };
1284
+ _grammarHandleCache.set(cacheKey, handle);
1285
+ return handle;
1286
+ }
1287
+ catch (err) {
1288
+ _grammarHandleCache.set(cacheKey, warnUnavailable(language, err));
1289
+ return null;
1290
+ }
1291
+ }
1292
+ /** Reset loader caches — test-only hook for the graceful-degradation test. */
1293
+ export function __resetGrammarCacheForTests() {
1294
+ _grammarHandleCache.clear();
1295
+ _warnedUnavailable.clear();
1296
+ }
1297
+ const NAME_CHILD_TYPES = new Set(['identifier', 'name', 'type_identifier', 'simple_identifier', 'word']);
1298
+ /** Walk up from a node to the nearest grouping construct; return its declared name. */
1299
+ function enclosingGroupName(node, classTypes) {
1300
+ let cursor = node.parent;
1301
+ while (cursor) {
1302
+ if (classTypes.has(cursor.type)) {
1303
+ const nameNode = cursor.namedChildren.find(c => NAME_CHILD_TYPES.has(c.type))
1304
+ ?? cursor.childForFieldName('name') ?? undefined;
1305
+ if (nameNode)
1306
+ return nameNode.text;
1307
+ }
1308
+ cursor = cursor.parent;
1309
+ }
1310
+ return undefined;
1311
+ }
1312
+ /**
1313
+ * Generic query-driven extractor shared by the structurally-similar languages.
1314
+ * Mirrors the Java extractor's shape; per-language differences are expressed
1315
+ * via the QueryLangSpec rather than copy-pasted bodies.
1316
+ */
1317
+ async function extractByQueries(spec, filePath, content) {
1318
+ const handle = await spec.loader();
1319
+ if (!handle)
1320
+ return { nodes: [], rawEdges: [] };
1321
+ return handle.withTree(content, (_root, runQuery) => {
1322
+ const nodes = [];
1323
+ for (const match of runQuery(spec.fnQuery)) {
1324
+ const nameCapture = match.captures.find(c => c.name === 'fn.name');
1325
+ const nodeCapture = match.captures.find(c => c.name === 'fn.node');
1326
+ if (!nameCapture || !nodeCapture)
1327
+ continue;
1328
+ const name = nameCapture.node.text;
1329
+ const fnNode = nodeCapture.node;
1330
+ const className = (spec.classTypes.size ? enclosingGroupName(fnNode, spec.classTypes) : undefined)
1331
+ ?? spec.extraClassName?.(fnNode);
1332
+ const id = className ? `${filePath}::${className}.${name}` : `${filePath}::${name}`;
1333
+ if (nodes.some(n => n.id === id))
1334
+ continue; // collapse multi-clause/overloads to one node
1335
+ nodes.push({
1336
+ id, name, filePath, className,
1337
+ isAsync: false,
1338
+ language: spec.language,
1339
+ startIndex: fnNode.startIndex,
1340
+ endIndex: fnNode.endIndex,
1341
+ fanIn: 0, fanOut: 0,
1342
+ signature: extractDeclaration(content, fnNode.startIndex, fnNode.endIndex, spec.language),
1343
+ });
1344
+ }
1345
+ const definedNames = new Set(nodes.map(n => n.name));
1346
+ const rawEdges = [];
1347
+ const seen = new Set();
1348
+ for (const match of runQuery(spec.callQuery)) {
1349
+ const nameCapture = match.captures.find(c => c.name === 'call.name');
1350
+ const nodeCapture = match.captures.find(c => c.name === 'call.node');
1351
+ const objectCapture = match.captures.find(c => c.name === 'call.object');
1352
+ if (!nameCapture || !nodeCapture)
1353
+ continue;
1354
+ const calleeName = nameCapture.node.text;
1355
+ if (isIgnoredCallee(calleeName))
1356
+ continue;
1357
+ if (spec.callFilter && !spec.callFilter(calleeName, definedNames))
1358
+ continue;
1359
+ const caller = findEnclosingFunction(nodes, nodeCapture.node.startIndex);
1360
+ if (!caller)
1361
+ continue;
1362
+ const calleeObject = objectCapture?.node.text;
1363
+ const key = `${caller.id}\0${calleeName}\0${calleeObject ?? ''}\0${nodeCapture.node.startIndex}`;
1364
+ if (seen.has(key))
1365
+ continue;
1366
+ seen.add(key);
1367
+ rawEdges.push({ callerId: caller.id, calleeName, line: nodeCapture.node.startPosition.row + 1, calleeObject });
1368
+ }
1369
+ return { nodes, rawEdges };
1370
+ });
1371
+ }
1372
+ // ── C# ──────────────────────────────────────────────────────────────────────
1373
+ const CSHARP_SPEC = {
1374
+ language: 'C#',
1375
+ loader: () => loadGrammarSoft('C#', () => import('tree-sitter-c-sharp'), m => m.default),
1376
+ classTypes: new Set(['class_declaration', 'struct_declaration', 'record_declaration', 'interface_declaration', 'enum_declaration']),
1377
+ fnQuery: `
1378
+ (method_declaration name: (identifier) @fn.name) @fn.node
1379
+ (constructor_declaration name: (identifier) @fn.name) @fn.node
1380
+ (local_function_statement name: (identifier) @fn.name) @fn.node
1381
+ `,
1382
+ // Name-based resolution (matches the codebase's best-effort approach): capture
1383
+ // the callee name only — the object isn't used for resolution in these langs.
1384
+ callQuery: `
1385
+ (invocation_expression function: (member_access_expression name: (identifier) @call.name)) @call.node
1386
+ (invocation_expression function: (identifier) @call.name) @call.node
1387
+ `,
1388
+ };
1389
+ // ── Kotlin ──────────────────────────────────────────────────────────────────
1390
+ const KOTLIN_SPEC = {
1391
+ language: 'Kotlin',
1392
+ loader: () => loadGrammarSoft('Kotlin', () => import('tree-sitter-kotlin'), m => m.default),
1393
+ classTypes: new Set(['class_declaration', 'object_declaration', 'interface_declaration', 'companion_object']),
1394
+ // Extension functions: `fun Foo.bar()` — receiver user_type becomes the className.
1395
+ extraClassName: (fnNode) => {
1396
+ const receiver = fnNode.namedChildren.find(c => c.type === 'user_type');
1397
+ return receiver?.text;
1398
+ },
1399
+ fnQuery: `
1400
+ (function_declaration (simple_identifier) @fn.name) @fn.node
1401
+ `,
1402
+ callQuery: `
1403
+ (call_expression (simple_identifier) @call.name) @call.node
1404
+ (call_expression (navigation_expression (navigation_suffix (simple_identifier) @call.name))) @call.node
1405
+ `,
1406
+ };
1407
+ // ── PHP ─────────────────────────────────────────────────────────────────────
1408
+ const PHP_SPEC = {
1409
+ language: 'PHP',
1410
+ loader: () => loadGrammarSoft('PHP', () => import('tree-sitter-php'), m => m.default.php),
1411
+ classTypes: new Set(['class_declaration', 'trait_declaration', 'interface_declaration', 'enum_declaration']),
1412
+ fnQuery: `
1413
+ (function_definition name: (name) @fn.name) @fn.node
1414
+ (method_declaration name: (name) @fn.name) @fn.node
1415
+ `,
1416
+ callQuery: `
1417
+ (function_call_expression function: (name) @call.name) @call.node
1418
+ (member_call_expression name: (name) @call.name) @call.node
1419
+ (scoped_call_expression name: (name) @call.name) @call.node
1420
+ `,
1421
+ };
1422
+ // ── C ───────────────────────────────────────────────────────────────────────
1423
+ const C_SPEC = {
1424
+ language: 'C',
1425
+ loader: () => loadGrammarSoft('C', () => import('tree-sitter-c'), m => m.default),
1426
+ classTypes: new Set(), // C has no classes — file scope is the implicit grouping
1427
+ fnQuery: `
1428
+ (function_definition declarator: (function_declarator declarator: (identifier) @fn.name)) @fn.node
1429
+ `,
1430
+ callQuery: `
1431
+ (call_expression function: (identifier) @call.name) @call.node
1432
+ `,
1433
+ };
1434
+ // ── Scala ───────────────────────────────────────────────────────────────────
1435
+ const SCALA_SPEC = {
1436
+ language: 'Scala',
1437
+ loader: () => loadGrammarSoft('Scala', () => import('tree-sitter-scala'), m => m.default),
1438
+ classTypes: new Set(['object_definition', 'class_definition', 'trait_definition']),
1439
+ fnQuery: `
1440
+ (function_definition name: (identifier) @fn.name) @fn.node
1441
+ `,
1442
+ callQuery: `
1443
+ (call_expression function: (identifier) @call.name) @call.node
1444
+ (call_expression function: (field_expression field: (identifier) @call.name)) @call.node
1445
+ `,
1446
+ };
1447
+ // ── Lua (via bundled WASM — no ABI-compatible native build for the host) ─────
1448
+ const LUA_SPEC = {
1449
+ language: 'Lua',
1450
+ loader: () => loadWasmGrammarSoft('Lua', 'tree-sitter-wasms/out/tree-sitter-lua.wasm'),
1451
+ classTypes: new Set(),
1452
+ // `function t.f()` / `function t:m()` record the table name in className.
1453
+ extraClassName: (fnNode) => {
1454
+ const nameVar = fnNode.childForFieldName('name');
1455
+ if (nameVar?.type === 'variable')
1456
+ return nameVar.childForFieldName('table')?.text;
1457
+ return undefined;
1458
+ },
1459
+ fnQuery: `
1460
+ (local_function_definition_statement name: (identifier) @fn.name) @fn.node
1461
+ (function_definition_statement name: (identifier) @fn.name) @fn.node
1462
+ (function_definition_statement name: (variable field: (identifier) @fn.name)) @fn.node
1463
+ (function_definition_statement name: (variable method: (identifier) @fn.name)) @fn.node
1464
+ `,
1465
+ callQuery: `
1466
+ (call function: (variable name: (identifier) @call.name)) @call.node
1467
+ (call function: (variable field: (identifier) @call.name)) @call.node
1468
+ (call function: (variable method: (identifier) @call.name)) @call.node
1469
+ `,
1470
+ };
1471
+ // ── Bash ────────────────────────────────────────────────────────────────────
1472
+ const BASH_SPEC = {
1473
+ language: 'Bash',
1474
+ loader: () => loadGrammarSoft('Bash', () => import('tree-sitter-bash'), m => m.default),
1475
+ classTypes: new Set(),
1476
+ // Only edge to project-defined functions, never external binaries (grep/ls/…).
1477
+ callFilter: (calleeName, definedNames) => definedNames.has(calleeName),
1478
+ fnQuery: `
1479
+ (function_definition name: (word) @fn.name) @fn.node
1480
+ `,
1481
+ callQuery: `
1482
+ (command name: (command_name (word) @call.name)) @call.node
1483
+ `,
1484
+ };
1485
+ const QUERY_LANG_SPECS = {
1486
+ 'C#': CSHARP_SPEC, 'Kotlin': KOTLIN_SPEC, 'PHP': PHP_SPEC, 'C': C_SPEC,
1487
+ 'Scala': SCALA_SPEC, 'Lua': LUA_SPEC, 'Bash': BASH_SPEC,
1488
+ };
1489
+ // ── Dart (via portable WASM + web-tree-sitter) ───────────────────────────────
1490
+ //
1491
+ // No ABI-compatible native Dart grammar exists for the pinned host binding, so
1492
+ // Dart loads the portable `tree-sitter-wasms` WASM through web-tree-sitter
1493
+ // (ABI-agnostic, pure JS/WASM, builds on every platform) — each WASM grammar in
1494
+ // its own module instance (see loadWasmGrammarSoft). Dart's grammar places the
1495
+ // `function_body` as a SIBLING of `function_signature` (not a child), so a
1496
+ // generic query extractor would attribute no calls — hence a custom walk that
1497
+ // spans signature+body.
1498
+ const DART_CLASS_TYPES = new Set(['class_definition', 'mixin_declaration', 'extension_declaration', 'enum_declaration']);
1499
+ async function extractDartGraph(filePath, content) {
1500
+ const handle = await loadWasmGrammarSoft('Dart', 'tree-sitter-wasms/out/tree-sitter-dart.wasm');
1501
+ if (!handle)
1502
+ return { nodes: [], rawEdges: [] };
1503
+ return handle.withTree(content, (root) => {
1504
+ const enclosingClass = (node) => {
1505
+ let c = node.parent;
1506
+ while (c) {
1507
+ if (DART_CLASS_TYPES.has(c.type))
1508
+ return c.childForFieldName('name')?.text;
1509
+ c = c.parent;
1510
+ }
1511
+ return undefined;
1512
+ };
1513
+ const nodes = [];
1514
+ const collectFns = (n) => {
1515
+ if (n.type === 'function_signature') {
1516
+ const nameNode = n.childForFieldName('name');
1517
+ if (nameNode) {
1518
+ // Body is a sibling of the signature (or of its method_signature parent).
1519
+ const unit = n.parent && n.parent.type === 'method_signature' ? n.parent : n;
1520
+ const sib = unit.nextNamedSibling;
1521
+ const endIndex = sib && sib.type === 'function_body' ? sib.endIndex : n.endIndex;
1522
+ const className = enclosingClass(n);
1523
+ const id = className ? `${filePath}::${className}.${nameNode.text}` : `${filePath}::${nameNode.text}`;
1524
+ if (!nodes.some(x => x.id === id)) {
1525
+ nodes.push({
1526
+ id, name: nameNode.text, filePath, className, isAsync: false, language: 'Dart',
1527
+ startIndex: n.startIndex, endIndex, fanIn: 0, fanOut: 0,
1528
+ signature: extractDeclaration(content, n.startIndex, n.endIndex, 'Dart'),
1529
+ });
1530
+ }
1531
+ }
1532
+ }
1533
+ for (const c of n.namedChildren)
1534
+ collectFns(c);
1535
+ };
1536
+ collectFns(root);
1537
+ const rawEdges = [];
1538
+ const seen = new Set();
1539
+ const collectCalls = (n) => {
1540
+ if (n.type === 'selector' && n.namedChildren.some(c => c.type === 'argument_part')) {
1541
+ const prev = n.previousNamedSibling;
1542
+ let name;
1543
+ if (prev?.type === 'identifier')
1544
+ name = prev.text;
1545
+ else if (prev?.type === 'selector') {
1546
+ const uas = prev.namedChildren.find(c => c.type === 'unconditional_assignable_selector');
1547
+ name = uas?.namedChildren.find(c => c.type === 'identifier')?.text;
1548
+ }
1549
+ if (name && !isIgnoredCallee(name)) {
1550
+ const caller = findEnclosingFunction(nodes, n.startIndex);
1551
+ if (caller) {
1552
+ const key = `${caller.id}\0${name}\0${n.startIndex}`;
1553
+ if (!seen.has(key)) {
1554
+ seen.add(key);
1555
+ rawEdges.push({ callerId: caller.id, calleeName: name, line: n.startPosition.row + 1 });
1556
+ }
1557
+ }
1558
+ }
1559
+ }
1560
+ for (const c of n.namedChildren)
1561
+ collectCalls(c);
1562
+ };
1563
+ collectCalls(root);
1564
+ return { nodes, rawEdges };
1565
+ });
1566
+ }
1567
+ // ── Elixir (custom walk — everything is a `call` node) ───────────────────────
1568
+ const ELIXIR_DEF_KEYWORDS = new Set(['def', 'defp', 'defmacro', 'defmacrop']);
1569
+ async function extractElixirGraph(filePath, content) {
1570
+ const loaded = await loadGrammarSoft('Elixir', () => import('tree-sitter-elixir'), m => m.default);
1571
+ if (!loaded)
1572
+ return { nodes: [], rawEdges: [] };
1573
+ return loaded.withTree(content, (root) => {
1574
+ const nodes = [];
1575
+ const calls = [];
1576
+ const targetIdent = (call) => {
1577
+ const t = call.childForFieldName('target');
1578
+ return t ?? call.namedChildren[0];
1579
+ };
1580
+ const walk = (node, moduleName) => {
1581
+ if (node.type === 'call') {
1582
+ const target = targetIdent(node);
1583
+ const kw = target?.type === 'identifier' ? target.text : undefined;
1584
+ const args = node.childForFieldName('arguments') ?? node.namedChildren.find(c => c.type === 'arguments');
1585
+ if (kw === 'defmodule') {
1586
+ const aliasNode = args?.namedChildren.find(c => c.type === 'alias');
1587
+ const newModule = aliasNode?.text ?? moduleName;
1588
+ for (const child of node.namedChildren)
1589
+ walk(child, newModule);
1590
+ return;
1591
+ }
1592
+ if (kw && ELIXIR_DEF_KEYWORDS.has(kw)) {
1593
+ // First argument is the function head: an identifier (no args) or a call (with args).
1594
+ const head = args?.namedChildren[0];
1595
+ let fnName;
1596
+ let arity = 0;
1597
+ if (head?.type === 'identifier') {
1598
+ fnName = head.text;
1599
+ }
1600
+ else if (head?.type === 'call') {
1601
+ const ht = head.childForFieldName('target') ?? head.namedChildren[0];
1602
+ fnName = ht?.text;
1603
+ const hargs = head.childForFieldName('arguments') ?? head.namedChildren.find(c => c.type === 'arguments');
1604
+ arity = hargs?.namedChildren.length ?? 0;
1605
+ }
1606
+ if (fnName) {
1607
+ const id = moduleName ? `${filePath}::${moduleName}.${fnName}` : `${filePath}::${fnName}`;
1608
+ const existing = nodes.find(n => n.id === id);
1609
+ if (existing) {
1610
+ existing.signature = `${existing.signature} (+clause)`;
1611
+ }
1612
+ else {
1613
+ nodes.push({
1614
+ id, name: fnName, filePath, className: moduleName, isAsync: false,
1615
+ language: 'Elixir', startIndex: node.startIndex, endIndex: node.endIndex,
1616
+ fanIn: 0, fanOut: 0, signature: `${kw} ${fnName}/${arity}`,
1617
+ });
1618
+ }
1619
+ }
1620
+ // Recurse into the body for nested calls.
1621
+ for (const child of node.namedChildren)
1622
+ walk(child, moduleName);
1623
+ return;
1624
+ }
1625
+ // Otherwise it's a call site: local `fun(...)` or remote `Mod.fun(...)`.
1626
+ if (target?.type === 'identifier' && !ELIXIR_DEF_KEYWORDS.has(target.text)) {
1627
+ calls.push({ name: target.text, pos: node.startIndex, row: node.startPosition.row });
1628
+ }
1629
+ else if (target?.type === 'dot') {
1630
+ // Remote `Mod.fun(...)`: emit the function name only (no receiver), so
1631
+ // name-based resolution can match an in-project function (matching how
1632
+ // the other spec-08 languages resolve member/static calls).
1633
+ const right = target.childForFieldName('right') ?? target.namedChildren[target.namedChildren.length - 1];
1634
+ if (right)
1635
+ calls.push({ name: right.text, pos: node.startIndex, row: node.startPosition.row });
1636
+ }
1637
+ }
1638
+ for (const child of node.namedChildren)
1639
+ walk(child, moduleName);
1640
+ };
1641
+ walk(root, undefined);
1642
+ const rawEdges = [];
1643
+ const seen = new Set();
1644
+ for (const c of calls) {
1645
+ if (isIgnoredCallee(c.name))
1646
+ continue;
1647
+ const caller = findEnclosingFunction(nodes, c.pos);
1648
+ if (!caller)
1649
+ continue;
1650
+ const key = `${caller.id}\0${c.name}\0${c.object ?? ''}\0${c.pos}`;
1651
+ if (seen.has(key))
1652
+ continue;
1653
+ seen.add(key);
1654
+ rawEdges.push({ callerId: caller.id, calleeName: c.name, line: c.row + 1, calleeObject: c.object });
1655
+ }
1656
+ return { nodes, rawEdges };
1657
+ });
1658
+ }
1659
+ // ============================================================================
1170
1660
  // CLASS HIERARCHY EXTRACTION
1171
1661
  // ============================================================================
1172
1662
  /**
@@ -1537,8 +2027,13 @@ export class CallGraphBuilder {
1537
2027
  * @param files Source files with path, content, and language
1538
2028
  * @param layers Optional layer map { layerName: [path prefix, ...] }
1539
2029
  * @param importMap Optional per-file import map (from ImportResolverBridge)
2030
+ * @param resolutionNodes Optional pre-existing nodes used only to seed the
2031
+ * call-resolution trie (not added to the returned nodes/edges). An
2032
+ * incremental subset rebuild passes the full set of known nodes so calls
2033
+ * into files outside the re-parsed subset resolve to their real node
2034
+ * instead of degrading to a synthetic `external::` leaf.
1540
2035
  */
1541
- async build(files, layers, importMap) {
2036
+ async build(files, layers, importMap, resolutionNodes) {
1542
2037
  const allNodes = new Map();
1543
2038
  const allRawEdges = [];
1544
2039
  // Pass 1: Extract nodes and raw edges from each file
@@ -1569,6 +2064,16 @@ export class CallGraphBuilder {
1569
2064
  else if (file.language === 'Swift') {
1570
2065
  result = await extractSwiftGraph(file.path, file.content);
1571
2066
  }
2067
+ else if (file.language === 'Elixir') {
2068
+ result = await extractElixirGraph(file.path, file.content);
2069
+ }
2070
+ else if (file.language === 'Dart') {
2071
+ result = await extractDartGraph(file.path, file.content);
2072
+ }
2073
+ else if (QUERY_LANG_SPECS[file.language]) {
2074
+ // spec-08 additional languages (C#, Kotlin, PHP, C, Scala, Dart, Lua, Bash).
2075
+ result = await extractByQueries(QUERY_LANG_SPECS[file.language], file.path, file.content);
2076
+ }
1572
2077
  else {
1573
2078
  continue;
1574
2079
  }
@@ -1607,6 +2112,15 @@ export class CallGraphBuilder {
1607
2112
  const trie = new FunctionRegistryTrie();
1608
2113
  for (const node of allNodes.values())
1609
2114
  trie.insert(node);
2115
+ // Seed resolution with pre-existing nodes (incremental subset rebuilds) so
2116
+ // cross-file calls outside the re-parsed subset still resolve internally.
2117
+ // These are NOT added to allNodes, so they never appear in the output.
2118
+ if (resolutionNodes) {
2119
+ for (const node of resolutionNodes) {
2120
+ if (!allNodes.has(node.id) && !node.isExternal)
2121
+ trie.insert(node);
2122
+ }
2123
+ }
1610
2124
  // Build per-function-body content slices for type inference (keyed by functionId)
1611
2125
  const fileContents = new Map();
1612
2126
  for (const file of files)
@@ -1750,6 +2264,20 @@ export class CallGraphBuilder {
1750
2264
  catch {
1751
2265
  // HTTP edge extraction is best-effort; don't fail the whole build
1752
2266
  }
2267
+ // Pass 2c: Infrastructure-as-Code projection (spec-07).
2268
+ // IaC resources/references project onto the existing node/edge primitives.
2269
+ let iacClasses = [];
2270
+ try {
2271
+ const iac = buildProjectedIac(files);
2272
+ for (const n of iac.nodes)
2273
+ if (!allNodes.has(n.id))
2274
+ allNodes.set(n.id, n);
2275
+ edges.push(...iac.edges);
2276
+ iacClasses = iac.classes;
2277
+ }
2278
+ catch {
2279
+ // IaC extraction is best-effort; never fail the whole build
2280
+ }
1753
2281
  // Pass 3: Calculate fanIn / fanOut (count unique caller→callee pairs, not call sites)
1754
2282
  const seenPairs = new Set();
1755
2283
  for (const edge of edges) {
@@ -1968,6 +2496,11 @@ export class CallGraphBuilder {
1968
2496
  // Pass 7: Build class hierarchy (inheritance + grouping)
1969
2497
  const relationships = await extractClassRelationships(files);
1970
2498
  const { classes, inheritanceEdges } = buildClassNodes(allNodes, relationships);
2499
+ // Merge IaC module groupings (deduped by id) into the class set.
2500
+ const classIds = new Set(classes.map(c => c.id));
2501
+ for (const c of iacClasses)
2502
+ if (!classIds.has(c.id))
2503
+ classes.push(c);
1971
2504
  return {
1972
2505
  nodes: allNodes,
1973
2506
  edges,