@veraxhq/verax 0.3.0 → 0.4.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 (191) hide show
  1. package/README.md +28 -20
  2. package/bin/verax.js +11 -18
  3. package/package.json +28 -7
  4. package/src/cli/commands/baseline.js +1 -2
  5. package/src/cli/commands/default.js +72 -81
  6. package/src/cli/commands/doctor.js +29 -0
  7. package/src/cli/commands/ga.js +3 -0
  8. package/src/cli/commands/gates.js +1 -1
  9. package/src/cli/commands/inspect.js +6 -133
  10. package/src/cli/commands/release-check.js +2 -0
  11. package/src/cli/commands/run.js +74 -246
  12. package/src/cli/commands/security-check.js +2 -1
  13. package/src/cli/commands/truth.js +0 -1
  14. package/src/cli/entry.js +82 -309
  15. package/src/cli/util/angular-component-extractor.js +2 -2
  16. package/src/cli/util/angular-navigation-detector.js +2 -2
  17. package/src/cli/util/ast-interactive-detector.js +4 -6
  18. package/src/cli/util/ast-network-detector.js +3 -3
  19. package/src/cli/util/ast-promise-extractor.js +581 -0
  20. package/src/cli/util/ast-usestate-detector.js +3 -3
  21. package/src/cli/util/atomic-write.js +12 -1
  22. package/src/cli/util/console-reporter.js +72 -0
  23. package/src/cli/util/detection-engine.js +105 -41
  24. package/src/cli/util/determinism-runner.js +2 -1
  25. package/src/cli/util/determinism-writer.js +1 -1
  26. package/src/cli/util/digest-engine.js +359 -0
  27. package/src/cli/util/dom-diff.js +226 -0
  28. package/src/cli/util/env-url.js +0 -4
  29. package/src/cli/util/evidence-engine.js +287 -0
  30. package/src/cli/util/expectation-extractor.js +217 -367
  31. package/src/cli/util/findings-writer.js +19 -126
  32. package/src/cli/util/framework-detector.js +572 -0
  33. package/src/cli/util/idgen.js +1 -1
  34. package/src/cli/util/interaction-planner.js +529 -0
  35. package/src/cli/util/learn-writer.js +2 -2
  36. package/src/cli/util/ledger-writer.js +110 -0
  37. package/src/cli/util/monorepo-resolver.js +162 -0
  38. package/src/cli/util/observation-engine.js +127 -278
  39. package/src/cli/util/observe-writer.js +2 -2
  40. package/src/cli/util/paths.js +12 -3
  41. package/src/cli/util/project-discovery.js +284 -3
  42. package/src/cli/util/project-writer.js +2 -2
  43. package/src/cli/util/run-id.js +23 -27
  44. package/src/cli/util/run-result.js +778 -0
  45. package/src/cli/util/selector-resolver.js +235 -0
  46. package/src/cli/util/summary-writer.js +2 -1
  47. package/src/cli/util/svelte-navigation-detector.js +3 -3
  48. package/src/cli/util/svelte-sfc-extractor.js +0 -1
  49. package/src/cli/util/svelte-state-detector.js +1 -2
  50. package/src/cli/util/trust-activation-integration.js +496 -0
  51. package/src/cli/util/trust-activation-wrapper.js +85 -0
  52. package/src/cli/util/trust-integration-hooks.js +164 -0
  53. package/src/cli/util/types.js +153 -0
  54. package/src/cli/util/url-validation.js +40 -0
  55. package/src/cli/util/vue-navigation-detector.js +4 -3
  56. package/src/cli/util/vue-sfc-extractor.js +1 -2
  57. package/src/cli/util/vue-state-detector.js +1 -1
  58. package/src/types/fs-augment.d.ts +23 -0
  59. package/src/types/global.d.ts +137 -0
  60. package/src/types/internal-types.d.ts +35 -0
  61. package/src/verax/cli/finding-explainer.js +3 -56
  62. package/src/verax/cli/init.js +4 -18
  63. package/src/verax/core/action-classifier.js +4 -3
  64. package/src/verax/core/artifacts/registry.js +0 -15
  65. package/src/verax/core/artifacts/verifier.js +18 -8
  66. package/src/verax/core/baseline/baseline.snapshot.js +2 -0
  67. package/src/verax/core/capabilities/gates.js +7 -1
  68. package/src/verax/core/confidence/confidence-compute.js +14 -7
  69. package/src/verax/core/confidence/confidence.loader.js +1 -0
  70. package/src/verax/core/confidence-engine-refactor.js +8 -3
  71. package/src/verax/core/confidence-engine.js +162 -23
  72. package/src/verax/core/contracts/types.js +1 -0
  73. package/src/verax/core/contracts/validators.js +79 -4
  74. package/src/verax/core/decision-snapshot.js +3 -30
  75. package/src/verax/core/decisions/decision.trace.js +2 -0
  76. package/src/verax/core/determinism/contract-writer.js +2 -2
  77. package/src/verax/core/determinism/contract.js +1 -1
  78. package/src/verax/core/determinism/diff.js +42 -1
  79. package/src/verax/core/determinism/engine.js +7 -6
  80. package/src/verax/core/determinism/finding-identity.js +3 -2
  81. package/src/verax/core/determinism/normalize.js +32 -4
  82. package/src/verax/core/determinism/report-writer.js +1 -0
  83. package/src/verax/core/determinism/run-fingerprint.js +7 -2
  84. package/src/verax/core/dynamic-route-intelligence.js +8 -7
  85. package/src/verax/core/evidence/evidence-capture-service.js +1 -0
  86. package/src/verax/core/evidence/evidence-intent-ledger.js +2 -1
  87. package/src/verax/core/evidence-builder.js +2 -2
  88. package/src/verax/core/execution-mode-context.js +1 -1
  89. package/src/verax/core/execution-mode-detector.js +5 -3
  90. package/src/verax/core/failures/exit-codes.js +39 -37
  91. package/src/verax/core/failures/failure-summary.js +1 -1
  92. package/src/verax/core/failures/failure.factory.js +3 -3
  93. package/src/verax/core/failures/failure.ledger.js +3 -2
  94. package/src/verax/core/ga/ga.artifact.js +1 -1
  95. package/src/verax/core/ga/ga.contract.js +3 -2
  96. package/src/verax/core/ga/ga.enforcer.js +1 -0
  97. package/src/verax/core/guardrails/policy.loader.js +1 -0
  98. package/src/verax/core/guardrails/truth-reconciliation.js +1 -1
  99. package/src/verax/core/guardrails-engine.js +2 -2
  100. package/src/verax/core/incremental-store.js +1 -0
  101. package/src/verax/core/integrity/budget.js +138 -0
  102. package/src/verax/core/integrity/determinism.js +342 -0
  103. package/src/verax/core/integrity/integrity.js +208 -0
  104. package/src/verax/core/integrity/poisoning.js +108 -0
  105. package/src/verax/core/integrity/transaction.js +140 -0
  106. package/src/verax/core/observe/run-timeline.js +2 -0
  107. package/src/verax/core/perf/perf.report.js +2 -0
  108. package/src/verax/core/pipeline-tracker.js +5 -0
  109. package/src/verax/core/release/provenance.builder.js +73 -214
  110. package/src/verax/core/release/release.enforcer.js +14 -9
  111. package/src/verax/core/release/reproducibility.check.js +1 -0
  112. package/src/verax/core/release/sbom.builder.js +32 -23
  113. package/src/verax/core/replay-validator.js +2 -0
  114. package/src/verax/core/replay.js +4 -0
  115. package/src/verax/core/report/cross-index.js +6 -3
  116. package/src/verax/core/report/human-summary.js +141 -1
  117. package/src/verax/core/route-intelligence.js +4 -3
  118. package/src/verax/core/run-id.js +6 -3
  119. package/src/verax/core/run-manifest.js +4 -3
  120. package/src/verax/core/security/secrets.scan.js +10 -7
  121. package/src/verax/core/security/security.enforcer.js +4 -0
  122. package/src/verax/core/security/supplychain.policy.js +9 -1
  123. package/src/verax/core/security/vuln.scan.js +2 -2
  124. package/src/verax/core/truth/truth.certificate.js +3 -1
  125. package/src/verax/core/ui-feedback-intelligence.js +12 -46
  126. package/src/verax/detect/conditional-ui-silent-failure.js +84 -0
  127. package/src/verax/detect/confidence-engine.js +100 -660
  128. package/src/verax/detect/confidence-helper.js +1 -0
  129. package/src/verax/detect/detection-engine.js +1 -18
  130. package/src/verax/detect/dynamic-route-findings.js +17 -14
  131. package/src/verax/detect/expectation-chain-detector.js +1 -1
  132. package/src/verax/detect/expectation-model.js +3 -5
  133. package/src/verax/detect/failure-cause-inference.js +293 -0
  134. package/src/verax/detect/findings-writer.js +126 -166
  135. package/src/verax/detect/flow-detector.js +2 -2
  136. package/src/verax/detect/form-silent-failure.js +98 -0
  137. package/src/verax/detect/index.js +51 -234
  138. package/src/verax/detect/invariants-enforcer.js +147 -0
  139. package/src/verax/detect/journey-stall-detector.js +4 -4
  140. package/src/verax/detect/navigation-silent-failure.js +82 -0
  141. package/src/verax/detect/problem-aggregator.js +361 -0
  142. package/src/verax/detect/route-findings.js +7 -6
  143. package/src/verax/detect/summary-writer.js +477 -0
  144. package/src/verax/detect/test-failure-cause-inference.js +314 -0
  145. package/src/verax/detect/ui-feedback-findings.js +18 -18
  146. package/src/verax/detect/verdict-engine.js +3 -57
  147. package/src/verax/detect/view-switch-correlator.js +2 -2
  148. package/src/verax/flow/flow-engine.js +2 -1
  149. package/src/verax/flow/flow-spec.js +0 -6
  150. package/src/verax/index.js +48 -412
  151. package/src/verax/intel/ts-program.js +1 -0
  152. package/src/verax/intel/vue-navigation-extractor.js +3 -0
  153. package/src/verax/learn/action-contract-extractor.js +67 -682
  154. package/src/verax/learn/ast-contract-extractor.js +1 -1
  155. package/src/verax/learn/flow-extractor.js +1 -0
  156. package/src/verax/learn/project-detector.js +5 -0
  157. package/src/verax/learn/react-router-extractor.js +2 -0
  158. package/src/verax/learn/route-validator.js +1 -4
  159. package/src/verax/learn/source-instrumenter.js +1 -0
  160. package/src/verax/learn/state-extractor.js +2 -1
  161. package/src/verax/learn/static-extractor.js +1 -0
  162. package/src/verax/observe/coverage-gaps.js +132 -0
  163. package/src/verax/observe/expectation-handler.js +126 -0
  164. package/src/verax/observe/incremental-skip.js +46 -0
  165. package/src/verax/observe/index.js +735 -84
  166. package/src/verax/observe/interaction-executor.js +192 -0
  167. package/src/verax/observe/interaction-runner.js +782 -530
  168. package/src/verax/observe/network-firewall.js +86 -0
  169. package/src/verax/observe/observation-builder.js +169 -0
  170. package/src/verax/observe/observe-context.js +1 -1
  171. package/src/verax/observe/observe-helpers.js +2 -1
  172. package/src/verax/observe/observe-runner.js +28 -24
  173. package/src/verax/observe/observers/budget-observer.js +3 -3
  174. package/src/verax/observe/observers/console-observer.js +4 -4
  175. package/src/verax/observe/observers/coverage-observer.js +4 -4
  176. package/src/verax/observe/observers/interaction-observer.js +3 -3
  177. package/src/verax/observe/observers/navigation-observer.js +4 -4
  178. package/src/verax/observe/observers/network-observer.js +4 -4
  179. package/src/verax/observe/observers/safety-observer.js +1 -1
  180. package/src/verax/observe/observers/ui-feedback-observer.js +4 -4
  181. package/src/verax/observe/page-traversal.js +138 -0
  182. package/src/verax/observe/snapshot-ops.js +94 -0
  183. package/src/verax/observe/ui-signal-sensor.js +2 -148
  184. package/src/verax/scan-summary-writer.js +10 -42
  185. package/src/verax/shared/artifact-manager.js +30 -13
  186. package/src/verax/shared/caching.js +1 -0
  187. package/src/verax/shared/expectation-tracker.js +1 -0
  188. package/src/verax/shared/zip-artifacts.js +6 -0
  189. package/src/verax/core/confidence-engine.js.backup +0 -471
  190. package/src/verax/shared/config-loader.js +0 -169
  191. /package/src/verax/shared/{expectation-proof.js → expectation-validation.js} +0 -0
@@ -8,12 +8,10 @@
8
8
  * NO HEURISTICS. Only static, deterministic analysis.
9
9
  */
10
10
 
11
- import { normalizeTemplateLiteral } from '../shared/dynamic-route-utils.js';
12
- import { parse } from '@babel/parser';
11
+ import { normalizeTemplateLiteral } from '../shared/dynamic-route-utils.js';import { parse } from '@babel/parser';
13
12
  import traverse from '@babel/traverse';
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';
13
+ import { readFileSync } from 'fs';
14
+ import { relative, sep } from 'path';
17
15
 
18
16
  /**
19
17
  * Extract action contracts from a source file.
@@ -32,41 +30,18 @@ export function extractActionContracts(filePath, workspaceRoot) {
32
30
  }
33
31
  }
34
32
 
35
- export function extractActionContractsFromCode(filePath, workspaceRoot, code, lineOffset = 0) {
33
+ function extractActionContractsFromCode(filePath, workspaceRoot, code, lineOffset = 0) {
36
34
  const contracts = [];
37
35
 
38
36
  // Track function declarations and arrow function assignments with location
39
37
  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);
44
38
 
45
39
  const ast = parse(code, {
46
40
  sourceType: 'unambiguous',
47
41
  plugins: ['jsx', 'typescript'],
48
42
  });
49
-
50
- // First pass: collect imports and function definitions
43
+
51
44
  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
-
70
45
  FunctionDeclaration(path) {
71
46
  if (path.node.id && path.node.id.name) {
72
47
  functionBodies.set(path.node.id.name, { body: path.node.body, loc: path.node.loc });
@@ -81,11 +56,8 @@ export function extractActionContractsFromCode(filePath, workspaceRoot, code, li
81
56
  ) {
82
57
  functionBodies.set(path.node.id.name, { body: path.node.init.body, loc: path.node.loc });
83
58
  }
84
- }
85
- });
59
+ },
86
60
 
87
- // Second pass: extract contracts with cross-file support
88
- traverse.default(ast, {
89
61
  // JSX handlers (React)
90
62
  JSXAttribute(path) {
91
63
  const attrName = path.node.name.name;
@@ -103,8 +75,7 @@ export function extractActionContractsFromCode(filePath, workspaceRoot, code, li
103
75
  value.expression.type === 'ArrowFunctionExpression'
104
76
  ) {
105
77
  const handlerBody = value.expression.body;
106
- const networkCalls = findNetworkCallsInNode(handlerBody, functionBodies, importMap, filePath, workspaceRoot, 0);
107
- const viewSwitchCalls = findViewSwitchCallsInNode(handlerBody, functionBodies, importMap, filePath, workspaceRoot, 0);
78
+ const networkCalls = findNetworkCallsInNode(handlerBody);
108
79
 
109
80
  for (const call of networkCalls) {
110
81
  const loc = path.node.loc;
@@ -121,116 +92,52 @@ export function extractActionContractsFromCode(filePath, workspaceRoot, code, li
121
92
  selectorHint: null
122
93
  });
123
94
  } else if (call.kind !== 'VALIDATION_BLOCK') {
124
- const contract = {
95
+ contracts.push({
125
96
  kind: call.kind || 'NETWORK_ACTION',
126
97
  method: call.method,
127
98
  urlPath: call.url,
128
99
  source: sourceRef,
129
100
  elementType: path.parent.name.name,
130
101
  handlerRef: sourceRef
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);
102
+ });
138
103
  }
139
104
  }
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
- }
160
105
  } else if (
161
106
  value.type === 'JSXExpressionContainer' &&
162
107
  value.expression.type === 'Identifier'
163
108
  ) {
164
109
  const refName = value.expression.name;
165
110
  const handlerRecord = functionBodies.get(refName);
166
-
167
- // Try local function first
168
- let networkCalls = [];
169
- let viewSwitchCalls = [];
170
- if (handlerRecord) {
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
111
 
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;
112
+ 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
+ });
210
138
  }
211
- contracts.push(contract);
212
139
  }
213
140
  }
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
- }
234
141
  }
235
142
  },
236
143
 
@@ -260,68 +167,12 @@ export function extractActionContractsFromCode(filePath, workspaceRoot, code, li
260
167
  if (record) {
261
168
  handlerBody = record.body;
262
169
  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
- }
318
170
  }
319
171
  }
320
172
 
321
173
  if (!handlerBody) return;
322
174
 
323
- const networkCalls = findNetworkCallsInNode(handlerBody, functionBodies, importMap, filePath, workspaceRoot, 0);
324
- const viewSwitchCalls = findViewSwitchCallsInNode(handlerBody, functionBodies, importMap, filePath, workspaceRoot, 0);
175
+ const networkCalls = findNetworkCallsInNode(handlerBody);
325
176
  const isSubmitEvent = eventType === 'submit';
326
177
 
327
178
  for (const call of networkCalls) {
@@ -338,20 +189,14 @@ export function extractActionContractsFromCode(filePath, workspaceRoot, code, li
338
189
  selectorHint: null
339
190
  });
340
191
  } else if (call.kind !== 'VALIDATION_BLOCK') {
341
- const contract = {
192
+ contracts.push({
342
193
  kind: call.kind || 'NETWORK_ACTION',
343
194
  method: call.method,
344
195
  urlPath: call.url,
345
196
  source: handlerRef,
346
197
  handlerRef: `${handlerRef}#${eventType}`,
347
198
  elementType: 'dom'
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);
199
+ });
355
200
  }
356
201
  }
357
202
  }
@@ -364,37 +209,22 @@ export function extractActionContractsFromCode(filePath, workspaceRoot, code, li
364
209
  /**
365
210
  * Extract template literal pattern from node.
366
211
  * 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
378
212
  */
379
213
  function extractTemplateLiteralPath(node) {
380
214
  if (!node || node.type !== 'TemplateLiteral') {
381
215
  return null;
382
216
  }
383
217
 
384
- // Build template string with ${param} placeholders
218
+ // Build template string
385
219
  let templateStr = node.quasis[0]?.value?.cooked || '';
386
- const dynamicSegments = [];
387
220
 
388
221
  for (let i = 0; i < node.expressions.length; i++) {
389
222
  const expr = node.expressions[i];
390
223
 
391
- // Only support simple identifiers (truth boundary)
224
+ // Only support simple identifiers
392
225
  if (expr.type === 'Identifier') {
393
- const paramName = expr.name;
394
- templateStr += '${' + paramName + '}';
395
- dynamicSegments.push(paramName);
226
+ templateStr += '${' + expr.name + '}';
396
227
  } else {
397
- // Complex expression - reject (truth boundary)
398
228
  return null;
399
229
  }
400
230
 
@@ -403,150 +233,7 @@ function extractTemplateLiteralPath(node) {
403
233
  }
404
234
  }
405
235
 
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 [];
236
+ return templateStr;
550
237
  }
551
238
 
552
239
  /**
@@ -554,14 +241,9 @@ function followCrossFileFunction(importInfo, workspaceRoot, depth) {
554
241
  * Only returns calls with static URL/path literals or template patterns.
555
242
  *
556
243
  * @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
562
244
  * @returns {Array<Object>} - Array of {kind, method, url} where kind is 'NETWORK_ACTION' or 'NAVIGATION_ACTION'
563
245
  */
564
- function findNetworkCallsInNode(node, functionBodies = new Map(), importMap = new Map(), filePath = '', workspaceRoot = '', depth = 0) {
246
+ function findNetworkCallsInNode(node) {
565
247
  const calls = [];
566
248
 
567
249
  // Track if we found preventDefault or return false for validation block detection
@@ -575,57 +257,27 @@ function findNetworkCallsInNode(node, functionBodies = new Map(), importMap = ne
575
257
  // Check if this is a CallExpression
576
258
  if (n.type === 'CallExpression') {
577
259
  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
- }
596
260
 
597
261
  // Case 1: fetch(url, options)
598
262
  if (callee.type === 'Identifier' && callee.name === 'fetch') {
599
263
  const urlArg = n.arguments[0];
600
264
  const optionsArg = n.arguments[1];
601
265
 
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();
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
+ }
610
278
  }
611
- }
612
279
 
613
- if (urlArg && urlArg.type === 'StringLiteral') {
614
- // Static URL
615
280
  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
- }
629
281
  }
630
282
  }
631
283
 
@@ -640,57 +292,7 @@ function findNetworkCallsInNode(node, functionBodies = new Map(), importMap = ne
640
292
  const urlArg = n.arguments[0];
641
293
 
642
294
  if (urlArg && urlArg.type === 'StringLiteral') {
643
- // Static URL
644
295
  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
- }
694
296
  }
695
297
  }
696
298
 
@@ -704,30 +306,15 @@ function findNetworkCallsInNode(node, functionBodies = new Map(), importMap = ne
704
306
  const methodArg = n.arguments[0];
705
307
  const urlArg = n.arguments[1];
706
308
 
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
- }
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
+ });
731
318
  }
732
319
  }
733
320
 
@@ -832,211 +419,6 @@ function findNetworkCallsInNode(node, functionBodies = new Map(), importMap = ne
832
419
  return calls;
833
420
  }
834
421
 
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
-
1040
422
  /**
1041
423
  * Format source reference as "file:line:col"
1042
424
  * Normalizes Windows paths to use forward slashes.
@@ -1108,10 +490,13 @@ export async function scanForContracts(rootPath, workspaceRoot) {
1108
490
  const html = readFileSync(fullPath, 'utf-8');
1109
491
  const scriptRegex = /<script[^>]*>([\s\S]*?)<\/script>/gi;
1110
492
  let match;
493
+ // @ts-expect-error - readFileSync with encoding returns string
1111
494
  while ((match = scriptRegex.exec(html)) !== null) {
1112
495
  const tagOpen = html.slice(match.index, html.indexOf('>', match.index) + 1);
496
+ // @ts-expect-error - readFileSync with encoding returns string
1113
497
  if (/\ssrc=/i.test(tagOpen)) continue; // skip external scripts
1114
498
  const before = html.slice(0, match.index);
499
+ // @ts-expect-error - readFileSync with encoding returns string
1115
500
  const lineOffset = (before.match(/\n/g) || []).length;
1116
501
  const code = match[1];
1117
502
  const blockContracts = extractActionContractsFromCode(fullPath, workspaceRoot, code, lineOffset + 1);