@veraxhq/verax 0.2.0 → 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 (217) hide show
  1. package/README.md +14 -18
  2. package/bin/verax.js +7 -0
  3. package/package.json +15 -5
  4. package/src/cli/commands/baseline.js +104 -0
  5. package/src/cli/commands/default.js +323 -111
  6. package/src/cli/commands/doctor.js +36 -4
  7. package/src/cli/commands/ga.js +243 -0
  8. package/src/cli/commands/gates.js +95 -0
  9. package/src/cli/commands/inspect.js +131 -2
  10. package/src/cli/commands/release-check.js +213 -0
  11. package/src/cli/commands/run.js +498 -103
  12. package/src/cli/commands/security-check.js +211 -0
  13. package/src/cli/commands/truth.js +114 -0
  14. package/src/cli/entry.js +305 -68
  15. package/src/cli/util/angular-component-extractor.js +179 -0
  16. package/src/cli/util/angular-navigation-detector.js +141 -0
  17. package/src/cli/util/angular-network-detector.js +161 -0
  18. package/src/cli/util/angular-state-detector.js +162 -0
  19. package/src/cli/util/ast-interactive-detector.js +546 -0
  20. package/src/cli/util/ast-network-detector.js +603 -0
  21. package/src/cli/util/ast-usestate-detector.js +602 -0
  22. package/src/cli/util/bootstrap-guard.js +86 -0
  23. package/src/cli/util/detection-engine.js +4 -3
  24. package/src/cli/util/determinism-runner.js +123 -0
  25. package/src/cli/util/determinism-writer.js +129 -0
  26. package/src/cli/util/env-url.js +4 -0
  27. package/src/cli/util/events.js +76 -0
  28. package/src/cli/util/expectation-extractor.js +380 -74
  29. package/src/cli/util/findings-writer.js +126 -15
  30. package/src/cli/util/learn-writer.js +3 -1
  31. package/src/cli/util/observation-engine.js +69 -23
  32. package/src/cli/util/observe-writer.js +3 -1
  33. package/src/cli/util/paths.js +6 -14
  34. package/src/cli/util/project-discovery.js +23 -0
  35. package/src/cli/util/project-writer.js +3 -1
  36. package/src/cli/util/redact.js +2 -2
  37. package/src/cli/util/run-resolver.js +64 -0
  38. package/src/cli/util/runtime-budget.js +147 -0
  39. package/src/cli/util/source-requirement.js +55 -0
  40. package/src/cli/util/summary-writer.js +13 -1
  41. package/src/cli/util/svelte-navigation-detector.js +163 -0
  42. package/src/cli/util/svelte-network-detector.js +80 -0
  43. package/src/cli/util/svelte-sfc-extractor.js +147 -0
  44. package/src/cli/util/svelte-state-detector.js +243 -0
  45. package/src/cli/util/vue-navigation-detector.js +177 -0
  46. package/src/cli/util/vue-sfc-extractor.js +162 -0
  47. package/src/cli/util/vue-state-detector.js +215 -0
  48. package/src/types/global.d.ts +28 -0
  49. package/src/types/ts-ast.d.ts +24 -0
  50. package/src/verax/cli/doctor.js +2 -2
  51. package/src/verax/cli/finding-explainer.js +56 -3
  52. package/src/verax/cli/init.js +1 -1
  53. package/src/verax/cli/url-safety.js +12 -2
  54. package/src/verax/cli/wizard.js +13 -2
  55. package/src/verax/core/artifacts/registry.js +154 -0
  56. package/src/verax/core/artifacts/verifier.js +980 -0
  57. package/src/verax/core/baseline/baseline.enforcer.js +137 -0
  58. package/src/verax/core/baseline/baseline.snapshot.js +231 -0
  59. package/src/verax/core/budget-engine.js +1 -1
  60. package/src/verax/core/capabilities/gates.js +499 -0
  61. package/src/verax/core/capabilities/registry.js +475 -0
  62. package/src/verax/core/confidence/confidence-compute.js +137 -0
  63. package/src/verax/core/confidence/confidence-invariants.js +234 -0
  64. package/src/verax/core/confidence/confidence-report-writer.js +112 -0
  65. package/src/verax/core/confidence/confidence-weights.js +44 -0
  66. package/src/verax/core/confidence/confidence.defaults.js +65 -0
  67. package/src/verax/core/confidence/confidence.loader.js +79 -0
  68. package/src/verax/core/confidence/confidence.schema.js +94 -0
  69. package/src/verax/core/confidence-engine-refactor.js +484 -0
  70. package/src/verax/core/confidence-engine.js +486 -0
  71. package/src/verax/core/confidence-engine.js.backup +471 -0
  72. package/src/verax/core/contracts/index.js +29 -0
  73. package/src/verax/core/contracts/types.js +185 -0
  74. package/src/verax/core/contracts/validators.js +381 -0
  75. package/src/verax/core/decision-snapshot.js +31 -4
  76. package/src/verax/core/decisions/decision.trace.js +276 -0
  77. package/src/verax/core/determinism/contract-writer.js +89 -0
  78. package/src/verax/core/determinism/contract.js +139 -0
  79. package/src/verax/core/determinism/diff.js +364 -0
  80. package/src/verax/core/determinism/engine.js +221 -0
  81. package/src/verax/core/determinism/finding-identity.js +148 -0
  82. package/src/verax/core/determinism/normalize.js +438 -0
  83. package/src/verax/core/determinism/report-writer.js +92 -0
  84. package/src/verax/core/determinism/run-fingerprint.js +118 -0
  85. package/src/verax/core/determinism-model.js +35 -6
  86. package/src/verax/core/dynamic-route-intelligence.js +528 -0
  87. package/src/verax/core/evidence/evidence-capture-service.js +307 -0
  88. package/src/verax/core/evidence/evidence-intent-ledger.js +165 -0
  89. package/src/verax/core/evidence-builder.js +487 -0
  90. package/src/verax/core/execution-mode-context.js +77 -0
  91. package/src/verax/core/execution-mode-detector.js +190 -0
  92. package/src/verax/core/failures/exit-codes.js +86 -0
  93. package/src/verax/core/failures/failure-summary.js +76 -0
  94. package/src/verax/core/failures/failure.factory.js +225 -0
  95. package/src/verax/core/failures/failure.ledger.js +132 -0
  96. package/src/verax/core/failures/failure.types.js +196 -0
  97. package/src/verax/core/failures/index.js +10 -0
  98. package/src/verax/core/ga/ga-report-writer.js +43 -0
  99. package/src/verax/core/ga/ga.artifact.js +49 -0
  100. package/src/verax/core/ga/ga.contract.js +434 -0
  101. package/src/verax/core/ga/ga.enforcer.js +86 -0
  102. package/src/verax/core/guardrails/guardrails-report-writer.js +109 -0
  103. package/src/verax/core/guardrails/policy.defaults.js +210 -0
  104. package/src/verax/core/guardrails/policy.loader.js +83 -0
  105. package/src/verax/core/guardrails/policy.schema.js +110 -0
  106. package/src/verax/core/guardrails/truth-reconciliation.js +136 -0
  107. package/src/verax/core/guardrails-engine.js +505 -0
  108. package/src/verax/core/incremental-store.js +15 -7
  109. package/src/verax/core/observe/run-timeline.js +316 -0
  110. package/src/verax/core/perf/perf.contract.js +186 -0
  111. package/src/verax/core/perf/perf.display.js +65 -0
  112. package/src/verax/core/perf/perf.enforcer.js +91 -0
  113. package/src/verax/core/perf/perf.monitor.js +209 -0
  114. package/src/verax/core/perf/perf.report.js +198 -0
  115. package/src/verax/core/pipeline-tracker.js +238 -0
  116. package/src/verax/core/product-definition.js +127 -0
  117. package/src/verax/core/release/provenance.builder.js +271 -0
  118. package/src/verax/core/release/release-report-writer.js +40 -0
  119. package/src/verax/core/release/release.enforcer.js +159 -0
  120. package/src/verax/core/release/reproducibility.check.js +221 -0
  121. package/src/verax/core/release/sbom.builder.js +283 -0
  122. package/src/verax/core/replay-validator.js +4 -4
  123. package/src/verax/core/replay.js +1 -1
  124. package/src/verax/core/report/cross-index.js +192 -0
  125. package/src/verax/core/report/human-summary.js +222 -0
  126. package/src/verax/core/route-intelligence.js +419 -0
  127. package/src/verax/core/security/secrets.scan.js +326 -0
  128. package/src/verax/core/security/security-report.js +50 -0
  129. package/src/verax/core/security/security.enforcer.js +124 -0
  130. package/src/verax/core/security/supplychain.defaults.json +38 -0
  131. package/src/verax/core/security/supplychain.policy.js +326 -0
  132. package/src/verax/core/security/vuln.scan.js +265 -0
  133. package/src/verax/core/silence-impact.js +1 -1
  134. package/src/verax/core/silence-model.js +9 -7
  135. package/src/verax/core/truth/truth.certificate.js +250 -0
  136. package/src/verax/core/ui-feedback-intelligence.js +515 -0
  137. package/src/verax/detect/comparison.js +8 -3
  138. package/src/verax/detect/confidence-engine.js +645 -57
  139. package/src/verax/detect/confidence-helper.js +33 -0
  140. package/src/verax/detect/detection-engine.js +19 -2
  141. package/src/verax/detect/dynamic-route-findings.js +335 -0
  142. package/src/verax/detect/evidence-index.js +15 -65
  143. package/src/verax/detect/expectation-chain-detector.js +417 -0
  144. package/src/verax/detect/expectation-model.js +56 -3
  145. package/src/verax/detect/explanation-helpers.js +1 -1
  146. package/src/verax/detect/finding-detector.js +2 -2
  147. package/src/verax/detect/findings-writer.js +149 -20
  148. package/src/verax/detect/flow-detector.js +4 -4
  149. package/src/verax/detect/index.js +265 -15
  150. package/src/verax/detect/interactive-findings.js +3 -4
  151. package/src/verax/detect/journey-stall-detector.js +558 -0
  152. package/src/verax/detect/route-findings.js +218 -0
  153. package/src/verax/detect/signal-mapper.js +2 -2
  154. package/src/verax/detect/skip-classifier.js +4 -4
  155. package/src/verax/detect/ui-feedback-findings.js +207 -0
  156. package/src/verax/detect/verdict-engine.js +61 -9
  157. package/src/verax/detect/view-switch-correlator.js +242 -0
  158. package/src/verax/flow/flow-engine.js +3 -2
  159. package/src/verax/flow/flow-spec.js +1 -2
  160. package/src/verax/index.js +413 -33
  161. package/src/verax/intel/effect-detector.js +1 -1
  162. package/src/verax/intel/index.js +2 -2
  163. package/src/verax/intel/route-extractor.js +3 -3
  164. package/src/verax/intel/vue-navigation-extractor.js +81 -18
  165. package/src/verax/intel/vue-router-extractor.js +4 -2
  166. package/src/verax/learn/action-contract-extractor.js +684 -66
  167. package/src/verax/learn/ast-contract-extractor.js +53 -1
  168. package/src/verax/learn/index.js +36 -2
  169. package/src/verax/learn/manifest-writer.js +28 -14
  170. package/src/verax/learn/route-extractor.js +1 -1
  171. package/src/verax/learn/route-validator.js +12 -8
  172. package/src/verax/learn/state-extractor.js +1 -1
  173. package/src/verax/learn/static-extractor-navigation.js +1 -1
  174. package/src/verax/learn/static-extractor-validation.js +2 -2
  175. package/src/verax/learn/static-extractor.js +8 -7
  176. package/src/verax/learn/ts-contract-resolver.js +14 -12
  177. package/src/verax/observe/browser.js +22 -3
  178. package/src/verax/observe/console-sensor.js +2 -2
  179. package/src/verax/observe/expectation-executor.js +2 -1
  180. package/src/verax/observe/focus-sensor.js +1 -1
  181. package/src/verax/observe/human-driver.js +29 -10
  182. package/src/verax/observe/index.js +92 -844
  183. package/src/verax/observe/interaction-discovery.js +27 -15
  184. package/src/verax/observe/interaction-runner.js +31 -14
  185. package/src/verax/observe/loading-sensor.js +6 -0
  186. package/src/verax/observe/navigation-sensor.js +1 -1
  187. package/src/verax/observe/observe-context.js +205 -0
  188. package/src/verax/observe/observe-helpers.js +191 -0
  189. package/src/verax/observe/observe-runner.js +226 -0
  190. package/src/verax/observe/observers/budget-observer.js +185 -0
  191. package/src/verax/observe/observers/console-observer.js +102 -0
  192. package/src/verax/observe/observers/coverage-observer.js +107 -0
  193. package/src/verax/observe/observers/interaction-observer.js +471 -0
  194. package/src/verax/observe/observers/navigation-observer.js +132 -0
  195. package/src/verax/observe/observers/network-observer.js +87 -0
  196. package/src/verax/observe/observers/safety-observer.js +82 -0
  197. package/src/verax/observe/observers/ui-feedback-observer.js +99 -0
  198. package/src/verax/observe/settle.js +1 -0
  199. package/src/verax/observe/state-sensor.js +8 -4
  200. package/src/verax/observe/state-ui-sensor.js +7 -1
  201. package/src/verax/observe/traces-writer.js +27 -16
  202. package/src/verax/observe/ui-feedback-detector.js +742 -0
  203. package/src/verax/observe/ui-signal-sensor.js +155 -2
  204. package/src/verax/scan-summary-writer.js +46 -9
  205. package/src/verax/shared/artifact-manager.js +9 -6
  206. package/src/verax/shared/budget-profiles.js +2 -2
  207. package/src/verax/shared/caching.js +1 -1
  208. package/src/verax/shared/config-loader.js +1 -2
  209. package/src/verax/shared/css-spinner-rules.js +204 -0
  210. package/src/verax/shared/dynamic-route-utils.js +12 -6
  211. package/src/verax/shared/retry-policy.js +1 -6
  212. package/src/verax/shared/root-artifacts.js +1 -1
  213. package/src/verax/shared/view-switch-rules.js +208 -0
  214. package/src/verax/shared/zip-artifacts.js +1 -0
  215. package/src/verax/validate/context-validator.js +1 -1
  216. package/src/verax/observe/index.js.backup +0 -1
  217. package/src/verax/validate/context-validator.js.bak +0 -0
@@ -8,10 +8,12 @@
8
8
  * NO HEURISTICS. Only static, deterministic analysis.
9
9
  */
10
10
 
11
- import { normalizeTemplateLiteral } from '../shared/dynamic-route-utils.js';import { parse } from '@babel/parser';
11
+ import { normalizeTemplateLiteral } from '../shared/dynamic-route-utils.js';
12
+ import { parse } from '@babel/parser';
12
13
  import traverse from '@babel/traverse';
13
- import { readFileSync } from 'fs';
14
- import { resolve, relative, sep } from 'path';
14
+ import { readFileSync, existsSync } from 'fs';
15
+ import { relative, sep, dirname, resolve, extname } from 'path';
16
+ import { isViewSwitchFunction, isDetectableLiteralArg, VIEW_SWITCH_REASON_CODES } from '../shared/view-switch-rules.js';
15
17
 
16
18
  /**
17
19
  * Extract action contracts from a source file.
@@ -30,18 +32,41 @@ export function extractActionContracts(filePath, workspaceRoot) {
30
32
  }
31
33
  }
32
34
 
33
- function extractActionContractsFromCode(filePath, workspaceRoot, code, lineOffset = 0) {
35
+ export function extractActionContractsFromCode(filePath, workspaceRoot, code, lineOffset = 0) {
34
36
  const contracts = [];
35
37
 
36
38
  // Track function declarations and arrow function assignments with location
37
39
  const functionBodies = new Map(); // name -> { body, loc }
40
+
41
+ // Track imports for cross-file resolution
42
+ const importMap = new Map(); // localName -> { modulePath, exportName }
43
+ const fileDir = dirname(filePath);
38
44
 
39
45
  const ast = parse(code, {
40
46
  sourceType: 'unambiguous',
41
47
  plugins: ['jsx', 'typescript'],
42
48
  });
43
-
49
+
50
+ // First pass: collect imports and function definitions
44
51
  traverse.default(ast, {
52
+ ImportDeclaration(path) {
53
+ const source = path.node.source.value;
54
+ if (!source.startsWith('.') && !source.startsWith('/')) {
55
+ return; // Skip external packages
56
+ }
57
+
58
+ const resolvedPath = resolveModulePath(source, fileDir, workspaceRoot);
59
+ if (!resolvedPath) return;
60
+
61
+ path.node.specifiers.forEach(spec => {
62
+ if (spec.type === 'ImportSpecifier' || spec.type === 'ImportDefaultSpecifier') {
63
+ const localName = spec.local.name;
64
+ const exportName = spec.type === 'ImportDefaultSpecifier' ? 'default' : (spec.imported?.name || localName);
65
+ importMap.set(localName, { modulePath: resolvedPath, exportName });
66
+ }
67
+ });
68
+ },
69
+
45
70
  FunctionDeclaration(path) {
46
71
  if (path.node.id && path.node.id.name) {
47
72
  functionBodies.set(path.node.id.name, { body: path.node.body, loc: path.node.loc });
@@ -56,8 +81,11 @@ function extractActionContractsFromCode(filePath, workspaceRoot, code, lineOffse
56
81
  ) {
57
82
  functionBodies.set(path.node.id.name, { body: path.node.init.body, loc: path.node.loc });
58
83
  }
59
- },
84
+ }
85
+ });
60
86
 
87
+ // Second pass: extract contracts with cross-file support
88
+ traverse.default(ast, {
61
89
  // JSX handlers (React)
62
90
  JSXAttribute(path) {
63
91
  const attrName = path.node.name.name;
@@ -75,7 +103,8 @@ function extractActionContractsFromCode(filePath, workspaceRoot, code, lineOffse
75
103
  value.expression.type === 'ArrowFunctionExpression'
76
104
  ) {
77
105
  const handlerBody = value.expression.body;
78
- const networkCalls = findNetworkCallsInNode(handlerBody);
106
+ const networkCalls = findNetworkCallsInNode(handlerBody, functionBodies, importMap, filePath, workspaceRoot, 0);
107
+ const viewSwitchCalls = findViewSwitchCallsInNode(handlerBody, functionBodies, importMap, filePath, workspaceRoot, 0);
79
108
 
80
109
  for (const call of networkCalls) {
81
110
  const loc = path.node.loc;
@@ -92,52 +121,116 @@ function extractActionContractsFromCode(filePath, workspaceRoot, code, lineOffse
92
121
  selectorHint: null
93
122
  });
94
123
  } else if (call.kind !== 'VALIDATION_BLOCK') {
95
- contracts.push({
124
+ const contract = {
96
125
  kind: call.kind || 'NETWORK_ACTION',
97
126
  method: call.method,
98
127
  urlPath: call.url,
99
128
  source: sourceRef,
100
129
  elementType: path.parent.name.name,
101
130
  handlerRef: sourceRef
102
- });
131
+ };
132
+ if (call.isDynamic) {
133
+ contract.isDynamic = true;
134
+ contract.dynamicSegments = call.dynamicSegments;
135
+ if (call.astSnippet) contract.astSnippet = call.astSnippet;
136
+ }
137
+ contracts.push(contract);
103
138
  }
104
139
  }
140
+
141
+ // Extract view switch promises
142
+ for (const call of viewSwitchCalls) {
143
+ const loc = path.node.loc;
144
+ const sourceRef = formatSourceRef(filePath, workspaceRoot, loc, lineOffset);
145
+
146
+ contracts.push({
147
+ kind: 'VIEW_SWITCH_PROMISE',
148
+ target: call.target,
149
+ viewKind: call.viewKind,
150
+ pattern: call.pattern,
151
+ isUrlChanging: false,
152
+ source: sourceRef,
153
+ elementType: path.parent.name.name,
154
+ handlerRef: sourceRef,
155
+ astSnippet: call.astSnippet,
156
+ reasonCode: call.reasonCode,
157
+ confidenceHint: 'WEAK' // UI-bound but requires correlation
158
+ });
159
+ }
105
160
  } else if (
106
161
  value.type === 'JSXExpressionContainer' &&
107
162
  value.expression.type === 'Identifier'
108
163
  ) {
109
164
  const refName = value.expression.name;
110
165
  const handlerRecord = functionBodies.get(refName);
111
-
166
+
167
+ // Try local function first
168
+ let networkCalls = [];
169
+ let viewSwitchCalls = [];
112
170
  if (handlerRecord) {
113
- const networkCalls = findNetworkCallsInNode(handlerRecord.body);
114
-
115
- for (const call of networkCalls) {
116
- const loc = path.node.loc;
117
- const sourceRef = formatSourceRef(filePath, workspaceRoot, loc, lineOffset);
118
-
119
- if (call.kind === 'VALIDATION_BLOCK' && isSubmitHandler) {
120
- contracts.push({
121
- kind: 'VALIDATION_BLOCK',
122
- method: call.method,
123
- urlPath: null,
124
- source: sourceRef,
125
- elementType: path.parent.name.name,
126
- handlerRef: sourceRef,
127
- selectorHint: null
128
- });
129
- } else if (call.kind !== 'VALIDATION_BLOCK') {
130
- contracts.push({
131
- kind: call.kind || 'NETWORK_ACTION',
132
- method: call.method,
133
- urlPath: call.url,
134
- source: sourceRef,
135
- elementType: path.parent.name.name,
136
- handlerRef: sourceRef
137
- });
171
+ networkCalls = findNetworkCallsInNode(handlerRecord.body, functionBodies, importMap, filePath, workspaceRoot, 0);
172
+ viewSwitchCalls = findViewSwitchCallsInNode(handlerRecord.body, functionBodies, importMap, filePath, workspaceRoot, 0);
173
+ } else {
174
+ // Try cross-file resolution
175
+ const importInfo = importMap.get(refName);
176
+ if (importInfo) {
177
+ const crossFileCalls = followCrossFileFunction(importInfo, workspaceRoot, 0);
178
+ networkCalls = crossFileCalls.filter(c => c.kind !== 'VIEW_SWITCH_PROMISE');
179
+ viewSwitchCalls = crossFileCalls.filter(c => c.kind === 'VIEW_SWITCH_PROMISE');
180
+ }
181
+ }
182
+
183
+ for (const call of networkCalls) {
184
+ const loc = path.node.loc;
185
+ const sourceRef = formatSourceRef(filePath, workspaceRoot, loc, lineOffset);
186
+
187
+ if (call.kind === 'VALIDATION_BLOCK' && isSubmitHandler) {
188
+ contracts.push({
189
+ kind: 'VALIDATION_BLOCK',
190
+ method: call.method,
191
+ urlPath: null,
192
+ source: sourceRef,
193
+ elementType: path.parent.name.name,
194
+ handlerRef: call.handlerRef || sourceRef,
195
+ selectorHint: null
196
+ });
197
+ } else if (call.kind !== 'VALIDATION_BLOCK') {
198
+ const contract = {
199
+ kind: call.kind || 'NETWORK_ACTION',
200
+ method: call.method,
201
+ urlPath: call.url,
202
+ source: sourceRef,
203
+ elementType: path.parent.name.name,
204
+ handlerRef: call.handlerRef || sourceRef
205
+ };
206
+ if (call.isDynamic) {
207
+ contract.isDynamic = true;
208
+ contract.dynamicSegments = call.dynamicSegments;
209
+ if (call.astSnippet) contract.astSnippet = call.astSnippet;
138
210
  }
211
+ contracts.push(contract);
139
212
  }
140
213
  }
214
+
215
+ // Extract view switch promises
216
+ for (const call of viewSwitchCalls) {
217
+ const loc = path.node.loc;
218
+ const sourceRef = formatSourceRef(filePath, workspaceRoot, loc, lineOffset);
219
+
220
+ contracts.push({
221
+ kind: 'VIEW_SWITCH_PROMISE',
222
+ target: call.target,
223
+ viewKind: call.viewKind,
224
+ pattern: call.pattern,
225
+ isUrlChanging: false,
226
+ source: sourceRef,
227
+ elementType: path.parent.name.name,
228
+ handlerRef: sourceRef,
229
+ astSnippet: call.astSnippet,
230
+ reasonCode: call.reasonCode,
231
+ confidenceHint: 'WEAK'
232
+ });
233
+ }
141
234
  }
142
235
  },
143
236
 
@@ -167,12 +260,68 @@ function extractActionContractsFromCode(filePath, workspaceRoot, code, lineOffse
167
260
  if (record) {
168
261
  handlerBody = record.body;
169
262
  handlerLoc = record.loc || path.node.loc;
263
+ } else {
264
+ // Try cross-file resolution
265
+ const importInfo = importMap.get(handlerArg.name);
266
+ if (importInfo) {
267
+ const crossFileCalls = followCrossFileFunction(importInfo, workspaceRoot, 0);
268
+ if (crossFileCalls.length > 0) {
269
+ handlerBody = { type: 'BlockStatement', body: [] }; // Dummy body for location
270
+ handlerLoc = path.node.loc;
271
+ const handlerRef = formatSourceRef(filePath, workspaceRoot, handlerLoc, lineOffset);
272
+
273
+ for (const call of crossFileCalls) {
274
+ if (call.kind === 'VALIDATION_BLOCK' && isSubmitEvent) {
275
+ contracts.push({
276
+ kind: 'VALIDATION_BLOCK',
277
+ method: call.method,
278
+ urlPath: null,
279
+ source: handlerRef,
280
+ handlerRef: call.handlerRef || `${handlerRef}#${eventType}`,
281
+ elementType: 'dom',
282
+ selectorHint: null
283
+ });
284
+ } else if (call.kind === 'VIEW_SWITCH_PROMISE') {
285
+ contracts.push({
286
+ kind: 'VIEW_SWITCH_PROMISE',
287
+ target: call.target,
288
+ viewKind: call.viewKind,
289
+ pattern: call.pattern,
290
+ isUrlChanging: false,
291
+ source: handlerRef,
292
+ handlerRef: call.handlerRef || `${handlerRef}#${eventType}`,
293
+ elementType: 'dom',
294
+ astSnippet: call.astSnippet,
295
+ reasonCode: call.reasonCode,
296
+ confidenceHint: 'WEAK'
297
+ });
298
+ } else if (call.kind !== 'VALIDATION_BLOCK') {
299
+ const contract = {
300
+ kind: call.kind || 'NETWORK_ACTION',
301
+ method: call.method,
302
+ urlPath: call.url,
303
+ source: handlerRef,
304
+ handlerRef: call.handlerRef || `${handlerRef}#${eventType}`,
305
+ elementType: 'dom'
306
+ };
307
+ if (call.isDynamic) {
308
+ contract.isDynamic = true;
309
+ contract.dynamicSegments = call.dynamicSegments;
310
+ if (call.astSnippet) contract.astSnippet = call.astSnippet;
311
+ }
312
+ contracts.push(contract);
313
+ }
314
+ }
315
+ return; // Already processed
316
+ }
317
+ }
170
318
  }
171
319
  }
172
320
 
173
321
  if (!handlerBody) return;
174
322
 
175
- const networkCalls = findNetworkCallsInNode(handlerBody);
323
+ const networkCalls = findNetworkCallsInNode(handlerBody, functionBodies, importMap, filePath, workspaceRoot, 0);
324
+ const viewSwitchCalls = findViewSwitchCallsInNode(handlerBody, functionBodies, importMap, filePath, workspaceRoot, 0);
176
325
  const isSubmitEvent = eventType === 'submit';
177
326
 
178
327
  for (const call of networkCalls) {
@@ -189,14 +338,20 @@ function extractActionContractsFromCode(filePath, workspaceRoot, code, lineOffse
189
338
  selectorHint: null
190
339
  });
191
340
  } else if (call.kind !== 'VALIDATION_BLOCK') {
192
- contracts.push({
341
+ const contract = {
193
342
  kind: call.kind || 'NETWORK_ACTION',
194
343
  method: call.method,
195
344
  urlPath: call.url,
196
345
  source: handlerRef,
197
346
  handlerRef: `${handlerRef}#${eventType}`,
198
347
  elementType: 'dom'
199
- });
348
+ };
349
+ if (call.isDynamic) {
350
+ contract.isDynamic = true;
351
+ contract.dynamicSegments = call.dynamicSegments;
352
+ if (call.astSnippet) contract.astSnippet = call.astSnippet;
353
+ }
354
+ contracts.push(contract);
200
355
  }
201
356
  }
202
357
  }
@@ -209,22 +364,37 @@ function extractActionContractsFromCode(filePath, workspaceRoot, code, lineOffse
209
364
  /**
210
365
  * Extract template literal pattern from node.
211
366
  * Returns null if template has complex expressions.
367
+ *
368
+ * TRUTH BOUNDARY:
369
+ * - Detectable: Template literals with simple Identifier expressions
370
+ * - `/api/users/${id}` → `/api/users/:id`
371
+ * - `/api/${resource}/${id}` → `/api/:resource/:id`
372
+ * - Ambiguous/Not detectable: Complex expressions (function calls, member access, etc.)
373
+ * - `/api/${getPath()}` → null (rejected)
374
+ * - `/api/${obj.prop}` → null (rejected)
375
+ *
376
+ * @param {Object} node - AST TemplateLiteral node
377
+ * @returns {Object|null} - { templateStr, normalizedUrl, dynamicSegments, astSnippet } or null
212
378
  */
213
379
  function extractTemplateLiteralPath(node) {
214
380
  if (!node || node.type !== 'TemplateLiteral') {
215
381
  return null;
216
382
  }
217
383
 
218
- // Build template string
384
+ // Build template string with ${param} placeholders
219
385
  let templateStr = node.quasis[0]?.value?.cooked || '';
386
+ const dynamicSegments = [];
220
387
 
221
388
  for (let i = 0; i < node.expressions.length; i++) {
222
389
  const expr = node.expressions[i];
223
390
 
224
- // Only support simple identifiers
391
+ // Only support simple identifiers (truth boundary)
225
392
  if (expr.type === 'Identifier') {
226
- templateStr += '${' + expr.name + '}';
393
+ const paramName = expr.name;
394
+ templateStr += '${' + paramName + '}';
395
+ dynamicSegments.push(paramName);
227
396
  } else {
397
+ // Complex expression - reject (truth boundary)
228
398
  return null;
229
399
  }
230
400
 
@@ -233,7 +403,150 @@ function extractTemplateLiteralPath(node) {
233
403
  }
234
404
  }
235
405
 
236
- return templateStr;
406
+ // Normalize to :param format (not example values)
407
+ let normalizedUrl = templateStr;
408
+ for (const paramName of dynamicSegments) {
409
+ normalizedUrl = normalizedUrl.replace(`\${${paramName}}`, `:${paramName}`);
410
+ }
411
+
412
+ // Preserve AST snippet for evidence
413
+ const astSnippet = {
414
+ type: 'TemplateLiteral',
415
+ quasis: node.quasis.map(q => ({ value: q.value?.cooked || '' })),
416
+ expressions: node.expressions.map(e =>
417
+ e.type === 'Identifier' ? { type: 'Identifier', name: e.name } : null
418
+ ).filter(Boolean)
419
+ };
420
+
421
+ return {
422
+ templateStr,
423
+ normalizedUrl,
424
+ dynamicSegments,
425
+ astSnippet
426
+ };
427
+ }
428
+
429
+ /**
430
+ * Resolve module path from import specifier
431
+ */
432
+ function resolveModulePath(specifier, fromDir, workspaceRoot) {
433
+ if (!specifier.startsWith('.') && !specifier.startsWith('/')) {
434
+ return null; // External package
435
+ }
436
+
437
+ let resolved = resolve(fromDir, specifier);
438
+
439
+ // Try with extensions
440
+ const extensions = ['.js', '.jsx', '.ts', '.tsx', ''];
441
+ for (const ext of extensions) {
442
+ const withExt = ext ? resolved + ext : resolved;
443
+ if (existsSync(withExt)) {
444
+ return withExt;
445
+ }
446
+ // Try index files
447
+ const indexPath = resolve(withExt, 'index' + ext);
448
+ if (existsSync(indexPath)) {
449
+ return indexPath;
450
+ }
451
+ }
452
+
453
+ return null;
454
+ }
455
+
456
+ /**
457
+ * Follow function call across files
458
+ */
459
+ function followCrossFileFunction(importInfo, workspaceRoot, depth) {
460
+ if (depth > 3) return []; // Prevent infinite recursion
461
+
462
+ const { modulePath, exportName } = importInfo;
463
+ if (!existsSync(modulePath)) return [];
464
+
465
+ try {
466
+ const code = readFileSync(modulePath, 'utf-8');
467
+ const fileDir = dirname(modulePath);
468
+ const ast = parse(code, {
469
+ sourceType: 'unambiguous',
470
+ plugins: ['jsx', 'typescript'],
471
+ });
472
+
473
+ const functionBodies = new Map();
474
+ const importMap = new Map();
475
+
476
+ // Collect function definitions and imports
477
+ traverse.default(ast, {
478
+ ImportDeclaration(path) {
479
+ const source = path.node.source.value;
480
+ if (!source.startsWith('.') && !source.startsWith('/')) return;
481
+ const resolved = resolveModulePath(source, fileDir, workspaceRoot);
482
+ if (!resolved) return;
483
+ path.node.specifiers.forEach(spec => {
484
+ if (spec.type === 'ImportSpecifier' || spec.type === 'ImportDefaultSpecifier') {
485
+ const localName = spec.local.name;
486
+ const expName = spec.type === 'ImportDefaultSpecifier' ? 'default' : (spec.imported?.name || localName);
487
+ importMap.set(localName, { modulePath: resolved, exportName: expName });
488
+ }
489
+ });
490
+ },
491
+ FunctionDeclaration(path) {
492
+ if (path.node.id && path.node.id.name) {
493
+ functionBodies.set(path.node.id.name, { body: path.node.body, loc: path.node.loc });
494
+ }
495
+ },
496
+ VariableDeclarator(path) {
497
+ if (path.node.id.type === 'Identifier' && path.node.init &&
498
+ (path.node.init.type === 'ArrowFunctionExpression' || path.node.init.type === 'FunctionExpression')) {
499
+ functionBodies.set(path.node.id.name, { body: path.node.init.body, loc: path.node.loc });
500
+ }
501
+ },
502
+ ExportNamedDeclaration(path) {
503
+ if (exportName === 'default') return;
504
+ const decl = path.node.declaration;
505
+ if (decl && decl.type === 'FunctionDeclaration' && decl.id && decl.id.name === exportName) {
506
+ functionBodies.set(exportName, { body: decl.body, loc: decl.loc });
507
+ }
508
+ },
509
+ ExportDefaultDeclaration(path) {
510
+ if (exportName !== 'default') return;
511
+ const decl = path.node.declaration;
512
+ if (decl && decl.type === 'FunctionDeclaration' && decl.id) {
513
+ functionBodies.set(decl.id.name, { body: decl.body, loc: decl.loc });
514
+ } else if (decl && (decl.type === 'ArrowFunctionExpression' || decl.type === 'FunctionExpression')) {
515
+ // Anonymous default export - scan directly
516
+ const calls = findNetworkCallsInNode(decl.body, functionBodies, importMap, modulePath, workspaceRoot, depth + 1);
517
+ return calls.map(call => ({
518
+ ...call,
519
+ handlerRef: formatSourceRef(modulePath, workspaceRoot, decl.loc || { start: { line: 1, column: 0 } }, 0)
520
+ }));
521
+ }
522
+ }
523
+ });
524
+
525
+ // Find the exported function
526
+ const funcDef = functionBodies.get(exportName);
527
+ if (funcDef) {
528
+ const networkCalls = findNetworkCallsInNode(funcDef.body, functionBodies, importMap, modulePath, workspaceRoot, depth + 1);
529
+ const viewSwitchCalls = findViewSwitchCallsInNode(funcDef.body, functionBodies, importMap, modulePath, workspaceRoot, depth + 1);
530
+ const handlerRef = formatSourceRef(modulePath, workspaceRoot, funcDef.loc || { start: { line: 1, column: 0 } }, 0);
531
+
532
+ const allCalls = [
533
+ ...networkCalls.map(call => ({
534
+ ...call,
535
+ handlerRef
536
+ })),
537
+ ...viewSwitchCalls.map(call => ({
538
+ ...call,
539
+ handlerRef
540
+ }))
541
+ ];
542
+
543
+ return allCalls;
544
+ }
545
+ } catch (err) {
546
+ // Ignore parse errors
547
+ }
548
+
549
+ return [];
237
550
  }
238
551
 
239
552
  /**
@@ -241,9 +554,14 @@ function extractTemplateLiteralPath(node) {
241
554
  * Only returns calls with static URL/path literals or template patterns.
242
555
  *
243
556
  * @param {Object} node - AST node to scan
557
+ * @param {Map} functionBodies - Map of function name -> body for local functions
558
+ * @param {Map} importMap - Map of import name -> module info
559
+ * @param {string} filePath - Current file path
560
+ * @param {string} workspaceRoot - Workspace root
561
+ * @param {number} depth - Recursion depth
244
562
  * @returns {Array<Object>} - Array of {kind, method, url} where kind is 'NETWORK_ACTION' or 'NAVIGATION_ACTION'
245
563
  */
246
- function findNetworkCallsInNode(node) {
564
+ function findNetworkCallsInNode(node, functionBodies = new Map(), importMap = new Map(), filePath = '', workspaceRoot = '', depth = 0) {
247
565
  const calls = [];
248
566
 
249
567
  // Track if we found preventDefault or return false for validation block detection
@@ -257,27 +575,57 @@ function findNetworkCallsInNode(node) {
257
575
  // Check if this is a CallExpression
258
576
  if (n.type === 'CallExpression') {
259
577
  const callee = n.callee;
578
+
579
+ // Follow function calls to their definitions (cross-file support)
580
+ if (callee.type === 'Identifier' && depth < 3) {
581
+ const funcName = callee.name;
582
+ const localFunc = functionBodies.get(funcName);
583
+ if (localFunc) {
584
+ // Recursively scan local function
585
+ const innerCalls = findNetworkCallsInNode(localFunc.body, functionBodies, importMap, filePath, workspaceRoot, depth + 1);
586
+ calls.push(...innerCalls);
587
+ } else {
588
+ // Try cross-file resolution
589
+ const importInfo = importMap.get(funcName);
590
+ if (importInfo) {
591
+ const crossFileCalls = followCrossFileFunction(importInfo, workspaceRoot, depth);
592
+ calls.push(...crossFileCalls);
593
+ }
594
+ }
595
+ }
260
596
 
261
597
  // Case 1: fetch(url, options)
262
598
  if (callee.type === 'Identifier' && callee.name === 'fetch') {
263
599
  const urlArg = n.arguments[0];
264
600
  const optionsArg = n.arguments[1];
265
601
 
266
- // Only accept static string literals
267
- if (urlArg && urlArg.type === 'StringLiteral') {
268
- let method = 'GET'; // default
269
-
270
- // Try to extract method from options
271
- if (optionsArg && optionsArg.type === 'ObjectExpression') {
272
- const methodProp = optionsArg.properties.find(
273
- (p) => p.key && p.key.name === 'method'
274
- );
275
- if (methodProp && methodProp.value.type === 'StringLiteral') {
276
- method = methodProp.value.value.toUpperCase();
277
- }
602
+ let method = 'GET'; // default
603
+ // Try to extract method from options
604
+ if (optionsArg && optionsArg.type === 'ObjectExpression') {
605
+ const methodProp = optionsArg.properties.find(
606
+ (p) => p.key && p.key.name === 'method'
607
+ );
608
+ if (methodProp && methodProp.value.type === 'StringLiteral') {
609
+ method = methodProp.value.value.toUpperCase();
278
610
  }
611
+ }
279
612
 
613
+ if (urlArg && urlArg.type === 'StringLiteral') {
614
+ // Static URL
280
615
  calls.push({ kind: 'NETWORK_ACTION', method, url: urlArg.value });
616
+ } else if (urlArg && urlArg.type === 'TemplateLiteral') {
617
+ // Dynamic URL: fetch(`/api/users/${id}`)
618
+ const templateInfo = extractTemplateLiteralPath(urlArg);
619
+ if (templateInfo && templateInfo.normalizedUrl.startsWith('/')) {
620
+ calls.push({
621
+ kind: 'NETWORK_ACTION',
622
+ method,
623
+ url: templateInfo.normalizedUrl,
624
+ isDynamic: true,
625
+ dynamicSegments: templateInfo.dynamicSegments.length,
626
+ astSnippet: templateInfo.astSnippet
627
+ });
628
+ }
281
629
  }
282
630
  }
283
631
 
@@ -292,7 +640,57 @@ function findNetworkCallsInNode(node) {
292
640
  const urlArg = n.arguments[0];
293
641
 
294
642
  if (urlArg && urlArg.type === 'StringLiteral') {
643
+ // Static URL
295
644
  calls.push({ kind: 'NETWORK_ACTION', method: methodName, url: urlArg.value });
645
+ } else if (urlArg && urlArg.type === 'TemplateLiteral') {
646
+ // Dynamic URL: axios.post(`/api/users/${id}`)
647
+ const templateInfo = extractTemplateLiteralPath(urlArg);
648
+ if (templateInfo && templateInfo.normalizedUrl.startsWith('/')) {
649
+ calls.push({
650
+ kind: 'NETWORK_ACTION',
651
+ method: methodName,
652
+ url: templateInfo.normalizedUrl,
653
+ isDynamic: true,
654
+ dynamicSegments: templateInfo.dynamicSegments.length,
655
+ astSnippet: templateInfo.astSnippet
656
+ });
657
+ }
658
+ }
659
+ }
660
+
661
+ // Case 2b: apiClient.get(url), apiClient.post(url), http.get(url), etc. (wrapped API clients)
662
+ // Check this only if not axios (already handled above)
663
+ if (
664
+ callee.type === 'MemberExpression' &&
665
+ callee.object.type === 'Identifier' &&
666
+ callee.object.name !== 'axios' &&
667
+ callee.property.type === 'Identifier'
668
+ ) {
669
+ const objectName = callee.object.name;
670
+ const methodName = callee.property.name.toUpperCase();
671
+ const urlArg = n.arguments[0];
672
+
673
+ // Common API client patterns (exclude axios which is handled above)
674
+ const apiClientNames = ['apiClient', 'http', 'request', 'api', 'client', 'httpClient'];
675
+ const httpMethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'];
676
+ if (apiClientNames.includes(objectName) && httpMethods.includes(methodName)) {
677
+ if (urlArg && urlArg.type === 'StringLiteral') {
678
+ // Static URL
679
+ calls.push({ kind: 'NETWORK_ACTION', method: methodName, url: urlArg.value });
680
+ } else if (urlArg && urlArg.type === 'TemplateLiteral') {
681
+ // Dynamic URL: apiClient.post(`/api/users/${id}`)
682
+ const templateInfo = extractTemplateLiteralPath(urlArg);
683
+ if (templateInfo && templateInfo.normalizedUrl.startsWith('/')) {
684
+ calls.push({
685
+ kind: 'NETWORK_ACTION',
686
+ method: methodName,
687
+ url: templateInfo.normalizedUrl,
688
+ isDynamic: true,
689
+ dynamicSegments: templateInfo.dynamicSegments.length,
690
+ astSnippet: templateInfo.astSnippet
691
+ });
692
+ }
693
+ }
296
694
  }
297
695
  }
298
696
 
@@ -306,15 +704,30 @@ function findNetworkCallsInNode(node) {
306
704
  const methodArg = n.arguments[0];
307
705
  const urlArg = n.arguments[1];
308
706
 
309
- if (
310
- methodArg && methodArg.type === 'StringLiteral' &&
311
- urlArg && urlArg.type === 'StringLiteral'
312
- ) {
313
- calls.push({
314
- kind: 'NETWORK_ACTION',
315
- method: methodArg.value.toUpperCase(),
316
- url: urlArg.value,
317
- });
707
+ if (methodArg && methodArg.type === 'StringLiteral') {
708
+ const method = methodArg.value.toUpperCase();
709
+
710
+ if (urlArg && urlArg.type === 'StringLiteral') {
711
+ // Static URL
712
+ calls.push({
713
+ kind: 'NETWORK_ACTION',
714
+ method,
715
+ url: urlArg.value,
716
+ });
717
+ } else if (urlArg && urlArg.type === 'TemplateLiteral') {
718
+ // Dynamic URL: xhr.open('GET', `/api/users/${id}`)
719
+ const templateInfo = extractTemplateLiteralPath(urlArg);
720
+ if (templateInfo && templateInfo.normalizedUrl.startsWith('/')) {
721
+ calls.push({
722
+ kind: 'NETWORK_ACTION',
723
+ method,
724
+ url: templateInfo.normalizedUrl,
725
+ isDynamic: true,
726
+ dynamicSegments: templateInfo.dynamicSegments.length,
727
+ astSnippet: templateInfo.astSnippet
728
+ });
729
+ }
730
+ }
318
731
  }
319
732
  }
320
733
 
@@ -394,7 +807,7 @@ function findNetworkCallsInNode(node) {
394
807
 
395
808
  // Recursively scan all properties
396
809
  for (const key in n) {
397
- if (n.hasOwnProperty(key)) {
810
+ if (Object.prototype.hasOwnProperty.call(n, key)) {
398
811
  const value = n[key];
399
812
  if (Array.isArray(value)) {
400
813
  value.forEach(item => scan(item));
@@ -419,6 +832,211 @@ function findNetworkCallsInNode(node) {
419
832
  return calls;
420
833
  }
421
834
 
835
+ /**
836
+ * Find view switch promise calls (setView, dispatch(NAVIGATE), etc.) in an AST node.
837
+ * TRUTH BOUNDARY: Only literal string/number arguments are accepted.
838
+ *
839
+ * @param {Object} node - AST node to scan
840
+ * @param {Map} functionBodies - Map of function name -> body for local functions
841
+ * @param {Map} importMap - Map of import name -> module info
842
+ * @param {string} filePath - Current file path
843
+ * @param {string} workspaceRoot - Workspace root
844
+ * @param {number} depth - Recursion depth
845
+ * @returns {Array<Object>} - Array of {kind: 'VIEW_SWITCH_PROMISE', target, kind, isUrlChanging, astSnippet}
846
+ */
847
+ function findViewSwitchCallsInNode(node, functionBodies = new Map(), importMap = new Map(), filePath = '', workspaceRoot = '', depth = 0) {
848
+ const calls = [];
849
+
850
+ function scan(n) {
851
+ if (!n || typeof n !== 'object') return;
852
+
853
+ if (n.type === 'CallExpression') {
854
+ const callee = n.callee;
855
+
856
+ // Follow function calls to their definitions (cross-file support)
857
+ if (callee.type === 'Identifier' && depth < 3) {
858
+ const funcName = callee.name;
859
+ const localFunc = functionBodies.get(funcName);
860
+ if (localFunc) {
861
+ const innerCalls = findViewSwitchCallsInNode(localFunc.body, functionBodies, importMap, filePath, workspaceRoot, depth + 1);
862
+ calls.push(...innerCalls);
863
+ } else {
864
+ const importInfo = importMap.get(funcName);
865
+ if (importInfo) {
866
+ const crossFileCalls = followCrossFileFunction(importInfo, workspaceRoot, depth);
867
+ // Filter for view switch calls only
868
+ const viewSwitchCalls = crossFileCalls.filter(c => c.kind === 'VIEW_SWITCH_PROMISE');
869
+ calls.push(...viewSwitchCalls);
870
+ }
871
+ }
872
+ }
873
+
874
+ // Case 1: React setState patterns: setView('settings'), setTab('billing')
875
+ if (callee.type === 'Identifier') {
876
+ const funcName = callee.name;
877
+ const viewSwitchInfo = isViewSwitchFunction(funcName);
878
+
879
+ if (viewSwitchInfo) {
880
+ const firstArg = n.arguments[0];
881
+ const literalArg = isDetectableLiteralArg(firstArg);
882
+
883
+ if (literalArg && literalArg.value) {
884
+ // Preserve AST snippet for evidence
885
+ const astSnippet = {
886
+ type: 'CallExpression',
887
+ callee: { type: 'Identifier', name: funcName },
888
+ arguments: [{
889
+ type: firstArg.type,
890
+ value: firstArg.type === 'StringLiteral' ? firstArg.value : firstArg.value
891
+ }]
892
+ };
893
+
894
+ calls.push({
895
+ kind: 'VIEW_SWITCH_PROMISE',
896
+ target: literalArg.value,
897
+ viewKind: viewSwitchInfo.kind,
898
+ pattern: viewSwitchInfo.pattern,
899
+ isUrlChanging: false,
900
+ astSnippet,
901
+ reasonCode: literalArg.reasonCode
902
+ });
903
+ }
904
+ }
905
+ }
906
+
907
+ // Case 2: Redux dispatch({type: 'NAVIGATE', to: 'profile'}) or dispatch(navigate('profile'))
908
+ if (callee.type === 'Identifier' && callee.name === 'dispatch') {
909
+ const firstArg = n.arguments[0];
910
+
911
+ // dispatch({type: 'NAVIGATE', to: 'profile'})
912
+ if (firstArg && firstArg.type === 'ObjectExpression') {
913
+ const typeProp = firstArg.properties.find(p =>
914
+ p.key && p.key.type === 'Identifier' && p.key.name === 'type'
915
+ );
916
+ const toProp = firstArg.properties.find(p =>
917
+ p.key && (p.key.name === 'to' || p.key.name === 'target' || p.key.name === 'view')
918
+ );
919
+
920
+ if (typeProp && typeProp.value && typeProp.value.type === 'StringLiteral') {
921
+ const actionType = typeProp.value.value;
922
+ const viewSwitchInfo = isViewSwitchFunction(actionType);
923
+
924
+ if (viewSwitchInfo && toProp && toProp.value) {
925
+ const literalArg = isDetectableLiteralArg(toProp.value);
926
+
927
+ if (literalArg && literalArg.value) {
928
+ const astSnippet = {
929
+ type: 'CallExpression',
930
+ callee: { type: 'Identifier', name: 'dispatch' },
931
+ arguments: [{
932
+ type: 'ObjectExpression',
933
+ properties: [
934
+ { key: 'type', value: actionType },
935
+ { key: toProp.key.name, value: literalArg.value }
936
+ ]
937
+ }]
938
+ };
939
+
940
+ calls.push({
941
+ kind: 'VIEW_SWITCH_PROMISE',
942
+ target: literalArg.value,
943
+ viewKind: viewSwitchInfo.kind,
944
+ pattern: 'redux',
945
+ isUrlChanging: false,
946
+ astSnippet,
947
+ reasonCode: literalArg.reasonCode
948
+ });
949
+ }
950
+ }
951
+ }
952
+ }
953
+
954
+ // dispatch(navigate('profile')) - action creator call
955
+ if (firstArg && firstArg.type === 'CallExpression') {
956
+ const actionCallee = firstArg.callee;
957
+ if (actionCallee.type === 'Identifier') {
958
+ const actionName = actionCallee.name;
959
+ const viewSwitchInfo = isViewSwitchFunction(actionName);
960
+
961
+ if (viewSwitchInfo) {
962
+ const actionFirstArg = firstArg.arguments[0];
963
+ const literalArg = isDetectableLiteralArg(actionFirstArg);
964
+
965
+ if (literalArg && literalArg.value) {
966
+ const astSnippet = {
967
+ type: 'CallExpression',
968
+ callee: { type: 'Identifier', name: 'dispatch' },
969
+ arguments: [{
970
+ type: 'CallExpression',
971
+ callee: { type: 'Identifier', name: actionName },
972
+ arguments: [{ type: actionFirstArg.type, value: literalArg.value }]
973
+ }]
974
+ };
975
+
976
+ calls.push({
977
+ kind: 'VIEW_SWITCH_PROMISE',
978
+ target: literalArg.value,
979
+ viewKind: viewSwitchInfo.kind,
980
+ pattern: 'redux',
981
+ isUrlChanging: false,
982
+ astSnippet,
983
+ reasonCode: literalArg.reasonCode
984
+ });
985
+ }
986
+ }
987
+ }
988
+ }
989
+ }
990
+
991
+ // Case 3: Generic function calls: showModal('login'), openDrawer('cart')
992
+ if (callee.type === 'Identifier') {
993
+ const funcName = callee.name;
994
+ const viewSwitchInfo = isViewSwitchFunction(funcName);
995
+
996
+ if (viewSwitchInfo && viewSwitchInfo.pattern === 'generic') {
997
+ const firstArg = n.arguments[0];
998
+ const literalArg = isDetectableLiteralArg(firstArg);
999
+
1000
+ if (literalArg && literalArg.value) {
1001
+ const astSnippet = {
1002
+ type: 'CallExpression',
1003
+ callee: { type: 'Identifier', name: funcName },
1004
+ arguments: [{
1005
+ type: firstArg.type,
1006
+ value: literalArg.value
1007
+ }]
1008
+ };
1009
+
1010
+ calls.push({
1011
+ kind: 'VIEW_SWITCH_PROMISE',
1012
+ target: literalArg.value,
1013
+ viewKind: viewSwitchInfo.kind,
1014
+ pattern: 'generic',
1015
+ isUrlChanging: false,
1016
+ astSnippet,
1017
+ reasonCode: literalArg.reasonCode
1018
+ });
1019
+ }
1020
+ }
1021
+ }
1022
+ }
1023
+
1024
+ // Recursively scan child nodes
1025
+ for (const key in n) {
1026
+ if (key === 'parent' || key === 'leadingComments' || key === 'trailingComments') continue;
1027
+ const child = n[key];
1028
+ if (Array.isArray(child)) {
1029
+ child.forEach(item => scan(item));
1030
+ } else if (child && typeof child === 'object') {
1031
+ scan(child);
1032
+ }
1033
+ }
1034
+ }
1035
+
1036
+ scan(node);
1037
+ return calls;
1038
+ }
1039
+
422
1040
  /**
423
1041
  * Format source reference as "file:line:col"
424
1042
  * Normalizes Windows paths to use forward slashes.
@@ -445,7 +1063,7 @@ function formatSourceRef(filePath, workspaceRoot, loc, lineOffset = 0) {
445
1063
  *
446
1064
  * @param {string} rootPath - Root directory to scan
447
1065
  * @param {string} workspaceRoot - Workspace root
448
- * @returns {Array<Object>} - All contracts found
1066
+ * @returns {Promise<Array<Object>>} - All contracts found
449
1067
  */
450
1068
  export async function scanForContracts(rootPath, workspaceRoot) {
451
1069
  const contracts = [];