@veraxhq/verax 0.1.0 → 0.2.1

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 (135) hide show
  1. package/README.md +123 -88
  2. package/bin/verax.js +11 -452
  3. package/package.json +24 -36
  4. package/src/cli/commands/default.js +681 -0
  5. package/src/cli/commands/doctor.js +197 -0
  6. package/src/cli/commands/inspect.js +109 -0
  7. package/src/cli/commands/run.js +586 -0
  8. package/src/cli/entry.js +196 -0
  9. package/src/cli/util/atomic-write.js +37 -0
  10. package/src/cli/util/detection-engine.js +297 -0
  11. package/src/cli/util/env-url.js +33 -0
  12. package/src/cli/util/errors.js +44 -0
  13. package/src/cli/util/events.js +110 -0
  14. package/src/cli/util/expectation-extractor.js +388 -0
  15. package/src/cli/util/findings-writer.js +32 -0
  16. package/src/cli/util/idgen.js +87 -0
  17. package/src/cli/util/learn-writer.js +39 -0
  18. package/src/cli/util/observation-engine.js +412 -0
  19. package/src/cli/util/observe-writer.js +25 -0
  20. package/src/cli/util/paths.js +30 -0
  21. package/src/cli/util/project-discovery.js +297 -0
  22. package/src/cli/util/project-writer.js +26 -0
  23. package/src/cli/util/redact.js +128 -0
  24. package/src/cli/util/run-id.js +30 -0
  25. package/src/cli/util/runtime-budget.js +147 -0
  26. package/src/cli/util/summary-writer.js +43 -0
  27. package/src/types/global.d.ts +28 -0
  28. package/src/types/ts-ast.d.ts +24 -0
  29. package/src/verax/cli/ci-summary.js +35 -0
  30. package/src/verax/cli/context-explanation.js +89 -0
  31. package/src/verax/cli/doctor.js +277 -0
  32. package/src/verax/cli/error-normalizer.js +154 -0
  33. package/src/verax/cli/explain-output.js +105 -0
  34. package/src/verax/cli/finding-explainer.js +130 -0
  35. package/src/verax/cli/init.js +237 -0
  36. package/src/verax/cli/run-overview.js +163 -0
  37. package/src/verax/cli/url-safety.js +111 -0
  38. package/src/verax/cli/wizard.js +109 -0
  39. package/src/verax/cli/zero-findings-explainer.js +57 -0
  40. package/src/verax/cli/zero-interaction-explainer.js +127 -0
  41. package/src/verax/core/action-classifier.js +86 -0
  42. package/src/verax/core/budget-engine.js +218 -0
  43. package/src/verax/core/canonical-outcomes.js +157 -0
  44. package/src/verax/core/decision-snapshot.js +335 -0
  45. package/src/verax/core/determinism-model.js +432 -0
  46. package/src/verax/core/incremental-store.js +245 -0
  47. package/src/verax/core/invariants.js +356 -0
  48. package/src/verax/core/promise-model.js +230 -0
  49. package/src/verax/core/replay-validator.js +350 -0
  50. package/src/verax/core/replay.js +222 -0
  51. package/src/verax/core/run-id.js +175 -0
  52. package/src/verax/core/run-manifest.js +99 -0
  53. package/src/verax/core/silence-impact.js +369 -0
  54. package/src/verax/core/silence-model.js +523 -0
  55. package/src/verax/detect/comparison.js +7 -34
  56. package/src/verax/detect/confidence-engine.js +764 -329
  57. package/src/verax/detect/detection-engine.js +293 -0
  58. package/src/verax/detect/evidence-index.js +127 -0
  59. package/src/verax/detect/expectation-model.js +241 -168
  60. package/src/verax/detect/explanation-helpers.js +187 -0
  61. package/src/verax/detect/finding-detector.js +450 -0
  62. package/src/verax/detect/findings-writer.js +41 -12
  63. package/src/verax/detect/flow-detector.js +366 -0
  64. package/src/verax/detect/index.js +200 -288
  65. package/src/verax/detect/interactive-findings.js +612 -0
  66. package/src/verax/detect/signal-mapper.js +308 -0
  67. package/src/verax/detect/skip-classifier.js +4 -4
  68. package/src/verax/detect/verdict-engine.js +561 -0
  69. package/src/verax/evidence-index-writer.js +61 -0
  70. package/src/verax/flow/flow-engine.js +3 -2
  71. package/src/verax/flow/flow-spec.js +1 -2
  72. package/src/verax/index.js +103 -15
  73. package/src/verax/intel/effect-detector.js +368 -0
  74. package/src/verax/intel/handler-mapper.js +249 -0
  75. package/src/verax/intel/index.js +281 -0
  76. package/src/verax/intel/route-extractor.js +280 -0
  77. package/src/verax/intel/ts-program.js +256 -0
  78. package/src/verax/intel/vue-navigation-extractor.js +642 -0
  79. package/src/verax/intel/vue-router-extractor.js +325 -0
  80. package/src/verax/learn/action-contract-extractor.js +338 -104
  81. package/src/verax/learn/ast-contract-extractor.js +148 -6
  82. package/src/verax/learn/flow-extractor.js +172 -0
  83. package/src/verax/learn/index.js +36 -2
  84. package/src/verax/learn/manifest-writer.js +122 -58
  85. package/src/verax/learn/project-detector.js +40 -0
  86. package/src/verax/learn/route-extractor.js +28 -97
  87. package/src/verax/learn/route-validator.js +8 -7
  88. package/src/verax/learn/state-extractor.js +212 -0
  89. package/src/verax/learn/static-extractor-navigation.js +114 -0
  90. package/src/verax/learn/static-extractor-validation.js +88 -0
  91. package/src/verax/learn/static-extractor.js +119 -10
  92. package/src/verax/learn/truth-assessor.js +24 -21
  93. package/src/verax/learn/ts-contract-resolver.js +14 -12
  94. package/src/verax/observe/aria-sensor.js +211 -0
  95. package/src/verax/observe/browser.js +30 -6
  96. package/src/verax/observe/console-sensor.js +2 -18
  97. package/src/verax/observe/domain-boundary.js +10 -1
  98. package/src/verax/observe/expectation-executor.js +513 -0
  99. package/src/verax/observe/flow-matcher.js +143 -0
  100. package/src/verax/observe/focus-sensor.js +196 -0
  101. package/src/verax/observe/human-driver.js +660 -273
  102. package/src/verax/observe/index.js +910 -26
  103. package/src/verax/observe/interaction-discovery.js +378 -15
  104. package/src/verax/observe/interaction-runner.js +562 -197
  105. package/src/verax/observe/loading-sensor.js +145 -0
  106. package/src/verax/observe/navigation-sensor.js +255 -0
  107. package/src/verax/observe/network-sensor.js +55 -7
  108. package/src/verax/observe/observed-expectation-deriver.js +186 -0
  109. package/src/verax/observe/observed-expectation.js +305 -0
  110. package/src/verax/observe/page-frontier.js +234 -0
  111. package/src/verax/observe/settle.js +38 -17
  112. package/src/verax/observe/state-sensor.js +393 -0
  113. package/src/verax/observe/state-ui-sensor.js +7 -1
  114. package/src/verax/observe/timing-sensor.js +228 -0
  115. package/src/verax/observe/traces-writer.js +73 -21
  116. package/src/verax/observe/ui-signal-sensor.js +143 -17
  117. package/src/verax/scan-summary-writer.js +80 -15
  118. package/src/verax/shared/artifact-manager.js +111 -9
  119. package/src/verax/shared/budget-profiles.js +136 -0
  120. package/src/verax/shared/caching.js +1 -1
  121. package/src/verax/shared/ci-detection.js +39 -0
  122. package/src/verax/shared/config-loader.js +169 -0
  123. package/src/verax/shared/dynamic-route-utils.js +224 -0
  124. package/src/verax/shared/expectation-coverage.js +44 -0
  125. package/src/verax/shared/expectation-prover.js +81 -0
  126. package/src/verax/shared/expectation-tracker.js +201 -0
  127. package/src/verax/shared/expectations-writer.js +60 -0
  128. package/src/verax/shared/first-run.js +44 -0
  129. package/src/verax/shared/progress-reporter.js +171 -0
  130. package/src/verax/shared/retry-policy.js +9 -1
  131. package/src/verax/shared/root-artifacts.js +49 -0
  132. package/src/verax/shared/scan-budget.js +86 -0
  133. package/src/verax/shared/url-normalizer.js +162 -0
  134. package/src/verax/shared/zip-artifacts.js +66 -0
  135. package/src/verax/validate/context-validator.js +244 -0
@@ -3,15 +3,15 @@
3
3
  *
4
4
  * Extracts PROVEN network action contracts from JSX/React source files.
5
5
  * Uses AST analysis to find onClick/onSubmit handlers that call fetch/axios
6
- * with static URL literals.
6
+ * with static URL literals and template literals.
7
7
  *
8
8
  * NO HEURISTICS. Only static, deterministic analysis.
9
9
  */
10
10
 
11
- import { parse } from '@babel/parser';
11
+ import { normalizeTemplateLiteral } from '../shared/dynamic-route-utils.js';import { parse } from '@babel/parser';
12
12
  import traverse from '@babel/traverse';
13
13
  import { readFileSync } from 'fs';
14
- import { resolve, relative, sep } from 'path';
14
+ import { relative, sep } from 'path';
15
15
 
16
16
  /**
17
17
  * Extract action contracts from a source file.
@@ -21,113 +21,234 @@ import { resolve, relative, sep } from 'path';
21
21
  * @returns {Array<Object>} - Array of contract objects
22
22
  */
23
23
  export function extractActionContracts(filePath, workspaceRoot) {
24
- const contracts = [];
25
-
26
24
  try {
27
25
  const code = readFileSync(filePath, 'utf-8');
28
- const ast = parse(code, {
29
- sourceType: 'module',
30
- plugins: ['jsx', 'typescript'],
31
- });
32
-
33
- // Track function declarations and arrow function assignments
34
- const functionBodies = new Map(); // name -> AST node
35
-
36
- traverse.default(ast, {
37
- // Track function declarations
38
- FunctionDeclaration(path) {
39
- if (path.node.id && path.node.id.name) {
40
- functionBodies.set(path.node.id.name, path.node.body);
41
- }
42
- },
43
-
44
- // Track arrow function variable assignments
45
- VariableDeclarator(path) {
46
- if (
47
- path.node.id.type === 'Identifier' &&
48
- path.node.init &&
49
- path.node.init.type === 'ArrowFunctionExpression'
50
- ) {
51
- functionBodies.set(path.node.id.name, path.node.init.body);
52
- }
53
- },
54
-
55
- // Find JSX elements with onClick or onSubmit
56
- JSXAttribute(path) {
57
- const attrName = path.node.name.name;
58
- if (attrName !== 'onClick' && attrName !== 'onSubmit') {
59
- return;
60
- }
61
-
62
- const value = path.node.value;
63
- if (!value) return;
64
-
65
- // Only analyze inline arrow functions for now
66
- // Case 1: Inline arrow function: onClick={() => fetch(...)}
67
- if (
68
- value.type === 'JSXExpressionContainer' &&
69
- value.expression.type === 'ArrowFunctionExpression'
70
- ) {
71
- const handlerBody = value.expression.body;
72
- const networkCalls = findNetworkCallsInNode(handlerBody);
73
-
74
- for (const call of networkCalls) {
75
- const loc = path.node.loc;
76
- const sourceRef = formatSourceRef(filePath, workspaceRoot, loc);
77
-
78
- contracts.push({
79
- kind: 'NETWORK_ACTION',
80
- method: call.method,
81
- urlPath: call.url,
82
- source: sourceRef,
83
- elementType: path.parent.name.name, // button, form, etc.
84
- });
85
- }
86
- }
87
- // Case 2: Function reference - analyze the function declaration
88
- else if (
89
- value.type === 'JSXExpressionContainer' &&
90
- value.expression.type === 'Identifier'
91
- ) {
92
- const refName = value.expression.name;
93
- const handlerBody = functionBodies.get(refName);
94
-
95
- if (handlerBody) {
96
- const networkCalls = findNetworkCallsInNode(handlerBody);
97
-
98
- for (const call of networkCalls) {
99
- const loc = path.node.loc;
100
- const sourceRef = formatSourceRef(filePath, workspaceRoot, loc);
101
-
102
- contracts.push({
103
- kind: 'NETWORK_ACTION',
104
- method: call.method,
105
- urlPath: call.url,
106
- source: sourceRef,
107
- elementType: path.parent.name.name,
108
- });
109
- }
110
- }
111
- }
112
- },
113
- });
26
+ return extractActionContractsFromCode(filePath, workspaceRoot, code, 0);
114
27
  } catch (err) {
115
- // Parse errors are not fatal; just return empty contracts
116
28
  console.warn(`Failed to parse ${filePath}: ${err.message}`);
29
+ return [];
117
30
  }
31
+ }
32
+
33
+ function extractActionContractsFromCode(filePath, workspaceRoot, code, lineOffset = 0) {
34
+ const contracts = [];
35
+
36
+ // Track function declarations and arrow function assignments with location
37
+ const functionBodies = new Map(); // name -> { body, loc }
38
+
39
+ const ast = parse(code, {
40
+ sourceType: 'unambiguous',
41
+ plugins: ['jsx', 'typescript'],
42
+ });
43
+
44
+ traverse.default(ast, {
45
+ FunctionDeclaration(path) {
46
+ if (path.node.id && path.node.id.name) {
47
+ functionBodies.set(path.node.id.name, { body: path.node.body, loc: path.node.loc });
48
+ }
49
+ },
50
+
51
+ VariableDeclarator(path) {
52
+ if (
53
+ path.node.id.type === 'Identifier' &&
54
+ path.node.init &&
55
+ path.node.init.type === 'ArrowFunctionExpression'
56
+ ) {
57
+ functionBodies.set(path.node.id.name, { body: path.node.init.body, loc: path.node.loc });
58
+ }
59
+ },
60
+
61
+ // JSX handlers (React)
62
+ JSXAttribute(path) {
63
+ const attrName = path.node.name.name;
64
+ if (attrName !== 'onClick' && attrName !== 'onSubmit') {
65
+ return;
66
+ }
67
+
68
+ const isSubmitHandler = attrName === 'onSubmit';
69
+
70
+ const value = path.node.value;
71
+ if (!value) return;
72
+
73
+ if (
74
+ value.type === 'JSXExpressionContainer' &&
75
+ value.expression.type === 'ArrowFunctionExpression'
76
+ ) {
77
+ const handlerBody = value.expression.body;
78
+ const networkCalls = findNetworkCallsInNode(handlerBody);
79
+
80
+ for (const call of networkCalls) {
81
+ const loc = path.node.loc;
82
+ const sourceRef = formatSourceRef(filePath, workspaceRoot, loc, lineOffset);
83
+
84
+ if (call.kind === 'VALIDATION_BLOCK' && isSubmitHandler) {
85
+ contracts.push({
86
+ kind: 'VALIDATION_BLOCK',
87
+ method: call.method,
88
+ urlPath: null,
89
+ source: sourceRef,
90
+ elementType: path.parent.name.name,
91
+ handlerRef: sourceRef,
92
+ selectorHint: null
93
+ });
94
+ } else if (call.kind !== 'VALIDATION_BLOCK') {
95
+ contracts.push({
96
+ kind: call.kind || 'NETWORK_ACTION',
97
+ method: call.method,
98
+ urlPath: call.url,
99
+ source: sourceRef,
100
+ elementType: path.parent.name.name,
101
+ handlerRef: sourceRef
102
+ });
103
+ }
104
+ }
105
+ } else if (
106
+ value.type === 'JSXExpressionContainer' &&
107
+ value.expression.type === 'Identifier'
108
+ ) {
109
+ const refName = value.expression.name;
110
+ const handlerRecord = functionBodies.get(refName);
111
+
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
+ });
138
+ }
139
+ }
140
+ }
141
+ }
142
+ },
143
+
144
+ // DOM addEventListener handlers (static HTML/JS)
145
+ CallExpression(path) {
146
+ const callee = path.node.callee;
147
+ if (
148
+ callee.type === 'MemberExpression' &&
149
+ callee.property.type === 'Identifier' &&
150
+ callee.property.name === 'addEventListener'
151
+ ) {
152
+ const args = path.node.arguments || [];
153
+ if (args.length < 2) return;
154
+
155
+ const eventArg = args[0];
156
+ const handlerArg = args[1];
157
+ const eventType = eventArg && eventArg.type === 'StringLiteral' ? eventArg.value : 'event';
158
+
159
+ let handlerBody = null;
160
+ let handlerLoc = handlerArg?.loc || path.node.loc;
161
+
162
+ if (handlerArg && (handlerArg.type === 'ArrowFunctionExpression' || handlerArg.type === 'FunctionExpression')) {
163
+ handlerBody = handlerArg.body;
164
+ handlerLoc = handlerArg.loc || path.node.loc;
165
+ } else if (handlerArg && handlerArg.type === 'Identifier') {
166
+ const record = functionBodies.get(handlerArg.name);
167
+ if (record) {
168
+ handlerBody = record.body;
169
+ handlerLoc = record.loc || path.node.loc;
170
+ }
171
+ }
172
+
173
+ if (!handlerBody) return;
174
+
175
+ const networkCalls = findNetworkCallsInNode(handlerBody);
176
+ const isSubmitEvent = eventType === 'submit';
177
+
178
+ for (const call of networkCalls) {
179
+ const handlerRef = formatSourceRef(filePath, workspaceRoot, handlerLoc, lineOffset);
180
+
181
+ if (call.kind === 'VALIDATION_BLOCK' && isSubmitEvent) {
182
+ contracts.push({
183
+ kind: 'VALIDATION_BLOCK',
184
+ method: call.method,
185
+ urlPath: null,
186
+ source: handlerRef,
187
+ handlerRef: `${handlerRef}#${eventType}`,
188
+ elementType: 'dom',
189
+ selectorHint: null
190
+ });
191
+ } else if (call.kind !== 'VALIDATION_BLOCK') {
192
+ contracts.push({
193
+ kind: call.kind || 'NETWORK_ACTION',
194
+ method: call.method,
195
+ urlPath: call.url,
196
+ source: handlerRef,
197
+ handlerRef: `${handlerRef}#${eventType}`,
198
+ elementType: 'dom'
199
+ });
200
+ }
201
+ }
202
+ }
203
+ }
204
+ });
118
205
 
119
206
  return contracts;
120
207
  }
121
208
 
122
209
  /**
123
- * Find network calls (fetch, axios) in an AST node by recursively scanning.
124
- * Only returns calls with static URL literals.
210
+ * Extract template literal pattern from node.
211
+ * Returns null if template has complex expressions.
212
+ */
213
+ function extractTemplateLiteralPath(node) {
214
+ if (!node || node.type !== 'TemplateLiteral') {
215
+ return null;
216
+ }
217
+
218
+ // Build template string
219
+ let templateStr = node.quasis[0]?.value?.cooked || '';
220
+
221
+ for (let i = 0; i < node.expressions.length; i++) {
222
+ const expr = node.expressions[i];
223
+
224
+ // Only support simple identifiers
225
+ if (expr.type === 'Identifier') {
226
+ templateStr += '${' + expr.name + '}';
227
+ } else {
228
+ return null;
229
+ }
230
+
231
+ if (node.quasis[i + 1]) {
232
+ templateStr += node.quasis[i + 1].value.cooked || '';
233
+ }
234
+ }
235
+
236
+ return templateStr;
237
+ }
238
+
239
+ /**
240
+ * Find action calls (fetch, axios, router.push, navigate) in an AST node by recursively scanning.
241
+ * Only returns calls with static URL/path literals or template patterns.
125
242
  *
126
243
  * @param {Object} node - AST node to scan
127
- * @returns {Array<Object>} - Array of {method, url}
244
+ * @returns {Array<Object>} - Array of {kind, method, url} where kind is 'NETWORK_ACTION' or 'NAVIGATION_ACTION'
128
245
  */
129
246
  function findNetworkCallsInNode(node) {
130
247
  const calls = [];
248
+
249
+ // Track if we found preventDefault or return false for validation block detection
250
+ let hasPreventDefault = false;
251
+ let hasReturnFalse = false;
131
252
 
132
253
  // Recursive function to scan all nodes
133
254
  function scan(n) {
@@ -156,7 +277,7 @@ function findNetworkCallsInNode(node) {
156
277
  }
157
278
  }
158
279
 
159
- calls.push({ method, url: urlArg.value });
280
+ calls.push({ kind: 'NETWORK_ACTION', method, url: urlArg.value });
160
281
  }
161
282
  }
162
283
 
@@ -171,7 +292,7 @@ function findNetworkCallsInNode(node) {
171
292
  const urlArg = n.arguments[0];
172
293
 
173
294
  if (urlArg && urlArg.type === 'StringLiteral') {
174
- calls.push({ method: methodName, url: urlArg.value });
295
+ calls.push({ kind: 'NETWORK_ACTION', method: methodName, url: urlArg.value });
175
296
  }
176
297
  }
177
298
 
@@ -190,16 +311,90 @@ function findNetworkCallsInNode(node) {
190
311
  urlArg && urlArg.type === 'StringLiteral'
191
312
  ) {
192
313
  calls.push({
314
+ kind: 'NETWORK_ACTION',
193
315
  method: methodArg.value.toUpperCase(),
194
316
  url: urlArg.value,
195
317
  });
196
318
  }
197
319
  }
320
+
321
+ // Case 4: router.push(path), router.replace(path), history.push(path)
322
+ if (
323
+ callee.type === 'MemberExpression' &&
324
+ callee.object.type === 'Identifier' &&
325
+ (callee.object.name === 'router' || callee.object.name === 'history') &&
326
+ callee.property.type === 'Identifier' &&
327
+ ['push', 'replace', 'navigate'].includes(callee.property.name)
328
+ ) {
329
+ const pathArg = n.arguments[0];
330
+ if (pathArg && pathArg.type === 'StringLiteral') {
331
+ calls.push({
332
+ kind: 'NAVIGATION_ACTION',
333
+ method: `${callee.object.name}.${callee.property.name}`,
334
+ url: pathArg.value
335
+ });
336
+ } else if (pathArg && pathArg.type === 'TemplateLiteral') {
337
+ // Template literal: router.push(`/users/${id}`)
338
+ const templatePath = extractTemplateLiteralPath(pathArg);
339
+ if (templatePath && templatePath.startsWith('/')) {
340
+ // Normalize to example path
341
+ const normalized = normalizeTemplateLiteral(templatePath);
342
+ calls.push({
343
+ kind: 'NAVIGATION_ACTION',
344
+ method: `${callee.object.name}.${callee.property.name}`,
345
+ url: normalized ? normalized.examplePath : templatePath
346
+ });
347
+ }
348
+ }
349
+ }
350
+
351
+ // Case 5: navigate(path) - standalone function
352
+ if (callee.type === 'Identifier' && callee.name === 'navigate') {
353
+ const pathArg = n.arguments[0];
354
+ if (pathArg && pathArg.type === 'StringLiteral') {
355
+ calls.push({
356
+ kind: 'NAVIGATION_ACTION',
357
+ method: 'navigate',
358
+ url: pathArg.value
359
+ });
360
+ } else if (pathArg && pathArg.type === 'TemplateLiteral') {
361
+ // Template literal: navigate(`/users/${id}`)
362
+ const templatePath = extractTemplateLiteralPath(pathArg);
363
+ if (templatePath && templatePath.startsWith('/')) {
364
+ // Normalize to example path
365
+ const normalized = normalizeTemplateLiteral(templatePath);
366
+ calls.push({
367
+ kind: 'NAVIGATION_ACTION',
368
+ method: 'navigate',
369
+ url: normalized ? normalized.examplePath : templatePath
370
+ });
371
+ }
372
+ }
373
+ }
374
+
375
+ // Case 6: event.preventDefault() - validation block
376
+ if (
377
+ callee.type === 'MemberExpression' &&
378
+ callee.property.type === 'Identifier' &&
379
+ callee.property.name === 'preventDefault'
380
+ ) {
381
+ hasPreventDefault = true;
382
+ }
383
+ }
384
+
385
+ // Check for return false statement
386
+ if (n.type === 'ReturnStatement' && n.argument) {
387
+ if (
388
+ (n.argument.type === 'BooleanLiteral' && n.argument.value === false) ||
389
+ (n.argument.type === 'Identifier' && n.argument.name === 'false')
390
+ ) {
391
+ hasReturnFalse = true;
392
+ }
198
393
  }
199
394
 
200
395
  // Recursively scan all properties
201
396
  for (const key in n) {
202
- if (n.hasOwnProperty(key)) {
397
+ if (Object.prototype.hasOwnProperty.call(n, key)) {
203
398
  const value = n[key];
204
399
  if (Array.isArray(value)) {
205
400
  value.forEach(item => scan(item));
@@ -211,6 +406,16 @@ function findNetworkCallsInNode(node) {
211
406
  }
212
407
 
213
408
  scan(node);
409
+
410
+ // If validation block detected, add VALIDATION_BLOCK contract
411
+ if (hasPreventDefault || hasReturnFalse) {
412
+ calls.push({
413
+ kind: 'VALIDATION_BLOCK',
414
+ method: hasPreventDefault ? 'preventDefault' : 'return-false',
415
+ url: null
416
+ });
417
+ }
418
+
214
419
  return calls;
215
420
  }
216
421
 
@@ -223,14 +428,14 @@ function findNetworkCallsInNode(node) {
223
428
  * @param {Object} loc - Location object from AST
224
429
  * @returns {string} - Formatted source reference
225
430
  */
226
- function formatSourceRef(filePath, workspaceRoot, loc) {
431
+ function formatSourceRef(filePath, workspaceRoot, loc, lineOffset = 0) {
227
432
  let relPath = relative(workspaceRoot, filePath);
228
433
 
229
434
  // Normalize to forward slashes
230
435
  relPath = relPath.split(sep).join('/');
231
436
 
232
- const line = loc.start.line;
233
- const col = loc.start.column;
437
+ const line = (loc?.start?.line || 1) + lineOffset;
438
+ const col = loc?.start?.column || 0;
234
439
 
235
440
  return `${relPath}:${line}:${col}`;
236
441
  }
@@ -240,7 +445,7 @@ function formatSourceRef(filePath, workspaceRoot, loc) {
240
445
  *
241
446
  * @param {string} rootPath - Root directory to scan
242
447
  * @param {string} workspaceRoot - Workspace root
243
- * @returns {Array<Object>} - All contracts found
448
+ * @returns {Promise<Array<Object>>} - All contracts found
244
449
  */
245
450
  export async function scanForContracts(rootPath, workspaceRoot) {
246
451
  const contracts = [];
@@ -260,14 +465,43 @@ export async function scanForContracts(rootPath, workspaceRoot) {
260
465
 
261
466
  if (stat.isDirectory()) {
262
467
  // Skip node_modules, .git, etc.
263
- if (!entry.startsWith('.') && entry !== 'node_modules') {
468
+ const ignoredDirs = new Set([
469
+ 'node_modules',
470
+ '.git',
471
+ '.verax',
472
+ 'dist',
473
+ 'build',
474
+ 'out',
475
+ 'artifacts',
476
+ 'logs',
477
+ 'temp-installs',
478
+ 'tmp',
479
+ 'temp'
480
+ ]);
481
+ if (!entry.startsWith('.') && !ignoredDirs.has(entry)) {
264
482
  walk(fullPath);
265
483
  }
266
484
  } else if (stat.isFile()) {
267
- // Only process .js, .jsx, .ts, .tsx files
268
485
  if (/\.(jsx?|tsx?)$/.test(entry)) {
269
486
  const fileContracts = extractActionContracts(fullPath, workspaceRoot);
270
487
  contracts.push(...fileContracts);
488
+ } else if (/\.html?$/.test(entry)) {
489
+ try {
490
+ const html = readFileSync(fullPath, 'utf-8');
491
+ const scriptRegex = /<script[^>]*>([\s\S]*?)<\/script>/gi;
492
+ let match;
493
+ while ((match = scriptRegex.exec(html)) !== null) {
494
+ const tagOpen = html.slice(match.index, html.indexOf('>', match.index) + 1);
495
+ if (/\ssrc=/i.test(tagOpen)) continue; // skip external scripts
496
+ const before = html.slice(0, match.index);
497
+ const lineOffset = (before.match(/\n/g) || []).length;
498
+ const code = match[1];
499
+ const blockContracts = extractActionContractsFromCode(fullPath, workspaceRoot, code, lineOffset + 1);
500
+ contracts.push(...blockContracts);
501
+ }
502
+ } catch (err) {
503
+ console.warn(`Failed to parse HTML scripts in ${fullPath}: ${err.message}`);
504
+ }
271
505
  }
272
506
  }
273
507
  }