@grafema/util 0.3.0-beta

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 (324) hide show
  1. package/LICENSE +190 -0
  2. package/dist/api/GraphAPI.d.ts +87 -0
  3. package/dist/api/GraphAPI.d.ts.map +1 -0
  4. package/dist/api/GraphAPI.js +212 -0
  5. package/dist/api/GraphAPI.js.map +1 -0
  6. package/dist/api/GuaranteeAPI.d.ts +147 -0
  7. package/dist/api/GuaranteeAPI.d.ts.map +1 -0
  8. package/dist/api/GuaranteeAPI.js +290 -0
  9. package/dist/api/GuaranteeAPI.js.map +1 -0
  10. package/dist/config/ConfigLoader.d.ts +214 -0
  11. package/dist/config/ConfigLoader.d.ts.map +1 -0
  12. package/dist/config/ConfigLoader.js +441 -0
  13. package/dist/config/ConfigLoader.js.map +1 -0
  14. package/dist/config/index.d.ts +6 -0
  15. package/dist/config/index.d.ts.map +1 -0
  16. package/dist/config/index.js +5 -0
  17. package/dist/config/index.js.map +1 -0
  18. package/dist/core/CoverageAnalyzer.d.ts +65 -0
  19. package/dist/core/CoverageAnalyzer.d.ts.map +1 -0
  20. package/dist/core/CoverageAnalyzer.js +199 -0
  21. package/dist/core/CoverageAnalyzer.js.map +1 -0
  22. package/dist/core/FileExplainer.d.ts +101 -0
  23. package/dist/core/FileExplainer.d.ts.map +1 -0
  24. package/dist/core/FileExplainer.js +140 -0
  25. package/dist/core/FileExplainer.js.map +1 -0
  26. package/dist/core/FileOverview.d.ts +124 -0
  27. package/dist/core/FileOverview.d.ts.map +1 -0
  28. package/dist/core/FileOverview.js +279 -0
  29. package/dist/core/FileOverview.js.map +1 -0
  30. package/dist/core/GrafemaUri.d.ts +66 -0
  31. package/dist/core/GrafemaUri.d.ts.map +1 -0
  32. package/dist/core/GrafemaUri.js +191 -0
  33. package/dist/core/GrafemaUri.js.map +1 -0
  34. package/dist/core/GraphBackend.d.ts +158 -0
  35. package/dist/core/GraphBackend.d.ts.map +1 -0
  36. package/dist/core/GraphBackend.js +85 -0
  37. package/dist/core/GraphBackend.js.map +1 -0
  38. package/dist/core/GraphFreshnessChecker.d.ts +33 -0
  39. package/dist/core/GraphFreshnessChecker.d.ts.map +1 -0
  40. package/dist/core/GraphFreshnessChecker.js +104 -0
  41. package/dist/core/GraphFreshnessChecker.js.map +1 -0
  42. package/dist/core/GuaranteeManager.d.ts +254 -0
  43. package/dist/core/GuaranteeManager.d.ts.map +1 -0
  44. package/dist/core/GuaranteeManager.js +447 -0
  45. package/dist/core/GuaranteeManager.js.map +1 -0
  46. package/dist/core/HashUtils.d.ts +24 -0
  47. package/dist/core/HashUtils.d.ts.map +1 -0
  48. package/dist/core/HashUtils.js +46 -0
  49. package/dist/core/HashUtils.js.map +1 -0
  50. package/dist/core/IncrementalReanalyzer.d.ts +33 -0
  51. package/dist/core/IncrementalReanalyzer.d.ts.map +1 -0
  52. package/dist/core/IncrementalReanalyzer.js +67 -0
  53. package/dist/core/IncrementalReanalyzer.js.map +1 -0
  54. package/dist/core/ResourceRegistry.d.ts +17 -0
  55. package/dist/core/ResourceRegistry.d.ts.map +1 -0
  56. package/dist/core/ResourceRegistry.js +32 -0
  57. package/dist/core/ResourceRegistry.js.map +1 -0
  58. package/dist/core/SemanticId.d.ts +159 -0
  59. package/dist/core/SemanticId.d.ts.map +1 -0
  60. package/dist/core/SemanticId.js +291 -0
  61. package/dist/core/SemanticId.js.map +1 -0
  62. package/dist/core/VersionManager.d.ts +166 -0
  63. package/dist/core/VersionManager.d.ts.map +1 -0
  64. package/dist/core/VersionManager.js +239 -0
  65. package/dist/core/VersionManager.js.map +1 -0
  66. package/dist/core/brandNodeInternal.d.ts +14 -0
  67. package/dist/core/brandNodeInternal.d.ts.map +1 -0
  68. package/dist/core/brandNodeInternal.js +4 -0
  69. package/dist/core/brandNodeInternal.js.map +1 -0
  70. package/dist/core/nodes/GuaranteeNode.d.ts +76 -0
  71. package/dist/core/nodes/GuaranteeNode.d.ts.map +1 -0
  72. package/dist/core/nodes/GuaranteeNode.js +118 -0
  73. package/dist/core/nodes/GuaranteeNode.js.map +1 -0
  74. package/dist/core/nodes/IssueNode.d.ts +73 -0
  75. package/dist/core/nodes/IssueNode.d.ts.map +1 -0
  76. package/dist/core/nodes/IssueNode.js +130 -0
  77. package/dist/core/nodes/IssueNode.js.map +1 -0
  78. package/dist/core/nodes/NodeKind.d.ts +104 -0
  79. package/dist/core/nodes/NodeKind.d.ts.map +1 -0
  80. package/dist/core/nodes/NodeKind.js +166 -0
  81. package/dist/core/nodes/NodeKind.js.map +1 -0
  82. package/dist/diagnostics/DiagnosticCollector.d.ts +103 -0
  83. package/dist/diagnostics/DiagnosticCollector.d.ts.map +1 -0
  84. package/dist/diagnostics/DiagnosticCollector.js +133 -0
  85. package/dist/diagnostics/DiagnosticCollector.js.map +1 -0
  86. package/dist/diagnostics/DiagnosticReporter.d.ts +122 -0
  87. package/dist/diagnostics/DiagnosticReporter.d.ts.map +1 -0
  88. package/dist/diagnostics/DiagnosticReporter.js +300 -0
  89. package/dist/diagnostics/DiagnosticReporter.js.map +1 -0
  90. package/dist/diagnostics/DiagnosticWriter.d.ts +31 -0
  91. package/dist/diagnostics/DiagnosticWriter.d.ts.map +1 -0
  92. package/dist/diagnostics/DiagnosticWriter.js +44 -0
  93. package/dist/diagnostics/DiagnosticWriter.js.map +1 -0
  94. package/dist/diagnostics/categories.d.ts +57 -0
  95. package/dist/diagnostics/categories.d.ts.map +1 -0
  96. package/dist/diagnostics/categories.js +71 -0
  97. package/dist/diagnostics/categories.js.map +1 -0
  98. package/dist/diagnostics/index.d.ts +17 -0
  99. package/dist/diagnostics/index.d.ts.map +1 -0
  100. package/dist/diagnostics/index.js +15 -0
  101. package/dist/diagnostics/index.js.map +1 -0
  102. package/dist/errors/GrafemaError.d.ts +200 -0
  103. package/dist/errors/GrafemaError.d.ts.map +1 -0
  104. package/dist/errors/GrafemaError.js +209 -0
  105. package/dist/errors/GrafemaError.js.map +1 -0
  106. package/dist/index.d.ts +75 -0
  107. package/dist/index.d.ts.map +1 -0
  108. package/dist/index.js +76 -0
  109. package/dist/index.js.map +1 -0
  110. package/dist/instructions/index.d.ts +8 -0
  111. package/dist/instructions/index.d.ts.map +1 -0
  112. package/dist/instructions/index.js +20 -0
  113. package/dist/instructions/index.js.map +1 -0
  114. package/dist/instructions/onboarding.md +133 -0
  115. package/dist/knowledge/KnowledgeBase.d.ts +113 -0
  116. package/dist/knowledge/KnowledgeBase.d.ts.map +1 -0
  117. package/dist/knowledge/KnowledgeBase.js +420 -0
  118. package/dist/knowledge/KnowledgeBase.js.map +1 -0
  119. package/dist/knowledge/SemanticAddressResolver.d.ts +59 -0
  120. package/dist/knowledge/SemanticAddressResolver.d.ts.map +1 -0
  121. package/dist/knowledge/SemanticAddressResolver.js +160 -0
  122. package/dist/knowledge/SemanticAddressResolver.js.map +1 -0
  123. package/dist/knowledge/git-ingest.d.ts +58 -0
  124. package/dist/knowledge/git-ingest.d.ts.map +1 -0
  125. package/dist/knowledge/git-ingest.js +301 -0
  126. package/dist/knowledge/git-ingest.js.map +1 -0
  127. package/dist/knowledge/git-queries.d.ts +86 -0
  128. package/dist/knowledge/git-queries.d.ts.map +1 -0
  129. package/dist/knowledge/git-queries.js +177 -0
  130. package/dist/knowledge/git-queries.js.map +1 -0
  131. package/dist/knowledge/index.d.ts +14 -0
  132. package/dist/knowledge/index.d.ts.map +1 -0
  133. package/dist/knowledge/index.js +10 -0
  134. package/dist/knowledge/index.js.map +1 -0
  135. package/dist/knowledge/parser.d.ts +39 -0
  136. package/dist/knowledge/parser.d.ts.map +1 -0
  137. package/dist/knowledge/parser.js +292 -0
  138. package/dist/knowledge/parser.js.map +1 -0
  139. package/dist/knowledge/types.d.ts +133 -0
  140. package/dist/knowledge/types.d.ts.map +1 -0
  141. package/dist/knowledge/types.js +8 -0
  142. package/dist/knowledge/types.js.map +1 -0
  143. package/dist/logging/Logger.d.ts +98 -0
  144. package/dist/logging/Logger.d.ts.map +1 -0
  145. package/dist/logging/Logger.js +274 -0
  146. package/dist/logging/Logger.js.map +1 -0
  147. package/dist/notation/archetypes.d.ts +36 -0
  148. package/dist/notation/archetypes.d.ts.map +1 -0
  149. package/dist/notation/archetypes.js +173 -0
  150. package/dist/notation/archetypes.js.map +1 -0
  151. package/dist/notation/fold.d.ts +25 -0
  152. package/dist/notation/fold.d.ts.map +1 -0
  153. package/dist/notation/fold.js +598 -0
  154. package/dist/notation/fold.js.map +1 -0
  155. package/dist/notation/index.d.ts +18 -0
  156. package/dist/notation/index.d.ts.map +1 -0
  157. package/dist/notation/index.js +16 -0
  158. package/dist/notation/index.js.map +1 -0
  159. package/dist/notation/lodExtractor.d.ts +32 -0
  160. package/dist/notation/lodExtractor.d.ts.map +1 -0
  161. package/dist/notation/lodExtractor.js +149 -0
  162. package/dist/notation/lodExtractor.js.map +1 -0
  163. package/dist/notation/nameShortener.d.ts +22 -0
  164. package/dist/notation/nameShortener.d.ts.map +1 -0
  165. package/dist/notation/nameShortener.js +24 -0
  166. package/dist/notation/nameShortener.js.map +1 -0
  167. package/dist/notation/perspectives.d.ts +11 -0
  168. package/dist/notation/perspectives.d.ts.map +1 -0
  169. package/dist/notation/perspectives.js +16 -0
  170. package/dist/notation/perspectives.js.map +1 -0
  171. package/dist/notation/renderer.d.ts +31 -0
  172. package/dist/notation/renderer.d.ts.map +1 -0
  173. package/dist/notation/renderer.js +315 -0
  174. package/dist/notation/renderer.js.map +1 -0
  175. package/dist/notation/traceRenderer.d.ts +39 -0
  176. package/dist/notation/traceRenderer.d.ts.map +1 -0
  177. package/dist/notation/traceRenderer.js +358 -0
  178. package/dist/notation/traceRenderer.js.map +1 -0
  179. package/dist/notation/types.d.ts +66 -0
  180. package/dist/notation/types.d.ts.map +1 -0
  181. package/dist/notation/types.js +10 -0
  182. package/dist/notation/types.js.map +1 -0
  183. package/dist/queries/NodeContext.d.ts +81 -0
  184. package/dist/queries/NodeContext.d.ts.map +1 -0
  185. package/dist/queries/NodeContext.js +196 -0
  186. package/dist/queries/NodeContext.js.map +1 -0
  187. package/dist/queries/findCallsInFunction.d.ts +62 -0
  188. package/dist/queries/findCallsInFunction.d.ts.map +1 -0
  189. package/dist/queries/findCallsInFunction.js +169 -0
  190. package/dist/queries/findCallsInFunction.js.map +1 -0
  191. package/dist/queries/findContainingFunction.d.ts +57 -0
  192. package/dist/queries/findContainingFunction.d.ts.map +1 -0
  193. package/dist/queries/findContainingFunction.js +91 -0
  194. package/dist/queries/findContainingFunction.js.map +1 -0
  195. package/dist/queries/index.d.ts +18 -0
  196. package/dist/queries/index.d.ts.map +1 -0
  197. package/dist/queries/index.js +14 -0
  198. package/dist/queries/index.js.map +1 -0
  199. package/dist/queries/traceDataflow.d.ts +65 -0
  200. package/dist/queries/traceDataflow.d.ts.map +1 -0
  201. package/dist/queries/traceDataflow.js +754 -0
  202. package/dist/queries/traceDataflow.js.map +1 -0
  203. package/dist/queries/traceValues.d.ts +70 -0
  204. package/dist/queries/traceValues.d.ts.map +1 -0
  205. package/dist/queries/traceValues.js +373 -0
  206. package/dist/queries/traceValues.js.map +1 -0
  207. package/dist/queries/types.d.ts +166 -0
  208. package/dist/queries/types.d.ts.map +1 -0
  209. package/dist/queries/types.js +10 -0
  210. package/dist/queries/types.js.map +1 -0
  211. package/dist/schema/GraphSchemaExtractor.d.ts +53 -0
  212. package/dist/schema/GraphSchemaExtractor.d.ts.map +1 -0
  213. package/dist/schema/GraphSchemaExtractor.js +125 -0
  214. package/dist/schema/GraphSchemaExtractor.js.map +1 -0
  215. package/dist/schema/InterfaceSchemaExtractor.d.ts +73 -0
  216. package/dist/schema/InterfaceSchemaExtractor.d.ts.map +1 -0
  217. package/dist/schema/InterfaceSchemaExtractor.js +113 -0
  218. package/dist/schema/InterfaceSchemaExtractor.js.map +1 -0
  219. package/dist/schema/index.d.ts +5 -0
  220. package/dist/schema/index.d.ts.map +1 -0
  221. package/dist/schema/index.js +3 -0
  222. package/dist/schema/index.js.map +1 -0
  223. package/dist/storage/backends/RFDBServerBackend.d.ts +356 -0
  224. package/dist/storage/backends/RFDBServerBackend.d.ts.map +1 -0
  225. package/dist/storage/backends/RFDBServerBackend.js +748 -0
  226. package/dist/storage/backends/RFDBServerBackend.js.map +1 -0
  227. package/dist/storage/backends/typeValidation.d.ts +47 -0
  228. package/dist/storage/backends/typeValidation.d.ts.map +1 -0
  229. package/dist/storage/backends/typeValidation.js +141 -0
  230. package/dist/storage/backends/typeValidation.js.map +1 -0
  231. package/dist/utils/findRfdbBinary.d.ts +67 -0
  232. package/dist/utils/findRfdbBinary.d.ts.map +1 -0
  233. package/dist/utils/findRfdbBinary.js +261 -0
  234. package/dist/utils/findRfdbBinary.js.map +1 -0
  235. package/dist/utils/lazyDownload.d.ts +43 -0
  236. package/dist/utils/lazyDownload.d.ts.map +1 -0
  237. package/dist/utils/lazyDownload.js +175 -0
  238. package/dist/utils/lazyDownload.js.map +1 -0
  239. package/dist/utils/moduleResolution.d.ts +134 -0
  240. package/dist/utils/moduleResolution.d.ts.map +1 -0
  241. package/dist/utils/moduleResolution.js +189 -0
  242. package/dist/utils/moduleResolution.js.map +1 -0
  243. package/dist/utils/resolveNodeFile.d.ts +13 -0
  244. package/dist/utils/resolveNodeFile.d.ts.map +1 -0
  245. package/dist/utils/resolveNodeFile.js +18 -0
  246. package/dist/utils/resolveNodeFile.js.map +1 -0
  247. package/dist/utils/startRfdbServer.d.ts +63 -0
  248. package/dist/utils/startRfdbServer.d.ts.map +1 -0
  249. package/dist/utils/startRfdbServer.js +142 -0
  250. package/dist/utils/startRfdbServer.js.map +1 -0
  251. package/dist/validation/PathValidator.d.ts +80 -0
  252. package/dist/validation/PathValidator.d.ts.map +1 -0
  253. package/dist/validation/PathValidator.js +252 -0
  254. package/dist/validation/PathValidator.js.map +1 -0
  255. package/dist/version.d.ts +11 -0
  256. package/dist/version.d.ts.map +1 -0
  257. package/dist/version.js +26 -0
  258. package/dist/version.js.map +1 -0
  259. package/package.json +50 -0
  260. package/src/api/GraphAPI.ts +307 -0
  261. package/src/api/GuaranteeAPI.ts +402 -0
  262. package/src/config/ConfigLoader.ts +653 -0
  263. package/src/config/index.ts +13 -0
  264. package/src/core/CoverageAnalyzer.ts +243 -0
  265. package/src/core/FileExplainer.ts +179 -0
  266. package/src/core/FileOverview.ts +397 -0
  267. package/src/core/GrafemaUri.ts +216 -0
  268. package/src/core/GraphBackend.ts +266 -0
  269. package/src/core/GraphFreshnessChecker.ts +145 -0
  270. package/src/core/GuaranteeManager.ts +684 -0
  271. package/src/core/HashUtils.ts +48 -0
  272. package/src/core/IncrementalReanalyzer.ts +106 -0
  273. package/src/core/ResourceRegistry.ts +39 -0
  274. package/src/core/SemanticId.ts +423 -0
  275. package/src/core/VersionManager.ts +405 -0
  276. package/src/core/brandNodeInternal.ts +16 -0
  277. package/src/core/nodes/GuaranteeNode.ts +162 -0
  278. package/src/core/nodes/IssueNode.ts +177 -0
  279. package/src/core/nodes/NodeKind.ts +192 -0
  280. package/src/diagnostics/DiagnosticCollector.ts +170 -0
  281. package/src/diagnostics/DiagnosticReporter.ts +395 -0
  282. package/src/diagnostics/DiagnosticWriter.ts +50 -0
  283. package/src/diagnostics/categories.ts +104 -0
  284. package/src/diagnostics/index.ts +30 -0
  285. package/src/errors/GrafemaError.ts +297 -0
  286. package/src/index.ts +261 -0
  287. package/src/instructions/index.ts +21 -0
  288. package/src/instructions/onboarding.md +133 -0
  289. package/src/knowledge/KnowledgeBase.ts +486 -0
  290. package/src/knowledge/SemanticAddressResolver.ts +191 -0
  291. package/src/knowledge/git-ingest.ts +402 -0
  292. package/src/knowledge/git-queries.ts +269 -0
  293. package/src/knowledge/index.ts +29 -0
  294. package/src/knowledge/parser.ts +294 -0
  295. package/src/knowledge/types.ts +146 -0
  296. package/src/logging/Logger.ts +303 -0
  297. package/src/notation/archetypes.ts +189 -0
  298. package/src/notation/fold.ts +696 -0
  299. package/src/notation/index.ts +27 -0
  300. package/src/notation/lodExtractor.ts +177 -0
  301. package/src/notation/nameShortener.ts +24 -0
  302. package/src/notation/perspectives.ts +18 -0
  303. package/src/notation/renderer.ts +394 -0
  304. package/src/notation/traceRenderer.ts +458 -0
  305. package/src/notation/types.ts +79 -0
  306. package/src/queries/NodeContext.ts +280 -0
  307. package/src/queries/findCallsInFunction.ts +249 -0
  308. package/src/queries/findContainingFunction.ts +124 -0
  309. package/src/queries/index.ts +44 -0
  310. package/src/queries/traceDataflow.ts +838 -0
  311. package/src/queries/traceValues.ts +531 -0
  312. package/src/queries/types.ts +191 -0
  313. package/src/schema/GraphSchemaExtractor.ts +177 -0
  314. package/src/schema/InterfaceSchemaExtractor.ts +173 -0
  315. package/src/schema/index.ts +5 -0
  316. package/src/storage/backends/RFDBServerBackend.ts +895 -0
  317. package/src/storage/backends/typeValidation.ts +154 -0
  318. package/src/utils/findRfdbBinary.ts +288 -0
  319. package/src/utils/lazyDownload.ts +206 -0
  320. package/src/utils/moduleResolution.ts +271 -0
  321. package/src/utils/resolveNodeFile.ts +18 -0
  322. package/src/utils/startRfdbServer.ts +197 -0
  323. package/src/validation/PathValidator.ts +334 -0
  324. package/src/version.ts +28 -0
@@ -0,0 +1,838 @@
1
+ /**
2
+ * Dataflow BFS Tracing
3
+ *
4
+ * Full BFS-based dataflow analysis with 100% reachability.
5
+ * Forward: 8 heuristics for complete data flow propagation.
6
+ * Backward: structural descent + PA matching + THROWS propagation.
7
+ *
8
+ * Shared by MCP handler and CLI trace command.
9
+ *
10
+ * @module queries/traceDataflow
11
+ */
12
+
13
+ // === PUBLIC TYPES ===
14
+
15
+ /** Minimal node interface for dataflow tracing. */
16
+ export interface DataflowNode {
17
+ id: string;
18
+ type: string;
19
+ name?: string;
20
+ file?: string;
21
+ line?: number;
22
+ [key: string]: unknown;
23
+ }
24
+
25
+ /** Edge record for dataflow tracing. */
26
+ export interface DataflowEdge {
27
+ src: string;
28
+ dst: string;
29
+ type: string;
30
+ index?: number;
31
+ metadata?: Record<string, unknown>;
32
+ }
33
+
34
+ /** Backend interface for dataflow tracing — compatible with RFDBServerBackend and MCP GraphBackend. */
35
+ export interface DataflowBackend {
36
+ getNode(id: string): Promise<DataflowNode | null>;
37
+ queryNodes(filter: Record<string, unknown>): AsyncIterable<DataflowNode>;
38
+ getOutgoingEdges(id: string, types?: string[] | null): Promise<DataflowEdge[]>;
39
+ getIncomingEdges(id: string, types?: string[] | null): Promise<DataflowEdge[]>;
40
+ }
41
+
42
+ export interface TraceDataflowOptions {
43
+ direction?: 'forward' | 'backward' | 'both';
44
+ maxDepth?: number;
45
+ limit?: number;
46
+ }
47
+
48
+ export interface TraceDataflowResult {
49
+ direction: 'forward' | 'backward';
50
+ startNode: DataflowNode;
51
+ reached: DataflowNode[];
52
+ totalReached: number;
53
+ }
54
+
55
+ // === CONSTANTS ===
56
+
57
+ const MUTATION_METHODS = new Set([
58
+ 'push', 'unshift', 'splice', 'set', 'add', 'append', 'insert', 'enqueue', 'prepend',
59
+ ]);
60
+
61
+ const STRUCTURAL_EDGE_TYPES = ['HAS_PROPERTY', 'HAS_ELEMENT', 'HAS_CONSEQUENT', 'HAS_ALTERNATE'];
62
+
63
+ // === INTERNAL TYPES ===
64
+
65
+ interface ReceiverChain {
66
+ base: string;
67
+ path: string;
68
+ }
69
+
70
+ interface PAEntry {
71
+ id: string;
72
+ base: string;
73
+ path: string;
74
+ }
75
+
76
+ // === SHARED HELPERS ===
77
+
78
+ /** Resolve a node ID through REFERENCE → READS_FROM to reach the declaration. */
79
+ async function resolveRef(db: DataflowBackend, nodeId: string): Promise<string | null> {
80
+ const node = await db.getNode(nodeId);
81
+ if (!node) return null;
82
+ if (node.type === 'REFERENCE') {
83
+ const edges = await db.getOutgoingEdges(nodeId, ['READS_FROM']);
84
+ return edges.length > 0 ? edges[0].dst : null;
85
+ }
86
+ return nodeId;
87
+ }
88
+
89
+ /** Extract .index from edge metadata (PASSES_ARGUMENT/RECEIVES_ARGUMENT). */
90
+ function edgeIndex(edge: DataflowEdge): number | undefined {
91
+ if (edge.metadata && typeof edge.metadata.index === 'number') return edge.metadata.index;
92
+ if (typeof edge.index === 'number') return edge.index;
93
+ return undefined;
94
+ }
95
+
96
+ /** Check if two argument-index values match (undefined = wildcard). */
97
+ function indexMatch(paIdx: number | undefined, raIdx: number | undefined): boolean {
98
+ if (paIdx === undefined || raIdx === undefined) return true;
99
+ return paIdx === raIdx;
100
+ }
101
+
102
+ /** Resolve a PROPERTY_ACCESS receiver chain: walk READS_FROM to find base var + dot-path. */
103
+ async function resolveReceiverChain(db: DataflowBackend, paId: string): Promise<ReceiverChain | null> {
104
+ const pathParts: string[] = [];
105
+ let cur: string | null = paId;
106
+ const seen = new Set<string>();
107
+
108
+ while (cur && !seen.has(cur)) {
109
+ seen.add(cur);
110
+ const rf = await db.getOutgoingEdges(cur, ['READS_FROM']);
111
+ if (!rf.length) break;
112
+
113
+ const t = await db.getNode(rf[0].dst);
114
+ if (!t) break;
115
+ cur = rf[0].dst;
116
+
117
+ if (t.type === 'PROPERTY_ACCESS') {
118
+ if (t.name) pathParts.push(t.name);
119
+ continue;
120
+ }
121
+
122
+ if (t.type === 'REFERENCE') {
123
+ const re = await db.getOutgoingEdges(t.id, ['READS_FROM']);
124
+ if (re.length) {
125
+ const r = await db.getNode(re[0].dst);
126
+ if (r && (r.type === 'CONSTANT' || r.type === 'VARIABLE')) {
127
+ return { base: r.id, path: pathParts.join('.') };
128
+ }
129
+ }
130
+ return null;
131
+ }
132
+
133
+ if (t.type === 'CONSTANT' || t.type === 'VARIABLE') {
134
+ return { base: t.id, path: pathParts.join('.') };
135
+ }
136
+
137
+ break;
138
+ }
139
+
140
+ return null;
141
+ }
142
+
143
+ /** Resolve dynamic callee: follow ASSIGNED_FROM/READS_FROM chains to find the actual FUNCTION. */
144
+ async function resolveDynamicCallee(db: DataflowBackend, nodeId: string): Promise<string | null> {
145
+ const seen = new Set<string>();
146
+ let cur: string | null = nodeId;
147
+
148
+ while (cur && !seen.has(cur)) {
149
+ seen.add(cur);
150
+ const n = await db.getNode(cur);
151
+ if (!n) return null;
152
+
153
+ if (n.type === 'FUNCTION') return cur;
154
+
155
+ if (n.type === 'REFERENCE') {
156
+ const rf = await db.getOutgoingEdges(cur, ['READS_FROM']);
157
+ if (rf.length) { cur = rf[0].dst; continue; }
158
+ return null;
159
+ }
160
+
161
+ if (n.type === 'PARAMETER' || n.type === 'VARIABLE' || n.type === 'CONSTANT') {
162
+ const afs = await db.getOutgoingEdges(cur, ['ASSIGNED_FROM']);
163
+ for (const af of afs) {
164
+ const afN = await db.getNode(af.dst);
165
+ if (afN?.type === 'FUNCTION') return af.dst;
166
+ if (afN?.type === 'REFERENCE') {
167
+ const result = await resolveDynamicCallee(db, af.dst);
168
+ if (result) return result;
169
+ }
170
+ }
171
+ if (n.type === 'PARAMETER') {
172
+ const raIn = await db.getIncomingEdges(cur, ['RECEIVES_ARGUMENT']);
173
+ for (const ra of raIn) {
174
+ const fnId = ra.src;
175
+ const raIdx = edgeIndex(ra);
176
+ for (const ci of await db.getIncomingEdges(fnId, ['CALLS'])) {
177
+ const pas = await db.getOutgoingEdges(ci.src, ['PASSES_ARGUMENT']);
178
+ for (const pa of pas) {
179
+ if (!indexMatch(raIdx, edgeIndex(pa))) continue;
180
+ const paN = await db.getNode(pa.dst);
181
+ if (paN?.type === 'FUNCTION') return pa.dst;
182
+ if (paN?.type === 'REFERENCE') {
183
+ const result = await resolveDynamicCallee(db, pa.dst);
184
+ if (result) return result;
185
+ }
186
+ }
187
+ }
188
+ }
189
+ }
190
+ return null;
191
+ }
192
+
193
+ if (n.type === 'PROPERTY_ACCESS') {
194
+ const rf = await db.getOutgoingEdges(cur, ['READS_FROM']);
195
+ if (rf.length) { cur = rf[0].dst; continue; }
196
+ return null;
197
+ }
198
+
199
+ return null;
200
+ }
201
+
202
+ return null;
203
+ }
204
+
205
+ // === FORWARD BFS ===
206
+
207
+ /**
208
+ * Forward BFS dataflow trace.
209
+ * Starting from a declaration node, finds all nodes where data flows TO.
210
+ * Returns all reached nodes (including the start node at index 0).
211
+ */
212
+ export async function traceForwardBFS(
213
+ db: DataflowBackend,
214
+ startId: string,
215
+ maxIterations: number,
216
+ ): Promise<DataflowNode[]> {
217
+ const visited = new Set<string>();
218
+ const queue: string[] = [];
219
+ const reachedNodes: DataflowNode[] = [];
220
+
221
+ function enq(id: string | null): void {
222
+ if (id && !visited.has(id)) {
223
+ visited.add(id);
224
+ queue.push(id);
225
+ }
226
+ }
227
+
228
+ // Guard sets for helper functions
229
+ const processedCalls = new Set<string>();
230
+ const processedFns = new Set<string>();
231
+ const climbProcessed = new Set<string>();
232
+
233
+ // Lazy indexes
234
+ let paReadByName: Map<string, PAEntry[]> | null = null;
235
+ let catchParamIds: string[] | null = null;
236
+ /** Lazy index: "file::receiverName" → CALL node IDs for method calls on that receiver. */
237
+ let callsByReceiver: Map<string, string[]> | null = null;
238
+
239
+ async function getPAReadByName(): Promise<Map<string, PAEntry[]>> {
240
+ if (paReadByName) return paReadByName;
241
+ paReadByName = new Map();
242
+ for await (const n of db.queryNodes({ type: 'PROPERTY_ACCESS' })) {
243
+ const wt = await db.getOutgoingEdges(n.id, ['WRITES_TO']);
244
+ if (wt.length === 0 && n.name) {
245
+ const chain = await resolveReceiverChain(db, n.id);
246
+ if (chain) {
247
+ let list = paReadByName.get(n.name);
248
+ if (!list) { list = []; paReadByName.set(n.name, list); }
249
+ list.push({ id: n.id, base: chain.base, path: chain.path });
250
+ }
251
+ }
252
+ }
253
+ return paReadByName;
254
+ }
255
+
256
+ async function getCatchParams(): Promise<string[]> {
257
+ if (catchParamIds) return catchParamIds;
258
+ catchParamIds = [];
259
+ for await (const p of db.queryNodes({ type: 'PARAMETER' })) {
260
+ const decls = await db.getIncomingEdges(p.id, ['DECLARES']);
261
+ for (const d of decls) {
262
+ const scope = await db.getNode(d.src);
263
+ if (scope?.name === 'catch') { catchParamIds.push(p.id); break; }
264
+ }
265
+ }
266
+ return catchParamIds;
267
+ }
268
+
269
+ /**
270
+ * Build index of CALL nodes keyed by "file::receiverName".
271
+ * Used to find method calls on a declaration when CALL→DERIVED_FROM→PA→READS_FROM→REF
272
+ * edges are missing. CALL names encode the receiver: "db.getNode" → receiver "db".
273
+ */
274
+ async function getCallsByReceiver(): Promise<Map<string, string[]>> {
275
+ if (callsByReceiver) return callsByReceiver;
276
+ callsByReceiver = new Map();
277
+ for await (const c of db.queryNodes({ type: 'CALL' })) {
278
+ if (c.name?.includes('.') && c.file) {
279
+ const dotIdx = c.name.indexOf('.');
280
+ const receiver = c.name.substring(0, dotIdx);
281
+ // Skip computed receivers like "<obj>.method"
282
+ if (receiver.startsWith('<')) continue;
283
+ const key = `${c.file}::${receiver}`;
284
+ let list = callsByReceiver.get(key);
285
+ if (!list) { list = []; callsByReceiver.set(key, list); }
286
+ list.push(c.id);
287
+ }
288
+ }
289
+ return callsByReceiver;
290
+ }
291
+
292
+ // --- Helper: enqueue call result consumers ---
293
+ async function enqueueCallConsumers(callId: string): Promise<void> {
294
+ if (processedCalls.has(callId)) return;
295
+ processedCalls.add(callId);
296
+
297
+ for (const af of await db.getIncomingEdges(callId, ['ASSIGNED_FROM'])) enq(af.src);
298
+
299
+ // Structural climb
300
+ for (const se of await db.getIncomingEdges(callId, [...STRUCTURAL_EDGE_TYPES])) {
301
+ await enqueueClimb(se.src, 3);
302
+ }
303
+
304
+ // CALL result passed as arg to another CALL
305
+ for (const pa of await db.getIncomingEdges(callId, ['PASSES_ARGUMENT'])) {
306
+ for (const ce of await db.getOutgoingEdges(pa.src, ['CALLS'])) {
307
+ for (const ra of await db.getOutgoingEdges(ce.dst, ['RECEIVES_ARGUMENT'])) enq(ra.dst);
308
+ }
309
+ // Mutation on call result
310
+ for (const df of await db.getOutgoingEdges(pa.src, ['DERIVED_FROM'])) {
311
+ const dfN = await db.getNode(df.dst);
312
+ if (dfN?.type === 'PROPERTY_ACCESS' && dfN.name && MUTATION_METHODS.has(dfN.name)) {
313
+ for (const rf of await db.getOutgoingEdges(df.dst, ['READS_FROM'])) {
314
+ enq(await resolveRef(db, rf.dst));
315
+ }
316
+ }
317
+ }
318
+ // Outer call result
319
+ await enqueueCallConsumers(pa.src);
320
+ }
321
+
322
+ // Chained calls: another CALL uses this as callee (fn()())
323
+ for (const df of await db.getIncomingEdges(callId, ['DERIVED_FROM'])) {
324
+ const dfN = await db.getNode(df.src);
325
+ if (dfN?.type === 'CALL') await enqueueCallConsumers(df.src);
326
+ }
327
+
328
+ // CALL result returned by FUNCTION
329
+ for (const ret of await db.getIncomingEdges(callId, ['RETURNS'])) {
330
+ await enqueueFnCallers(ret.src);
331
+ }
332
+
333
+ // CALL result read via PA (call().value)
334
+ for (const rf of await db.getIncomingEdges(callId, ['READS_FROM'])) {
335
+ const rfN = await db.getNode(rf.src);
336
+ if (rfN?.type === 'PROPERTY_ACCESS') enq(rf.src);
337
+ }
338
+
339
+ // Dynamic callee resolution
340
+ const callsEdges = await db.getOutgoingEdges(callId, ['CALLS']);
341
+ let hasStaticCallee = false;
342
+ for (const ce of callsEdges) {
343
+ const ceN = await db.getNode(ce.dst);
344
+ if (ceN?.type === 'FUNCTION') { hasStaticCallee = true; break; }
345
+ }
346
+ if (!hasStaticCallee) {
347
+ for (const df of await db.getOutgoingEdges(callId, ['DERIVED_FROM'])) {
348
+ const resolved = await resolveDynamicCallee(db, df.dst);
349
+ if (resolved) {
350
+ for (const ra of await db.getOutgoingEdges(resolved, ['RECEIVES_ARGUMENT'])) enq(ra.dst);
351
+ }
352
+ }
353
+ for (const ce of callsEdges) {
354
+ const ceN = await db.getNode(ce.dst);
355
+ if (ceN?.type === 'PARAMETER') {
356
+ const resolved = await resolveDynamicCallee(db, ce.dst);
357
+ if (resolved) {
358
+ for (const ra of await db.getOutgoingEdges(resolved, ['RECEIVES_ARGUMENT'])) enq(ra.dst);
359
+ }
360
+ }
361
+ }
362
+ }
363
+
364
+ // Callback injection: if CALL passes a FUNCTION as argument, trace into callback params
365
+ for (const pa of await db.getOutgoingEdges(callId, ['PASSES_ARGUMENT'])) {
366
+ const paN = await db.getNode(pa.dst);
367
+ if (paN?.type === 'FUNCTION') {
368
+ for (const ra of await db.getOutgoingEdges(pa.dst, ['RECEIVES_ARGUMENT'])) enq(ra.dst);
369
+ }
370
+ }
371
+ }
372
+
373
+ // --- Helper: enqueue callers of a function ---
374
+ async function enqueueFnCallers(fnId: string): Promise<void> {
375
+ if (processedFns.has(fnId)) return;
376
+ processedFns.add(fnId);
377
+
378
+ for (const ci of await db.getIncomingEdges(fnId, ['CALLS'])) {
379
+ await enqueueCallConsumers(ci.src);
380
+ }
381
+ for (const fpa of await db.getIncomingEdges(fnId, ['PASSES_ARGUMENT'])) {
382
+ await enqueueCallConsumers(fpa.src);
383
+ }
384
+ for (const faf of await db.getIncomingEdges(fnId, ['ASSIGNED_FROM'])) {
385
+ for (const vc of await db.getIncomingEdges(faf.src, ['CALLS'])) {
386
+ await enqueueCallConsumers(vc.src);
387
+ }
388
+ }
389
+ for (const df of await db.getIncomingEdges(fnId, ['DERIVED_FROM'])) {
390
+ const dfN = await db.getNode(df.src);
391
+ if (dfN?.type === 'CALL') await enqueueCallConsumers(df.src);
392
+ }
393
+ }
394
+
395
+ // --- Helper: climb structural containers ---
396
+ async function enqueueClimb(nodeId: string, maxClimb: number): Promise<void> {
397
+ if (maxClimb <= 0 || climbProcessed.has(nodeId)) return;
398
+ climbProcessed.add(nodeId);
399
+
400
+ enq(nodeId);
401
+
402
+ for (const af of await db.getIncomingEdges(nodeId, ['ASSIGNED_FROM'])) enq(af.src);
403
+ for (const ret of await db.getIncomingEdges(nodeId, ['RETURNS'])) await enqueueFnCallers(ret.src);
404
+ for (const se of await db.getIncomingEdges(nodeId, [...STRUCTURAL_EDGE_TYPES, 'PASSES_ARGUMENT'])) {
405
+ await enqueueClimb(se.src, maxClimb - 1);
406
+ }
407
+ }
408
+
409
+ // --- Helper: follow PASSES_ARGUMENT to callee params + mutation + call result ---
410
+ async function followPassesArgument(pa: DataflowEdge): Promise<void> {
411
+ const callId = pa.src;
412
+ const paIdx = edgeIndex(pa);
413
+
414
+ // A. To params (with index matching)
415
+ for (const ce of await db.getOutgoingEdges(callId, ['CALLS'])) {
416
+ const raEdges = await db.getOutgoingEdges(ce.dst, ['RECEIVES_ARGUMENT']);
417
+ for (const ra of raEdges) {
418
+ if (!indexMatch(paIdx, edgeIndex(ra))) continue;
419
+ enq(ra.dst);
420
+ }
421
+ }
422
+
423
+ // B. Mutation detection
424
+ for (const df of await db.getOutgoingEdges(callId, ['DERIVED_FROM'])) {
425
+ const dfN = await db.getNode(df.dst);
426
+ if (dfN?.type === 'PROPERTY_ACCESS' && dfN.name && MUTATION_METHODS.has(dfN.name)) {
427
+ for (const rf of await db.getOutgoingEdges(df.dst, ['READS_FROM'])) {
428
+ enq(await resolveRef(db, rf.dst));
429
+ }
430
+ }
431
+ }
432
+
433
+ // C. Call result consumers
434
+ await enqueueCallConsumers(callId);
435
+ }
436
+
437
+ // === Main BFS loop ===
438
+ enq(startId);
439
+ let iterations = 0;
440
+
441
+ while (queue.length > 0) {
442
+ const declId = queue.shift()!;
443
+ iterations++;
444
+ if (iterations > maxIterations) break;
445
+
446
+ const node = await db.getNode(declId);
447
+ if (!node) continue;
448
+
449
+ reachedNodes.push(node);
450
+
451
+ // --- 1. Follow refs that read this declaration ---
452
+ const refsToDecl = await db.getIncomingEdges(declId, ['READS_FROM']);
453
+
454
+ for (const refEdge of refsToDecl) {
455
+ const refId = refEdge.src;
456
+
457
+ // 1a. ASSIGNED_FROM incoming on ref
458
+ for (const af of await db.getIncomingEdges(refId, ['ASSIGNED_FROM'])) enq(af.src);
459
+
460
+ // 1b. WRITES_TO incoming on ref
461
+ for (const wt of await db.getIncomingEdges(refId, ['WRITES_TO'])) {
462
+ enq(await resolveRef(db, wt.src));
463
+ }
464
+
465
+ // 1c. PASSES_ARGUMENT incoming on ref
466
+ for (const pa of await db.getIncomingEdges(refId, ['PASSES_ARGUMENT'])) {
467
+ await followPassesArgument(pa);
468
+ }
469
+
470
+ // 1d. Structural climb
471
+ for (const se of await db.getIncomingEdges(refId, [...STRUCTURAL_EDGE_TYPES])) {
472
+ await enqueueClimb(se.src, 4);
473
+ }
474
+
475
+ // 1e. DERIVED_FROM incoming on ref: EXPRESSION/CALL uses this ref
476
+ for (const df of await db.getIncomingEdges(refId, ['DERIVED_FROM'])) {
477
+ const dfN = await db.getNode(df.src);
478
+ if (dfN) {
479
+ if (dfN.type === 'EXPRESSION') enq(df.src);
480
+ else if (dfN.type === 'CALL') await enqueueCallConsumers(df.src);
481
+ }
482
+ }
483
+
484
+ // 1f. RETURNS / YIELDS — enqueue function callers + function itself
485
+ for (const ret of await db.getIncomingEdges(refId, ['RETURNS'])) {
486
+ await enqueueFnCallers(ret.src);
487
+ enq(ret.src);
488
+ }
489
+ for (const y of await db.getIncomingEdges(refId, ['YIELDS'])) {
490
+ await enqueueFnCallers(y.src);
491
+ enq(y.src);
492
+ }
493
+
494
+ // 1g. THROWS → all catch PARAMETERs
495
+ const throwsEdges = await db.getIncomingEdges(refId, ['THROWS']);
496
+ if (throwsEdges.length > 0) {
497
+ const cps = await getCatchParams();
498
+ for (const cpId of cps) enq(cpId);
499
+ }
500
+
501
+ // 1h. ITERATES_OVER incoming on ref
502
+ for (const io of await db.getIncomingEdges(refId, ['ITERATES_OVER'])) enq(io.src);
503
+
504
+ // 1i. PA reads from ref (receiver chain)
505
+ for (const paRead of await db.getIncomingEdges(refId, ['READS_FROM'])) {
506
+ const paN = await db.getNode(paRead.src);
507
+ if (paN?.type === 'PROPERTY_ACCESS') enq(paRead.src);
508
+ }
509
+
510
+ // 1j. DERIVED_FROM incoming (CALL consumers)
511
+ for (const df of await db.getIncomingEdges(refId, ['DERIVED_FROM'])) {
512
+ const dfN = await db.getNode(df.src);
513
+ if (dfN?.type === 'CALL') await enqueueCallConsumers(df.src);
514
+ }
515
+ }
516
+
517
+ // --- 2. Direct edges on declaration ---
518
+ for (const hel of await db.getIncomingEdges(declId, ['HAS_ELEMENT'])) enq(hel.src);
519
+ for (const hp of await db.getIncomingEdges(declId, ['HAS_PROPERTY'])) enq(hp.src);
520
+ for (const ret of await db.getIncomingEdges(declId, ['RETURNS'])) await enqueueFnCallers(ret.src);
521
+ for (const y of await db.getIncomingEdges(declId, ['YIELDS'])) await enqueueFnCallers(y.src);
522
+ for (const io of await db.getIncomingEdges(declId, ['ITERATES_OVER'])) enq(io.src);
523
+ for (const af of await db.getIncomingEdges(declId, ['ASSIGNED_FROM'])) enq(af.src);
524
+
525
+ // WRITES_TO incoming on declaration
526
+ for (const wt of await db.getIncomingEdges(declId, ['WRITES_TO'])) {
527
+ enq(await resolveRef(db, wt.src));
528
+ }
529
+
530
+ // PASSES_ARGUMENT incoming on declaration
531
+ for (const pa of await db.getIncomingEdges(declId, ['PASSES_ARGUMENT'])) {
532
+ await followPassesArgument(pa);
533
+ }
534
+
535
+ // --- 2b. Method call receiver heuristic ---
536
+ // When graph lacks CALL→DERIVED_FROM→PA→READS_FROM→REF edges for method calls,
537
+ // use CALL naming convention ("db.getNode" → receiver "db") to find method calls
538
+ // on this declaration and trace their consumers.
539
+ if ((node.type === 'CONSTANT' || node.type === 'VARIABLE' || node.type === 'PARAMETER') && node.name && node.file) {
540
+ const idx = await getCallsByReceiver();
541
+ const key = `${node.file}::${node.name}`;
542
+ const methodCalls = idx.get(key);
543
+ if (methodCalls) {
544
+ for (const callId of methodCalls) enq(callId);
545
+ }
546
+ }
547
+
548
+ // --- 3. PA write→read propagation via receiver chain matching ---
549
+ if (node.type === 'PROPERTY_ACCESS') {
550
+ const myChain = await resolveReceiverChain(db, declId);
551
+ if (myChain) {
552
+ const wt = await db.getOutgoingEdges(declId, ['WRITES_TO']);
553
+ if (wt.length > 0 && node.name) {
554
+ const idx = await getPAReadByName();
555
+ const readers = idx.get(node.name) || [];
556
+ for (const r of readers) {
557
+ if (r.base === myChain.base && r.path === myChain.path) enq(r.id);
558
+ }
559
+ }
560
+ }
561
+ }
562
+
563
+ // --- 4. DERIVED_FROM incoming on declaration ---
564
+ for (const df of await db.getIncomingEdges(declId, ['DERIVED_FROM'])) {
565
+ const dfN = await db.getNode(df.src);
566
+ if (dfN?.type === 'CALL') await enqueueCallConsumers(df.src);
567
+ else if (dfN?.type === 'EXPRESSION') enq(df.src);
568
+ }
569
+
570
+ // --- 5. FUNCTION type → enqueueFnCallers ---
571
+ if (node.type === 'FUNCTION') await enqueueFnCallers(declId);
572
+
573
+ // --- 6. CALL type → enqueueCallConsumers ---
574
+ if (node.type === 'CALL') await enqueueCallConsumers(declId);
575
+ }
576
+
577
+ return reachedNodes;
578
+ }
579
+
580
+ // === BACKWARD BFS ===
581
+
582
+ /**
583
+ * Backward BFS dataflow trace.
584
+ * Starting from a declaration node, finds all nodes where data comes FROM.
585
+ * Returns all reached nodes (including the start node at index 0).
586
+ */
587
+ export async function traceBackwardBFS(
588
+ db: DataflowBackend,
589
+ startId: string,
590
+ maxIterations: number,
591
+ ): Promise<DataflowNode[]> {
592
+ const visited = new Set<string>();
593
+ const queue: string[] = [];
594
+ const reachedNodes: DataflowNode[] = [];
595
+
596
+ function enq(id: string | null): void {
597
+ if (id && !visited.has(id)) {
598
+ visited.add(id);
599
+ queue.push(id);
600
+ }
601
+ }
602
+
603
+ // Lazy PA writer index
604
+ let paWriterByName: Map<string, PAEntry[]> | null = null;
605
+
606
+ async function getPAWriterByName(): Promise<Map<string, PAEntry[]>> {
607
+ if (paWriterByName) return paWriterByName;
608
+ paWriterByName = new Map();
609
+ for await (const n of db.queryNodes({ type: 'PROPERTY_ACCESS' })) {
610
+ const wt = await db.getOutgoingEdges(n.id, ['WRITES_TO']);
611
+ if (wt.length > 0 && n.name) {
612
+ const chain = await resolveReceiverChain(db, n.id);
613
+ if (chain) {
614
+ let list = paWriterByName.get(n.name);
615
+ if (!list) { list = []; paWriterByName.set(n.name, list); }
616
+ list.push({ id: n.id, base: chain.base, path: chain.path });
617
+ }
618
+ }
619
+ }
620
+ return paWriterByName;
621
+ }
622
+
623
+ enq(startId);
624
+ let iterations = 0;
625
+
626
+ while (queue.length > 0) {
627
+ const nodeId = queue.shift()!;
628
+ iterations++;
629
+ if (iterations > maxIterations) break;
630
+
631
+ const node = await db.getNode(nodeId);
632
+ if (!node) continue;
633
+
634
+ reachedNodes.push(node);
635
+
636
+ // 1. ASSIGNED_FROM outgoing → resolve ref → enqueue source
637
+ for (const af of await db.getOutgoingEdges(nodeId, ['ASSIGNED_FROM'])) {
638
+ const src = await resolveRef(db, af.dst);
639
+ enq(src);
640
+ const afNode = await db.getNode(af.dst);
641
+ if (afNode && !['CONSTANT', 'VARIABLE', 'PARAMETER', 'REFERENCE'].includes(afNode.type)) {
642
+ for (const ch of await db.getOutgoingEdges(af.dst, [...STRUCTURAL_EDGE_TYPES])) {
643
+ const resolved = await resolveRef(db, ch.dst);
644
+ enq(resolved);
645
+ }
646
+ }
647
+ }
648
+
649
+ // 2. Refs that write to this node → trace back to what was written
650
+ const refsToNode = await db.getIncomingEdges(nodeId, ['READS_FROM']);
651
+ for (const refEdge of refsToNode) {
652
+ const wtEdges = await db.getOutgoingEdges(refEdge.src, ['WRITES_TO']);
653
+ for (const wt of wtEdges) {
654
+ const resolved = await resolveRef(db, wt.dst);
655
+ enq(resolved);
656
+ }
657
+ }
658
+
659
+ // 3. PARAMETER: incoming RECEIVES_ARGUMENT → FUNCTION → incoming CALLS → CALL → PASSES_ARGUMENT
660
+ if (node.type === 'PARAMETER') {
661
+ const raEdges = await db.getIncomingEdges(nodeId, ['RECEIVES_ARGUMENT']);
662
+ for (const ra of raEdges) {
663
+ const fnId = ra.src;
664
+ const raIdx = edgeIndex(ra);
665
+ const callsIn = await db.getIncomingEdges(fnId, ['CALLS']);
666
+ for (const callEdge of callsIn) {
667
+ const paEdges = await db.getOutgoingEdges(callEdge.src, ['PASSES_ARGUMENT']);
668
+ for (const pa of paEdges) {
669
+ if (!indexMatch(raIdx, edgeIndex(pa))) continue;
670
+ const argSrc = await resolveRef(db, pa.dst);
671
+ enq(argSrc);
672
+ }
673
+ }
674
+ }
675
+ }
676
+
677
+ // 4. Structural descent: outgoing HAS_ELEMENT, HAS_PROPERTY → descend, resolveRef
678
+ for (const ch of await db.getOutgoingEdges(nodeId, ['HAS_ELEMENT', 'HAS_PROPERTY'])) {
679
+ const resolved = await resolveRef(db, ch.dst);
680
+ enq(resolved);
681
+ }
682
+
683
+ // 5. CALL node: trace arguments and receiver chain
684
+ if (node.type === 'CALL') {
685
+ for (const pa of await db.getOutgoingEdges(nodeId, ['PASSES_ARGUMENT'])) {
686
+ const argSrc = await resolveRef(db, pa.dst);
687
+ enq(argSrc);
688
+ }
689
+ for (const df of await db.getOutgoingEdges(nodeId, ['DERIVED_FROM'])) {
690
+ const dfN = await db.getNode(df.dst);
691
+ if (dfN?.type === 'PROPERTY_ACCESS') {
692
+ for (const rf of await db.getOutgoingEdges(df.dst, ['READS_FROM'])) {
693
+ const resolved = await resolveRef(db, rf.dst);
694
+ enq(resolved);
695
+ }
696
+ }
697
+ }
698
+ // 5b. Receiver heuristic: when CALL→DERIVED_FROM→PA→READS_FROM chain is missing,
699
+ // parse receiver name from CALL name ("db.getNode" → "db") and find its declaration.
700
+ if (node.name?.includes('.')) {
701
+ const dotIdx = node.name.indexOf('.');
702
+ const receiverName = node.name.substring(0, dotIdx);
703
+ if (!receiverName.startsWith('<')) {
704
+ for (const declType of ['CONSTANT', 'VARIABLE', 'PARAMETER'] as const) {
705
+ for await (const decl of db.queryNodes({ type: declType, name: receiverName })) {
706
+ if (decl.file === node.file) enq(decl.id);
707
+ }
708
+ }
709
+ }
710
+ }
711
+ }
712
+
713
+ // 6. PA node: READS_FROM → resolveRef (the receiver's data source)
714
+ if (node.type === 'PROPERTY_ACCESS') {
715
+ for (const rf of await db.getOutgoingEdges(nodeId, ['READS_FROM'])) {
716
+ const resolved = await resolveRef(db, rf.dst);
717
+ enq(resolved);
718
+ }
719
+ }
720
+
721
+ // 7. ITERATES_OVER outgoing → resolveRef → enqueue (the iterable)
722
+ for (const io of await db.getOutgoingEdges(nodeId, ['ITERATES_OVER'])) {
723
+ const resolved = await resolveRef(db, io.dst);
724
+ enq(resolved);
725
+ }
726
+
727
+ // 8. Property read→write propagation: if this is a PA reader, find matching writers
728
+ if (node.type === 'PROPERTY_ACCESS' && node.name) {
729
+ const wt = await db.getOutgoingEdges(nodeId, ['WRITES_TO']);
730
+ if (wt.length === 0) {
731
+ const myChain = await resolveReceiverChain(db, nodeId);
732
+ if (myChain) {
733
+ const idx = await getPAWriterByName();
734
+ const writers = idx.get(node.name) || [];
735
+ for (const w of writers) {
736
+ if (w.base === myChain.base && w.path === myChain.path) enq(w.id);
737
+ }
738
+ }
739
+ }
740
+ }
741
+
742
+ // 9. THROWS: if this is a catch PARAMETER, find thrown values
743
+ if (node.type === 'PARAMETER') {
744
+ const decls = await db.getIncomingEdges(nodeId, ['DECLARES']);
745
+ let isCatch = false;
746
+ for (const d of decls) {
747
+ const scope = await db.getNode(d.src);
748
+ if (scope?.name === 'catch') { isCatch = true; break; }
749
+ }
750
+ if (isCatch) {
751
+ for await (const fn of db.queryNodes({ type: 'FUNCTION' })) {
752
+ for (const t of await db.getOutgoingEdges(fn.id, ['THROWS'])) {
753
+ const resolved = await resolveRef(db, t.dst);
754
+ enq(resolved);
755
+ }
756
+ }
757
+ for await (const mod of db.queryNodes({ type: 'MODULE' })) {
758
+ for (const t of await db.getOutgoingEdges(mod.id, ['THROWS'])) {
759
+ const resolved = await resolveRef(db, t.dst);
760
+ enq(resolved);
761
+ }
762
+ }
763
+ }
764
+ }
765
+
766
+ // 10. DERIVED_FROM outgoing: trace expression operands
767
+ for (const df of await db.getOutgoingEdges(nodeId, ['DERIVED_FROM'])) {
768
+ const resolved = await resolveRef(db, df.dst);
769
+ enq(resolved);
770
+ }
771
+
772
+ // 11. RETURNS incoming → if someone returns this node, trace the function's callers' arguments
773
+ for (const ret of await db.getIncomingEdges(nodeId, ['RETURNS'])) {
774
+ const callsIn = await db.getIncomingEdges(ret.src, ['CALLS']);
775
+ for (const callEdge of callsIn) {
776
+ for (const pa of await db.getOutgoingEdges(callEdge.src, ['PASSES_ARGUMENT'])) {
777
+ const argSrc = await resolveRef(db, pa.dst);
778
+ enq(argSrc);
779
+ }
780
+ }
781
+ }
782
+ }
783
+
784
+ return reachedNodes;
785
+ }
786
+
787
+ // === HIGH-LEVEL API ===
788
+
789
+ /**
790
+ * Trace dataflow from a starting node.
791
+ * Handles REFERENCE resolution, runs BFS in requested direction(s).
792
+ * Returns results with start node excluded from reached lists.
793
+ */
794
+ export async function traceDataflow(
795
+ db: DataflowBackend,
796
+ sourceId: string,
797
+ options: TraceDataflowOptions = {},
798
+ ): Promise<TraceDataflowResult[]> {
799
+ const { direction = 'forward', maxDepth = 10, limit } = options;
800
+ const maxIterations = Math.min((maxDepth || 10) * 100, 5000);
801
+
802
+ // Resolve REFERENCE to declaration
803
+ let startId = sourceId;
804
+ const sourceNode = await db.getNode(sourceId);
805
+ if (sourceNode?.type === 'REFERENCE') {
806
+ const edges = await db.getOutgoingEdges(sourceId, ['READS_FROM']);
807
+ if (edges.length > 0) startId = edges[0].dst;
808
+ }
809
+
810
+ const resolvedStart = await db.getNode(startId);
811
+ if (!resolvedStart) return [];
812
+
813
+ const results: TraceDataflowResult[] = [];
814
+
815
+ if (direction === 'forward' || direction === 'both') {
816
+ const nodes = await traceForwardBFS(db, startId, maxIterations);
817
+ const reached = nodes.slice(1); // skip start node
818
+ results.push({
819
+ direction: 'forward',
820
+ startNode: resolvedStart,
821
+ reached: limit ? reached.slice(0, limit) : reached,
822
+ totalReached: reached.length,
823
+ });
824
+ }
825
+
826
+ if (direction === 'backward' || direction === 'both') {
827
+ const nodes = await traceBackwardBFS(db, startId, maxIterations);
828
+ const reached = nodes.slice(1);
829
+ results.push({
830
+ direction: 'backward',
831
+ startNode: resolvedStart,
832
+ reached: limit ? reached.slice(0, limit) : reached,
833
+ totalReached: reached.length,
834
+ });
835
+ }
836
+
837
+ return results;
838
+ }