@veraxhq/verax 0.2.1 → 0.3.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 (152) hide show
  1. package/README.md +14 -18
  2. package/bin/verax.js +7 -0
  3. package/package.json +3 -3
  4. package/src/cli/commands/baseline.js +104 -0
  5. package/src/cli/commands/default.js +79 -25
  6. package/src/cli/commands/ga.js +243 -0
  7. package/src/cli/commands/gates.js +95 -0
  8. package/src/cli/commands/inspect.js +131 -2
  9. package/src/cli/commands/release-check.js +213 -0
  10. package/src/cli/commands/run.js +246 -35
  11. package/src/cli/commands/security-check.js +211 -0
  12. package/src/cli/commands/truth.js +114 -0
  13. package/src/cli/entry.js +304 -67
  14. package/src/cli/util/angular-component-extractor.js +179 -0
  15. package/src/cli/util/angular-navigation-detector.js +141 -0
  16. package/src/cli/util/angular-network-detector.js +161 -0
  17. package/src/cli/util/angular-state-detector.js +162 -0
  18. package/src/cli/util/ast-interactive-detector.js +546 -0
  19. package/src/cli/util/ast-network-detector.js +603 -0
  20. package/src/cli/util/ast-usestate-detector.js +602 -0
  21. package/src/cli/util/bootstrap-guard.js +86 -0
  22. package/src/cli/util/determinism-runner.js +123 -0
  23. package/src/cli/util/determinism-writer.js +129 -0
  24. package/src/cli/util/env-url.js +4 -0
  25. package/src/cli/util/expectation-extractor.js +369 -73
  26. package/src/cli/util/findings-writer.js +126 -16
  27. package/src/cli/util/learn-writer.js +3 -1
  28. package/src/cli/util/observe-writer.js +3 -1
  29. package/src/cli/util/paths.js +3 -12
  30. package/src/cli/util/project-discovery.js +3 -0
  31. package/src/cli/util/project-writer.js +3 -1
  32. package/src/cli/util/run-resolver.js +64 -0
  33. package/src/cli/util/source-requirement.js +55 -0
  34. package/src/cli/util/summary-writer.js +1 -0
  35. package/src/cli/util/svelte-navigation-detector.js +163 -0
  36. package/src/cli/util/svelte-network-detector.js +80 -0
  37. package/src/cli/util/svelte-sfc-extractor.js +147 -0
  38. package/src/cli/util/svelte-state-detector.js +243 -0
  39. package/src/cli/util/vue-navigation-detector.js +177 -0
  40. package/src/cli/util/vue-sfc-extractor.js +162 -0
  41. package/src/cli/util/vue-state-detector.js +215 -0
  42. package/src/verax/cli/finding-explainer.js +56 -3
  43. package/src/verax/core/artifacts/registry.js +154 -0
  44. package/src/verax/core/artifacts/verifier.js +980 -0
  45. package/src/verax/core/baseline/baseline.enforcer.js +137 -0
  46. package/src/verax/core/baseline/baseline.snapshot.js +231 -0
  47. package/src/verax/core/capabilities/gates.js +499 -0
  48. package/src/verax/core/capabilities/registry.js +475 -0
  49. package/src/verax/core/confidence/confidence-compute.js +137 -0
  50. package/src/verax/core/confidence/confidence-invariants.js +234 -0
  51. package/src/verax/core/confidence/confidence-report-writer.js +112 -0
  52. package/src/verax/core/confidence/confidence-weights.js +44 -0
  53. package/src/verax/core/confidence/confidence.defaults.js +65 -0
  54. package/src/verax/core/confidence/confidence.loader.js +79 -0
  55. package/src/verax/core/confidence/confidence.schema.js +94 -0
  56. package/src/verax/core/confidence-engine-refactor.js +484 -0
  57. package/src/verax/core/confidence-engine.js +486 -0
  58. package/src/verax/core/confidence-engine.js.backup +471 -0
  59. package/src/verax/core/contracts/index.js +29 -0
  60. package/src/verax/core/contracts/types.js +185 -0
  61. package/src/verax/core/contracts/validators.js +381 -0
  62. package/src/verax/core/decision-snapshot.js +30 -3
  63. package/src/verax/core/decisions/decision.trace.js +276 -0
  64. package/src/verax/core/determinism/contract-writer.js +89 -0
  65. package/src/verax/core/determinism/contract.js +139 -0
  66. package/src/verax/core/determinism/diff.js +364 -0
  67. package/src/verax/core/determinism/engine.js +221 -0
  68. package/src/verax/core/determinism/finding-identity.js +148 -0
  69. package/src/verax/core/determinism/normalize.js +438 -0
  70. package/src/verax/core/determinism/report-writer.js +92 -0
  71. package/src/verax/core/determinism/run-fingerprint.js +118 -0
  72. package/src/verax/core/dynamic-route-intelligence.js +528 -0
  73. package/src/verax/core/evidence/evidence-capture-service.js +307 -0
  74. package/src/verax/core/evidence/evidence-intent-ledger.js +165 -0
  75. package/src/verax/core/evidence-builder.js +487 -0
  76. package/src/verax/core/execution-mode-context.js +77 -0
  77. package/src/verax/core/execution-mode-detector.js +190 -0
  78. package/src/verax/core/failures/exit-codes.js +86 -0
  79. package/src/verax/core/failures/failure-summary.js +76 -0
  80. package/src/verax/core/failures/failure.factory.js +225 -0
  81. package/src/verax/core/failures/failure.ledger.js +132 -0
  82. package/src/verax/core/failures/failure.types.js +196 -0
  83. package/src/verax/core/failures/index.js +10 -0
  84. package/src/verax/core/ga/ga-report-writer.js +43 -0
  85. package/src/verax/core/ga/ga.artifact.js +49 -0
  86. package/src/verax/core/ga/ga.contract.js +434 -0
  87. package/src/verax/core/ga/ga.enforcer.js +86 -0
  88. package/src/verax/core/guardrails/guardrails-report-writer.js +109 -0
  89. package/src/verax/core/guardrails/policy.defaults.js +210 -0
  90. package/src/verax/core/guardrails/policy.loader.js +83 -0
  91. package/src/verax/core/guardrails/policy.schema.js +110 -0
  92. package/src/verax/core/guardrails/truth-reconciliation.js +136 -0
  93. package/src/verax/core/guardrails-engine.js +505 -0
  94. package/src/verax/core/observe/run-timeline.js +316 -0
  95. package/src/verax/core/perf/perf.contract.js +186 -0
  96. package/src/verax/core/perf/perf.display.js +65 -0
  97. package/src/verax/core/perf/perf.enforcer.js +91 -0
  98. package/src/verax/core/perf/perf.monitor.js +209 -0
  99. package/src/verax/core/perf/perf.report.js +198 -0
  100. package/src/verax/core/pipeline-tracker.js +238 -0
  101. package/src/verax/core/product-definition.js +127 -0
  102. package/src/verax/core/release/provenance.builder.js +271 -0
  103. package/src/verax/core/release/release-report-writer.js +40 -0
  104. package/src/verax/core/release/release.enforcer.js +159 -0
  105. package/src/verax/core/release/reproducibility.check.js +221 -0
  106. package/src/verax/core/release/sbom.builder.js +283 -0
  107. package/src/verax/core/report/cross-index.js +192 -0
  108. package/src/verax/core/report/human-summary.js +222 -0
  109. package/src/verax/core/route-intelligence.js +419 -0
  110. package/src/verax/core/security/secrets.scan.js +326 -0
  111. package/src/verax/core/security/security-report.js +50 -0
  112. package/src/verax/core/security/security.enforcer.js +124 -0
  113. package/src/verax/core/security/supplychain.defaults.json +38 -0
  114. package/src/verax/core/security/supplychain.policy.js +326 -0
  115. package/src/verax/core/security/vuln.scan.js +265 -0
  116. package/src/verax/core/truth/truth.certificate.js +250 -0
  117. package/src/verax/core/ui-feedback-intelligence.js +515 -0
  118. package/src/verax/detect/confidence-engine.js +628 -40
  119. package/src/verax/detect/confidence-helper.js +33 -0
  120. package/src/verax/detect/detection-engine.js +18 -1
  121. package/src/verax/detect/dynamic-route-findings.js +335 -0
  122. package/src/verax/detect/expectation-chain-detector.js +417 -0
  123. package/src/verax/detect/expectation-model.js +3 -1
  124. package/src/verax/detect/findings-writer.js +141 -5
  125. package/src/verax/detect/index.js +229 -5
  126. package/src/verax/detect/journey-stall-detector.js +558 -0
  127. package/src/verax/detect/route-findings.js +218 -0
  128. package/src/verax/detect/ui-feedback-findings.js +207 -0
  129. package/src/verax/detect/verdict-engine.js +57 -3
  130. package/src/verax/detect/view-switch-correlator.js +242 -0
  131. package/src/verax/index.js +413 -45
  132. package/src/verax/learn/action-contract-extractor.js +682 -64
  133. package/src/verax/learn/route-validator.js +4 -1
  134. package/src/verax/observe/index.js +88 -843
  135. package/src/verax/observe/interaction-runner.js +25 -8
  136. package/src/verax/observe/observe-context.js +205 -0
  137. package/src/verax/observe/observe-helpers.js +191 -0
  138. package/src/verax/observe/observe-runner.js +226 -0
  139. package/src/verax/observe/observers/budget-observer.js +185 -0
  140. package/src/verax/observe/observers/console-observer.js +102 -0
  141. package/src/verax/observe/observers/coverage-observer.js +107 -0
  142. package/src/verax/observe/observers/interaction-observer.js +471 -0
  143. package/src/verax/observe/observers/navigation-observer.js +132 -0
  144. package/src/verax/observe/observers/network-observer.js +87 -0
  145. package/src/verax/observe/observers/safety-observer.js +82 -0
  146. package/src/verax/observe/observers/ui-feedback-observer.js +99 -0
  147. package/src/verax/observe/ui-feedback-detector.js +742 -0
  148. package/src/verax/observe/ui-signal-sensor.js +148 -2
  149. package/src/verax/scan-summary-writer.js +42 -8
  150. package/src/verax/shared/artifact-manager.js +8 -5
  151. package/src/verax/shared/css-spinner-rules.js +204 -0
  152. package/src/verax/shared/view-switch-rules.js +208 -0
@@ -0,0 +1,603 @@
1
+ import { parse } from '@babel/parser';
2
+ import _traverse from '@babel/traverse';
3
+
4
+ // Handle default export from @babel/traverse (CommonJS/ESM compatibility)
5
+ const traverse = _traverse.default || _traverse;
6
+
7
+ /**
8
+ * PHASE 9 — AST-Based Network Detection
9
+ *
10
+ * Production-quality AST-based network call detection.
11
+ * Detects fetch/axios/XMLHttpRequest calls in nested contexts:
12
+ * - Event handlers (onClick, onSubmit, etc.)
13
+ * - React hooks (useEffect, useCallback, etc.)
14
+ * - Custom functions bound to UI interactions
15
+ *
16
+ * Features:
17
+ * - AST source code extraction for evidence
18
+ * - False-positive filtering (analytics)
19
+ * - UI-bound handler detection
20
+ * - Deterministic behavior
21
+ */
22
+
23
+ /**
24
+ * Detect network calls in source code using AST parsing
25
+ * @param {string} content - File content
26
+ * @param {string} filePath - Absolute file path
27
+ * @param {string} relPath - Relative path from source root
28
+ * @returns {Array} Array of detected network calls with metadata including AST source
29
+ */
30
+ export function detectNetworkCallsAST(content, filePath, relPath) {
31
+ const detections = [];
32
+ const lines = content.split('\n');
33
+
34
+ try {
35
+ // Parse with comprehensive plugin support
36
+ const ast = parse(content, {
37
+ sourceType: 'module',
38
+ plugins: [
39
+ 'jsx',
40
+ 'typescript',
41
+ 'classProperties',
42
+ 'optionalChaining',
43
+ 'nullishCoalescingOperator',
44
+ 'dynamicImport',
45
+ ['decorators', { decoratorsBeforeExport: true }],
46
+ 'topLevelAwait',
47
+ 'objectRestSpread',
48
+ 'asyncGenerators',
49
+ 'functionBind',
50
+ 'exportDefaultFrom',
51
+ 'exportNamespaceFrom',
52
+ ],
53
+ errorRecovery: true,
54
+ });
55
+
56
+ // Track axios imports and aliases
57
+ const axiosBindings = new Set();
58
+
59
+ traverse(ast, {
60
+ // Track axios imports
61
+ ImportDeclaration(path) {
62
+ if (path.node.source.value === 'axios') {
63
+ path.node.specifiers.forEach((spec) => {
64
+ if (spec.type === 'ImportDefaultSpecifier' ||
65
+ spec.type === 'ImportSpecifier') {
66
+ axiosBindings.add(spec.local.name);
67
+ }
68
+ });
69
+ }
70
+ },
71
+
72
+ // Detect require('axios')
73
+ VariableDeclarator(path) {
74
+ if (path.node.init?.type === 'CallExpression' &&
75
+ path.node.init.callee.name === 'require' &&
76
+ path.node.init.arguments[0]?.value === 'axios') {
77
+ if (path.node.id.type === 'Identifier') {
78
+ axiosBindings.add(path.node.id.name);
79
+ }
80
+ }
81
+ },
82
+
83
+ // Detect fetch() calls
84
+ CallExpression(path) {
85
+ const { node } = path;
86
+ const loc = node.loc;
87
+
88
+ // Check for fetch(...)
89
+ if (node.callee.type === 'Identifier' && node.callee.name === 'fetch') {
90
+ // Check if fetch is shadowed in local scope
91
+ if (path.scope.hasBinding('fetch')) {
92
+ return; // Shadowed by local variable/parameter
93
+ }
94
+
95
+ const urlArg = node.arguments[0];
96
+ const initArg = node.arguments[1];
97
+ const url = extractUrl(urlArg);
98
+
99
+ // PHASE 9: Filter false positives (analytics calls)
100
+ if (isAnalyticsCall(url, path)) {
101
+ return; // Skip analytics - not a user-facing promise
102
+ }
103
+
104
+ const context = inferContext(path);
105
+ const isUIBound = isUIBoundHandler(path);
106
+
107
+ const detection = {
108
+ kind: 'fetch',
109
+ url: url,
110
+ method: extractMethod(initArg, 'GET'),
111
+ location: {
112
+ line: loc?.start.line,
113
+ column: loc?.start.column,
114
+ },
115
+ context: context,
116
+ isUIBound: isUIBound,
117
+ astSource: extractASTSource(node, lines, loc),
118
+ };
119
+
120
+ detections.push(detection);
121
+ }
122
+
123
+ // Check for globalThis.fetch(...)
124
+ if (node.callee.type === 'MemberExpression' &&
125
+ node.callee.object.name === 'globalThis' &&
126
+ node.callee.property.name === 'fetch') {
127
+ const urlArg = node.arguments[0];
128
+ const initArg = node.arguments[1];
129
+ const url = extractUrl(urlArg);
130
+
131
+ // PHASE 9: Filter false positives (analytics calls)
132
+ if (isAnalyticsCall(url, path)) {
133
+ return; // Skip analytics - not a user-facing promise
134
+ }
135
+
136
+ const context = inferContext(path);
137
+ const isUIBound = isUIBoundHandler(path);
138
+
139
+ const detection = {
140
+ kind: 'fetch',
141
+ url: url,
142
+ method: extractMethod(initArg, 'GET'),
143
+ location: {
144
+ line: loc?.start.line,
145
+ column: loc?.start.column,
146
+ },
147
+ context: context,
148
+ isUIBound: isUIBound,
149
+ astSource: extractASTSource(node, lines, loc),
150
+ };
151
+
152
+ detections.push(detection);
153
+ }
154
+
155
+ // Check for axios(...) or axios.get/post/etc(...)
156
+ if (isAxiosCall(node, axiosBindings)) {
157
+ const method = extractAxiosMethod(node);
158
+ const urlArg = getAxiosUrlArg(node, method);
159
+ const url = extractUrl(urlArg);
160
+
161
+ // PHASE 9: Filter false positives (analytics calls)
162
+ if (isAnalyticsCall(url, path)) {
163
+ return; // Skip analytics - not a user-facing promise
164
+ }
165
+
166
+ const context = inferContext(path);
167
+ const isUIBound = isUIBoundHandler(path);
168
+
169
+ const detection = {
170
+ kind: 'axios',
171
+ url: url,
172
+ method: method.toUpperCase(),
173
+ location: {
174
+ line: loc?.start.line,
175
+ column: loc?.start.column,
176
+ },
177
+ context: context,
178
+ isUIBound: isUIBound,
179
+ astSource: extractASTSource(node, lines, loc),
180
+ };
181
+
182
+ detections.push(detection);
183
+ }
184
+ },
185
+
186
+ // Detect new XMLHttpRequest()
187
+ NewExpression(path) {
188
+ const { node } = path;
189
+ const loc = node.loc;
190
+
191
+ if (node.callee.type === 'Identifier' &&
192
+ node.callee.name === 'XMLHttpRequest') {
193
+
194
+ // Try to find associated .open() call
195
+ const xhrDetails = findXhrOpen(path);
196
+
197
+ const url = xhrDetails.url || '<dynamic>';
198
+
199
+ // PHASE 9: Filter false positives (analytics calls)
200
+ if (isAnalyticsCall(url, path)) {
201
+ return; // Skip analytics - not a user-facing promise
202
+ }
203
+
204
+ const context = inferContext(path);
205
+ const isUIBound = isUIBoundHandler(path);
206
+
207
+ const detection = {
208
+ kind: 'xhr',
209
+ url: url,
210
+ method: xhrDetails.method || 'GET',
211
+ location: {
212
+ line: loc?.start.line,
213
+ column: loc?.start.column,
214
+ },
215
+ context: context,
216
+ isUIBound: isUIBound,
217
+ astSource: extractASTSource(node, lines, loc),
218
+ };
219
+
220
+ detections.push(detection);
221
+ }
222
+ },
223
+ });
224
+
225
+ } catch (error) {
226
+ // Parse errors are silently skipped (malformed code, etc.)
227
+ // In production, you might log these for debugging
228
+ }
229
+
230
+ return detections;
231
+ }
232
+
233
+ /**
234
+ * Check if a call expression is an axios call
235
+ */
236
+ function isAxiosCall(node, axiosBindings) {
237
+ // Direct axios call: axios(...)
238
+ if (node.callee.type === 'Identifier' &&
239
+ axiosBindings.has(node.callee.name)) {
240
+ return true;
241
+ }
242
+
243
+ // Method call: axios.get/post/etc(...)
244
+ if (node.callee.type === 'MemberExpression' &&
245
+ node.callee.object.type === 'Identifier' &&
246
+ axiosBindings.has(node.callee.object.name)) {
247
+ const method = node.callee.property.name;
248
+ return ['get', 'post', 'put', 'delete', 'patch', 'head', 'options', 'request'].includes(method);
249
+ }
250
+
251
+ return false;
252
+ }
253
+
254
+ /**
255
+ * Extract HTTP method from axios call
256
+ */
257
+ function extractAxiosMethod(node) {
258
+ if (node.callee.type === 'MemberExpression') {
259
+ return node.callee.property.name; // get, post, etc.
260
+ }
261
+
262
+ // Direct axios(...) call - check config.method
263
+ const configArg = node.arguments[0];
264
+ if (configArg?.type === 'ObjectExpression') {
265
+ const methodProp = configArg.properties.find(
266
+ p => p.key?.name === 'method'
267
+ );
268
+ if (methodProp?.value.type === 'StringLiteral') {
269
+ return methodProp.value.value;
270
+ }
271
+ }
272
+
273
+ return 'request'; // default for axios(config)
274
+ }
275
+
276
+ /**
277
+ * Get URL argument for axios call
278
+ */
279
+ function getAxiosUrlArg(node, method) {
280
+ // axios.get(url, ...) - URL is first arg
281
+ if (method !== 'request' && node.callee.type === 'MemberExpression') {
282
+ return node.arguments[0];
283
+ }
284
+
285
+ // axios(config) - URL is in config.url
286
+ const configArg = node.arguments[0];
287
+ if (configArg?.type === 'ObjectExpression') {
288
+ const urlProp = configArg.properties.find(
289
+ p => p.key?.name === 'url'
290
+ );
291
+ return urlProp?.value;
292
+ }
293
+
294
+ return null;
295
+ }
296
+
297
+ /**
298
+ * Extract URL from argument node
299
+ */
300
+ function extractUrl(urlArg) {
301
+ if (!urlArg) {
302
+ return '<dynamic>';
303
+ }
304
+
305
+ // String literal: "https://example.com"
306
+ if (urlArg.type === 'StringLiteral') {
307
+ return urlArg.value;
308
+ }
309
+
310
+ // Template literal without expressions: `https://example.com`
311
+ if (urlArg.type === 'TemplateLiteral' &&
312
+ urlArg.expressions.length === 0) {
313
+ return urlArg.quasis[0].value.cooked;
314
+ }
315
+
316
+ // Template literal with expressions: `https://example.com/${id}`
317
+ if (urlArg.type === 'TemplateLiteral' &&
318
+ urlArg.expressions.length > 0) {
319
+ return '<dynamic>';
320
+ }
321
+
322
+ // Any other expression (variable, computation, etc.)
323
+ return '<dynamic>';
324
+ }
325
+
326
+ /**
327
+ * Extract HTTP method from fetch init object
328
+ */
329
+ function extractMethod(initArg, defaultMethod = 'GET') {
330
+ if (!initArg || initArg.type !== 'ObjectExpression') {
331
+ return defaultMethod;
332
+ }
333
+
334
+ const methodProp = initArg.properties.find(
335
+ p => p.key?.name === 'method'
336
+ );
337
+
338
+ if (methodProp?.value.type === 'StringLiteral') {
339
+ return methodProp.value.value.toUpperCase();
340
+ }
341
+
342
+ return defaultMethod;
343
+ }
344
+
345
+ /**
346
+ * Infer execution context (handler, hook, component, etc.)
347
+ */
348
+ function inferContext(path) {
349
+ const contexts = [];
350
+
351
+ let current = path.parentPath;
352
+ while (current) {
353
+ const node = current.node;
354
+
355
+ // Event handler prop: onClick={() => ...}
356
+ if (current.isJSXAttribute()) {
357
+ const attrName = node.name.name;
358
+ if (attrName && attrName.startsWith('on')) {
359
+ contexts.push(`handler:${attrName}`);
360
+ }
361
+ }
362
+
363
+ // Hook: useEffect(() => ...), useCallback(() => ...)
364
+ if (current.isCallExpression() &&
365
+ current.node.callee.type === 'Identifier') {
366
+ const calleeName = current.node.callee.name;
367
+ if (calleeName.startsWith('use')) {
368
+ contexts.push(`hook:${calleeName}`);
369
+ }
370
+ }
371
+
372
+ // Function/Arrow in component
373
+ if (current.isFunctionDeclaration() ||
374
+ current.isFunctionExpression() ||
375
+ current.isArrowFunctionExpression()) {
376
+ const funcName = getFunctionName(current);
377
+ if (funcName) {
378
+ // Check if it looks like a handler
379
+ if (funcName.startsWith('handle') || funcName.startsWith('on')) {
380
+ contexts.push(`handler:${funcName}`);
381
+ } else {
382
+ contexts.push(`function:${funcName}`);
383
+ }
384
+ }
385
+ }
386
+
387
+ current = current.parentPath;
388
+ }
389
+
390
+ return contexts.length > 0 ? contexts.reverse().join(' > ') : 'top-level';
391
+ }
392
+
393
+ /**
394
+ * Get function name from path
395
+ */
396
+ function getFunctionName(path) {
397
+ const node = path.node;
398
+
399
+ // Named function
400
+ if (node.id?.name) {
401
+ return node.id.name;
402
+ }
403
+
404
+ // Variable declarator: const handleClick = () => ...
405
+ const parent = path.parent;
406
+ if (parent.type === 'VariableDeclarator' && parent.id.name) {
407
+ return parent.id.name;
408
+ }
409
+
410
+ // Object property: { onClick: () => ... }
411
+ if (parent.type === 'ObjectProperty' && parent.key.name) {
412
+ return parent.key.name;
413
+ }
414
+
415
+ return null;
416
+ }
417
+
418
+ /**
419
+ * Try to find xhr.open() call for an XMLHttpRequest instance
420
+ * This is a best-effort heuristic
421
+ */
422
+ function findXhrOpen(newExprPath) {
423
+ const result = { url: null, method: null };
424
+
425
+ // Check if assigned to a variable
426
+ const parent = newExprPath.parent;
427
+ if (parent.type === 'VariableDeclarator' && parent.id.name) {
428
+ const xhrName = parent.id.name;
429
+
430
+ // Look for xhr.open(...) in the same scope
431
+ const binding = newExprPath.scope.getBinding(xhrName);
432
+ if (binding) {
433
+ // Scan references for .open() calls
434
+ for (const refPath of binding.referencePaths) {
435
+ const refParent = refPath.parent;
436
+ if (refParent.type === 'MemberExpression' &&
437
+ refParent.property.name === 'open') {
438
+ // Check if it's a call expression
439
+ const callParent = refPath.parentPath.parent;
440
+ if (callParent.type === 'CallExpression') {
441
+ // xhr.open(method, url, ...)
442
+ const methodArg = callParent.arguments[0];
443
+ const urlArg = callParent.arguments[1];
444
+
445
+ result.method = extractUrl(methodArg)?.toUpperCase() || 'GET';
446
+ result.url = extractUrl(urlArg);
447
+ break;
448
+ }
449
+ }
450
+ }
451
+ }
452
+ }
453
+
454
+ return result;
455
+ }
456
+
457
+ /**
458
+ * PHASE 9: Extract AST source code snippet for evidence
459
+ * @param {Object} node - AST node
460
+ * @param {string[]} lines - File content split by lines
461
+ * @param {Object} loc - Location object with start/end
462
+ * @returns {string} Source code snippet
463
+ */
464
+ function extractASTSource(node, lines, loc) {
465
+ if (!loc || !loc.start || !loc.end) {
466
+ return '';
467
+ }
468
+
469
+ const startLine = loc.start.line - 1; // 0-indexed
470
+ const endLine = loc.end.line - 1;
471
+
472
+ if (startLine < 0 || endLine >= lines.length) {
473
+ return '';
474
+ }
475
+
476
+ if (startLine === endLine) {
477
+ // Single line - extract substring
478
+ const line = lines[startLine];
479
+ const startCol = loc.start.column;
480
+ const endCol = loc.end.column;
481
+ return line.substring(startCol, endCol).trim();
482
+ } else {
483
+ // Multi-line - extract full lines
484
+ const snippet = lines.slice(startLine, endLine + 1);
485
+ // Trim first line from start column, last line to end column
486
+ if (snippet.length > 0) {
487
+ snippet[0] = snippet[0].substring(loc.start.column);
488
+ snippet[snippet.length - 1] = snippet[snippet.length - 1].substring(0, loc.end.column);
489
+ }
490
+ return snippet.join('\n').trim();
491
+ }
492
+ }
493
+
494
+ /**
495
+ * PHASE 9: Check if network call is analytics (false positive trap)
496
+ * Analytics calls should NOT be reported as user-facing promises
497
+ * @param {string} url - Network call URL
498
+ * @param {Object} path - Babel path object
499
+ * @returns {boolean} True if this is an analytics call
500
+ */
501
+ function isAnalyticsCall(url, path) {
502
+ // Check URL patterns
503
+ if (typeof url === 'string') {
504
+ const analyticsPatterns = [
505
+ '/api/analytics',
506
+ '/analytics',
507
+ '/track',
508
+ '/api/track',
509
+ '/api/event',
510
+ '/events',
511
+ '/beacon',
512
+ '/api/beacon',
513
+ ];
514
+
515
+ for (const pattern of analyticsPatterns) {
516
+ if (url.includes(pattern)) {
517
+ return true;
518
+ }
519
+ }
520
+ }
521
+
522
+ // Check context for analytics-related function names
523
+ const context = inferContext(path);
524
+ const analyticsContexts = [
525
+ 'track',
526
+ 'analytics',
527
+ 'beacon',
528
+ 'telemetry',
529
+ 'metrics',
530
+ ];
531
+
532
+ for (const keyword of analyticsContexts) {
533
+ if (context.toLowerCase().includes(keyword)) {
534
+ return true;
535
+ }
536
+ }
537
+
538
+ // Check parent function/variable names
539
+ let current = path.parentPath;
540
+ while (current) {
541
+ const funcName = getFunctionName(current);
542
+ if (funcName) {
543
+ const lowerName = funcName.toLowerCase();
544
+ if (analyticsContexts.some(keyword => lowerName.includes(keyword))) {
545
+ return true;
546
+ }
547
+ }
548
+ current = current.parentPath;
549
+ }
550
+
551
+ return false;
552
+ }
553
+
554
+ /**
555
+ * PHASE 9: Determine if handler is UI-bound (connected to user interaction)
556
+ * @param {Object} path - Babel path object
557
+ * @returns {boolean} True if handler is bound to UI interaction
558
+ */
559
+ function isUIBoundHandler(path) {
560
+ const context = inferContext(path);
561
+
562
+ // Direct event handlers (onClick, onSubmit, etc.)
563
+ if (context.includes('handler:on')) {
564
+ return true;
565
+ }
566
+
567
+ // Handler functions (handleClick, handleSubmit, etc.)
568
+ if (context.includes('handler:handle')) {
569
+ return true;
570
+ }
571
+
572
+ // Check if function is referenced in JSX
573
+ let current = path.parentPath;
574
+ while (current) {
575
+ // Check if we're inside JSX attribute
576
+ if (current.isJSXAttribute()) {
577
+ const attrName = current.node.name?.name;
578
+ if (attrName && attrName.startsWith('on')) {
579
+ return true;
580
+ }
581
+ }
582
+
583
+ // Check if function is assigned to event handler
584
+ if (current.isVariableDeclarator()) {
585
+ const varName = current.node.id?.name;
586
+ if (varName && (varName.startsWith('handle') || varName.startsWith('on'))) {
587
+ // Check if this variable is used in JSX
588
+ const binding = current.scope.getBinding(varName);
589
+ if (binding) {
590
+ for (const refPath of binding.referencePaths) {
591
+ if (refPath.findParent(p => p.isJSXAttribute())) {
592
+ return true;
593
+ }
594
+ }
595
+ }
596
+ }
597
+ }
598
+
599
+ current = current.parentPath;
600
+ }
601
+
602
+ return false;
603
+ }