@veraxhq/verax 0.1.0 → 0.2.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 (126) hide show
  1. package/README.md +123 -88
  2. package/bin/verax.js +11 -452
  3. package/package.json +14 -36
  4. package/src/cli/commands/default.js +523 -0
  5. package/src/cli/commands/doctor.js +165 -0
  6. package/src/cli/commands/inspect.js +109 -0
  7. package/src/cli/commands/run.js +402 -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 +296 -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 +34 -0
  14. package/src/cli/util/expectation-extractor.js +378 -0
  15. package/src/cli/util/findings-writer.js +31 -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 +366 -0
  19. package/src/cli/util/observe-writer.js +25 -0
  20. package/src/cli/util/paths.js +29 -0
  21. package/src/cli/util/project-discovery.js +277 -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/summary-writer.js +32 -0
  26. package/src/verax/cli/ci-summary.js +35 -0
  27. package/src/verax/cli/context-explanation.js +89 -0
  28. package/src/verax/cli/doctor.js +277 -0
  29. package/src/verax/cli/error-normalizer.js +154 -0
  30. package/src/verax/cli/explain-output.js +105 -0
  31. package/src/verax/cli/finding-explainer.js +130 -0
  32. package/src/verax/cli/init.js +237 -0
  33. package/src/verax/cli/run-overview.js +163 -0
  34. package/src/verax/cli/url-safety.js +101 -0
  35. package/src/verax/cli/wizard.js +98 -0
  36. package/src/verax/cli/zero-findings-explainer.js +57 -0
  37. package/src/verax/cli/zero-interaction-explainer.js +127 -0
  38. package/src/verax/core/action-classifier.js +86 -0
  39. package/src/verax/core/budget-engine.js +218 -0
  40. package/src/verax/core/canonical-outcomes.js +157 -0
  41. package/src/verax/core/decision-snapshot.js +335 -0
  42. package/src/verax/core/determinism-model.js +403 -0
  43. package/src/verax/core/incremental-store.js +237 -0
  44. package/src/verax/core/invariants.js +356 -0
  45. package/src/verax/core/promise-model.js +230 -0
  46. package/src/verax/core/replay-validator.js +350 -0
  47. package/src/verax/core/replay.js +222 -0
  48. package/src/verax/core/run-id.js +175 -0
  49. package/src/verax/core/run-manifest.js +99 -0
  50. package/src/verax/core/silence-impact.js +369 -0
  51. package/src/verax/core/silence-model.js +521 -0
  52. package/src/verax/detect/comparison.js +2 -34
  53. package/src/verax/detect/confidence-engine.js +764 -329
  54. package/src/verax/detect/detection-engine.js +293 -0
  55. package/src/verax/detect/evidence-index.js +177 -0
  56. package/src/verax/detect/expectation-model.js +194 -172
  57. package/src/verax/detect/explanation-helpers.js +187 -0
  58. package/src/verax/detect/finding-detector.js +450 -0
  59. package/src/verax/detect/findings-writer.js +44 -8
  60. package/src/verax/detect/flow-detector.js +366 -0
  61. package/src/verax/detect/index.js +172 -286
  62. package/src/verax/detect/interactive-findings.js +613 -0
  63. package/src/verax/detect/signal-mapper.js +308 -0
  64. package/src/verax/detect/verdict-engine.js +563 -0
  65. package/src/verax/evidence-index-writer.js +61 -0
  66. package/src/verax/index.js +90 -14
  67. package/src/verax/intel/effect-detector.js +368 -0
  68. package/src/verax/intel/handler-mapper.js +249 -0
  69. package/src/verax/intel/index.js +281 -0
  70. package/src/verax/intel/route-extractor.js +280 -0
  71. package/src/verax/intel/ts-program.js +256 -0
  72. package/src/verax/intel/vue-navigation-extractor.js +579 -0
  73. package/src/verax/intel/vue-router-extractor.js +323 -0
  74. package/src/verax/learn/action-contract-extractor.js +335 -101
  75. package/src/verax/learn/ast-contract-extractor.js +95 -5
  76. package/src/verax/learn/flow-extractor.js +172 -0
  77. package/src/verax/learn/manifest-writer.js +97 -47
  78. package/src/verax/learn/project-detector.js +40 -0
  79. package/src/verax/learn/route-extractor.js +27 -96
  80. package/src/verax/learn/state-extractor.js +212 -0
  81. package/src/verax/learn/static-extractor-navigation.js +114 -0
  82. package/src/verax/learn/static-extractor-validation.js +88 -0
  83. package/src/verax/learn/static-extractor.js +112 -4
  84. package/src/verax/learn/truth-assessor.js +24 -21
  85. package/src/verax/observe/aria-sensor.js +211 -0
  86. package/src/verax/observe/browser.js +10 -5
  87. package/src/verax/observe/console-sensor.js +1 -17
  88. package/src/verax/observe/domain-boundary.js +10 -1
  89. package/src/verax/observe/expectation-executor.js +512 -0
  90. package/src/verax/observe/flow-matcher.js +143 -0
  91. package/src/verax/observe/focus-sensor.js +196 -0
  92. package/src/verax/observe/human-driver.js +643 -275
  93. package/src/verax/observe/index.js +908 -27
  94. package/src/verax/observe/index.js.backup +1 -0
  95. package/src/verax/observe/interaction-discovery.js +365 -14
  96. package/src/verax/observe/interaction-runner.js +563 -198
  97. package/src/verax/observe/loading-sensor.js +139 -0
  98. package/src/verax/observe/navigation-sensor.js +255 -0
  99. package/src/verax/observe/network-sensor.js +55 -7
  100. package/src/verax/observe/observed-expectation-deriver.js +186 -0
  101. package/src/verax/observe/observed-expectation.js +305 -0
  102. package/src/verax/observe/page-frontier.js +234 -0
  103. package/src/verax/observe/settle.js +37 -17
  104. package/src/verax/observe/state-sensor.js +389 -0
  105. package/src/verax/observe/timing-sensor.js +228 -0
  106. package/src/verax/observe/traces-writer.js +61 -20
  107. package/src/verax/observe/ui-signal-sensor.js +136 -17
  108. package/src/verax/scan-summary-writer.js +77 -15
  109. package/src/verax/shared/artifact-manager.js +110 -8
  110. package/src/verax/shared/budget-profiles.js +136 -0
  111. package/src/verax/shared/ci-detection.js +39 -0
  112. package/src/verax/shared/config-loader.js +170 -0
  113. package/src/verax/shared/dynamic-route-utils.js +218 -0
  114. package/src/verax/shared/expectation-coverage.js +44 -0
  115. package/src/verax/shared/expectation-prover.js +81 -0
  116. package/src/verax/shared/expectation-tracker.js +201 -0
  117. package/src/verax/shared/expectations-writer.js +60 -0
  118. package/src/verax/shared/first-run.js +44 -0
  119. package/src/verax/shared/progress-reporter.js +171 -0
  120. package/src/verax/shared/retry-policy.js +14 -1
  121. package/src/verax/shared/root-artifacts.js +49 -0
  122. package/src/verax/shared/scan-budget.js +86 -0
  123. package/src/verax/shared/url-normalizer.js +162 -0
  124. package/src/verax/shared/zip-artifacts.js +65 -0
  125. package/src/verax/validate/context-validator.js +244 -0
  126. package/src/verax/validate/context-validator.js.bak +0 -0
@@ -3,12 +3,12 @@
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
14
  import { resolve, relative, sep } from 'path';
@@ -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,11 +311,85 @@ 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
@@ -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
  }
@@ -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
  }
@@ -4,6 +4,7 @@ import { readFileSync } from 'fs';
4
4
  import { glob } from 'glob';
5
5
  import { resolve } from 'path';
6
6
  import { ExpectationProof } from '../shared/expectation-proof.js';
7
+ import { normalizeTemplateLiteral } from '../shared/dynamic-route-utils.js';
7
8
 
8
9
  const MAX_FILES_TO_SCAN = 200;
9
10
 
@@ -45,18 +46,73 @@ function extractStaticStringValue(node) {
45
46
  return null;
46
47
  }
47
48
 
49
+ /**
50
+ * Extracts template literal pattern from Babel TemplateLiteral node.
51
+ * Returns null if template has complex expressions that cannot be normalized.
52
+ *
53
+ * Examples:
54
+ * - `${id}` → { pattern: '${id}', examplePath: '/1' }
55
+ * - `/users/${id}` → { pattern: '/users/${id}', examplePath: '/users/1' }
56
+ * - `/posts/${slug}` → { pattern: '/posts/${slug}', examplePath: '/posts/example' }
57
+ */
58
+ function extractTemplatePattern(templateNode) {
59
+ if (!templateNode || templateNode.type !== 'TemplateLiteral') {
60
+ return null;
61
+ }
62
+
63
+ // Build the template string with ${} placeholders
64
+ let templateStr = templateNode.quasis[0]?.value?.cooked || '';
65
+
66
+ for (let i = 0; i < templateNode.expressions.length; i++) {
67
+ const expr = templateNode.expressions[i];
68
+
69
+ // Only support simple identifiers: ${id}, ${slug}
70
+ if (expr.type === 'Identifier') {
71
+ templateStr += '${' + expr.name + '}';
72
+ } else {
73
+ // Complex expressions like function calls - cannot normalize
74
+ return null;
75
+ }
76
+
77
+ // Add the next quasi
78
+ if (templateNode.quasis[i + 1]) {
79
+ templateStr += templateNode.quasis[i + 1].value.cooked || '';
80
+ }
81
+ }
82
+
83
+ // Must be a valid route pattern (start with /)
84
+ if (!templateStr.startsWith('/')) {
85
+ return null;
86
+ }
87
+
88
+ // Normalize to example path
89
+ const normalized = normalizeTemplateLiteral(templateStr);
90
+ if (normalized) {
91
+ return {
92
+ pattern: templateStr,
93
+ examplePath: normalized.examplePath,
94
+ isDynamic: true,
95
+ originalPattern: normalized.originalPattern
96
+ };
97
+ }
98
+
99
+ return null;
100
+ }
101
+
48
102
  /**
49
103
  * Extracts PROVEN navigation contracts from JSX elements and imperative calls.
50
104
  *
51
- * Supported patterns (all require static string literals):
52
- * - Next.js: <Link href="/about">
53
- * - React Router: <Link to="/about"> or <NavLink to="/about">
105
+ * Supported patterns (all require static string literals or template patterns):
106
+ * - Next.js: <Link href="/about"> or <Link href={`/about`}>
107
+ * - React Router: <Link to="/about"> or <RouterLink :to="`/users/${id}`">
54
108
  * - Plain JSX: <a href="/about">
55
- * - Imperative: navigate("/about"), router.push("/about")
109
+ * - Imperative: navigate("/about"), router.push("/about"), router.push(`/users/${id}`)
56
110
  *
57
111
  * Returns array of contracts with:
58
112
  * - kind: 'NAVIGATION'
59
- * - targetPath: string
113
+ * - targetPath: string (example path for dynamic routes)
114
+ * - originalPattern: string (original pattern for dynamic routes)
115
+ * - isDynamic: boolean (true if dynamic route)
60
116
  * - sourceFile: string
61
117
  * - element: 'Link' | 'NavLink' | 'a' | 'navigate' | 'router'
62
118
  * - attribute: 'href' | 'to' (for runtime matching)
@@ -138,6 +194,23 @@ function extractContractsFromFile(filePath, fileContent) {
138
194
  imperativeOnly: true // Cannot match to DOM element
139
195
  });
140
196
  }
197
+ } else if (firstArg && firstArg.type === 'TemplateLiteral') {
198
+ // Template literal: navigate(`/users/${id}`)
199
+ const templatePattern = extractTemplatePattern(firstArg);
200
+ if (templatePattern) {
201
+ contracts.push({
202
+ kind: 'NAVIGATION',
203
+ targetPath: templatePattern.examplePath,
204
+ originalPattern: templatePattern.pattern,
205
+ isDynamic: true,
206
+ sourceFile: filePath,
207
+ element: 'navigate',
208
+ attribute: null,
209
+ proof: ExpectationProof.PROVEN_EXPECTATION,
210
+ line: path.node.loc?.start.line || null,
211
+ imperativeOnly: true // Cannot match to DOM element
212
+ });
213
+ }
141
214
  }
142
215
  }
143
216
 
@@ -162,6 +235,23 @@ function extractContractsFromFile(filePath, fileContent) {
162
235
  imperativeOnly: true // Cannot match to DOM element
163
236
  });
164
237
  }
238
+ } else if (firstArg && firstArg.type === 'TemplateLiteral') {
239
+ // Template literal: router.push(`/users/${id}`)
240
+ const templatePattern = extractTemplatePattern(firstArg);
241
+ if (templatePattern) {
242
+ contracts.push({
243
+ kind: 'NAVIGATION',
244
+ targetPath: templatePattern.examplePath,
245
+ originalPattern: templatePattern.pattern,
246
+ isDynamic: true,
247
+ sourceFile: filePath,
248
+ element: 'router',
249
+ attribute: null,
250
+ proof: ExpectationProof.PROVEN_EXPECTATION,
251
+ line: path.node.loc?.start.line || null,
252
+ imperativeOnly: true // Cannot match to DOM element
253
+ });
254
+ }
165
255
  }
166
256
  }
167
257
  }