@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
@@ -1,24 +1,12 @@
1
1
  import { readdirSync, readFileSync, statSync } from 'fs';
2
2
  import { join, relative, resolve } from 'path';
3
3
  import { expIdFromHash, compareExpectations } from './idgen.js';
4
- import { detectNetworkCallsAST } from './ast-network-detector.js';
5
- import { detectUseStatePromises } from './ast-usestate-detector.js';
6
- import { detectInteractiveElementsAST } from './ast-interactive-detector.js';
7
- import { extractVueSFC, extractTemplateBindings, mapTemplateHandlersToScript } from './vue-sfc-extractor.js';
8
- import { detectVueNavigationPromises } from './vue-navigation-detector.js';
9
- import { detectVueStatePromises } from './vue-state-detector.js';
10
- import { extractSvelteSFC, extractTemplateBindings as extractSvelteTemplateBindings, mapTemplateHandlersToScript as mapSvelteTemplateHandlersToScript } from './svelte-sfc-extractor.js';
11
- import { detectSvelteNavigation } from './svelte-navigation-detector.js';
12
- import { detectSvelteNetwork } from './svelte-network-detector.js';
13
- import { detectSvelteState } from './svelte-state-detector.js';
14
- import { extractAngularComponent, extractTemplateBindings as extractAngularTemplateBindings, mapTemplateHandlersToClass } from './angular-component-extractor.js';
15
- import { detectAngularNavigation } from './angular-navigation-detector.js';
16
- import { detectAngularNetwork } from './angular-network-detector.js';
17
- import { detectAngularState } from './angular-state-detector.js';
4
+ import { extractPromisesFromAST } from './ast-promise-extractor.js';
18
5
 
19
6
  /**
20
7
  * Static Expectation Extractor
21
- * Extracts explicit, static expectations from source files
8
+ * PHASE H2/M2: AST-based promise extraction
9
+ * Extracts explicit, static expectations from source files using AST parsing
22
10
  */
23
11
 
24
12
  export async function extractExpectations(projectProfile, _srcPath) {
@@ -75,7 +63,16 @@ function getScanPaths(projectProfile, sourceRoot) {
75
63
  }
76
64
 
77
65
  if (framework === 'react-vite' || framework === 'react-cra') {
78
- return [resolve(sourceRoot, 'src')];
66
+ const srcPath = resolve(sourceRoot, 'src');
67
+ try {
68
+ if (statSync(srcPath).isDirectory()) {
69
+ return [srcPath];
70
+ }
71
+ } catch (error) {
72
+ // src doesn't exist - scan root for React files
73
+ }
74
+ // Fallback: scan root directory for React files
75
+ return [sourceRoot];
79
76
  }
80
77
 
81
78
  if (framework === 'static-html') {
@@ -92,7 +89,7 @@ function getScanPaths(projectProfile, sourceRoot) {
92
89
  }
93
90
  }
94
91
 
95
- // Unknown framework - scan src if it exists
92
+ // Unknown framework - scan src if it exists, otherwise scan root
96
93
  const srcPath = resolve(sourceRoot, 'src');
97
94
  try {
98
95
  if (statSync(srcPath).isDirectory()) {
@@ -102,7 +99,8 @@ function getScanPaths(projectProfile, sourceRoot) {
102
99
  // src doesn't exist
103
100
  }
104
101
 
105
- return [];
102
+ // Fallback: scan root directory
103
+ return [sourceRoot];
106
104
  }
107
105
 
108
106
  /**
@@ -162,8 +160,8 @@ function shouldSkipDirectory(name) {
162
160
  * Check if file should be scanned
163
161
  */
164
162
  function shouldScanFile(name) {
165
- const extensions = ['.js', '.jsx', '.ts', '.tsx', '.html', '.mjs', '.vue', '.svelte'];
166
- return extensions.some(ext => name.endsWith(ext)) || (name.endsWith('.component.ts') || name.endsWith('.service.ts'));
163
+ const extensions = ['.js', '.jsx', '.ts', '.tsx', '.html', '.mjs'];
164
+ return extensions.some(ext => name.endsWith(ext));
167
165
  }
168
166
 
169
167
  /**
@@ -177,18 +175,15 @@ function scanFile(filePath, sourceRoot, skipped) {
177
175
  const relPath = relative(sourceRoot, filePath);
178
176
 
179
177
  if (filePath.endsWith('.html')) {
178
+ // HTML files: Use regex-based extraction (static HTML fallback)
180
179
  const htmlExpectations = extractHtmlExpectations(content, filePath, relPath);
181
180
  expectations.push(...htmlExpectations);
182
- } else if (filePath.endsWith('.vue')) {
183
- const vueExpectations = extractVueExpectations(content, filePath, relPath, skipped);
184
- expectations.push(...vueExpectations);
185
- } else if (filePath.endsWith('.svelte')) {
186
- const svelteExpectations = extractSvelteExpectations(content, filePath, relPath, skipped);
187
- expectations.push(...svelteExpectations);
188
- } else if (filePath.endsWith('.component.ts') || (filePath.endsWith('.ts') && content.includes('@Component'))) {
189
- const angularExpectations = extractAngularExpectations(content, filePath, relPath, skipped);
190
- expectations.push(...angularExpectations);
191
181
  } else {
182
+ // JS/JSX/TS/TSX files: Use AST-based extraction (PHASE H2)
183
+ const astPromises = extractPromisesFromAST(content, filePath, relPath);
184
+ expectations.push(...astPromises);
185
+
186
+ // Legacy regex-based extraction (preserved for navigation/network/state)
192
187
  const jsExpectations = extractJsExpectations(content, filePath, relPath, skipped);
193
188
  expectations.push(...jsExpectations);
194
189
  }
@@ -201,13 +196,12 @@ function scanFile(filePath, sourceRoot, skipped) {
201
196
 
202
197
  /**
203
198
  * Extract expectations from HTML files
204
- * PHASE 9: Enhanced to extract network calls from <script> tags using AST
199
+ * PHASE H2: Enhanced with button, form, and validation detection
205
200
  */
206
201
  function extractHtmlExpectations(content, filePath, relPath) {
207
202
  const expectations = [];
208
- const skipped = { dynamic: 0, computed: 0, external: 0, parseError: 0, other: 0 };
209
203
 
210
- // Extract <a href="/path"> links
204
+ // Extract <a href="/path"> links (navigation)
211
205
  const hrefRegex = /<a\s+[^>]*href=["']([^"']+)["']/gi;
212
206
  let match;
213
207
 
@@ -233,233 +227,143 @@ function extractHtmlExpectations(content, filePath, relPath) {
233
227
  }
234
228
  }
235
229
 
236
- // PHASE 9: Extract JavaScript from <script> tags and detect network calls
237
- const scriptRegex = /<script[^>]*>([\s\S]*?)<\/script>/gi;
238
- let scriptMatch;
239
- let scriptIndex = 0;
240
-
241
- while ((scriptMatch = scriptRegex.exec(content)) !== null) {
242
- const scriptContent = scriptMatch[1].trim();
243
- const scriptStartIndex = scriptMatch.index;
244
- const scriptStartLine = content.substring(0, scriptStartIndex).split('\n').length;
230
+ // Extract <button> elements (interaction promise)
231
+ const buttonRegex = /<button\s+[^>]*>/gi;
232
+ while ((match = buttonRegex.exec(content)) !== null) {
233
+ const lineNum = content.substring(0, match.index).split('\n').length;
234
+ const buttonText = extractTextFromTag(content, match.index, 'button');
245
235
 
246
- // Skip empty scripts and external scripts
247
- if (!scriptContent || scriptMatch[0].includes('src=')) {
248
- continue;
249
- }
236
+ expectations.push({
237
+ category: 'button',
238
+ type: 'interaction',
239
+ promise: {
240
+ kind: 'click',
241
+ value: buttonText || 'button click',
242
+ },
243
+ source: {
244
+ file: relPath,
245
+ line: lineNum,
246
+ column: match.index - content.lastIndexOf('\n', match.index),
247
+ },
248
+ selector: buttonText ? `button:contains("${buttonText}")` : 'button',
249
+ action: 'click',
250
+ expectedOutcome: 'ui-change',
251
+ confidenceHint: 'low',
252
+ });
253
+ }
254
+
255
+ // Extract <form> elements (submission promise)
256
+ const formRegex = /<form\s+[^>]*>/gi;
257
+ while ((match = formRegex.exec(content)) !== null) {
258
+ const lineNum = content.substring(0, match.index).split('\n').length;
259
+ const formTag = match[0];
250
260
 
251
- // Use AST-based network detection on script content
252
- try {
253
- const networkCalls = detectNetworkCallsAST(scriptContent, filePath, relPath);
254
-
255
- for (const call of networkCalls) {
256
- const url = call.url;
257
-
258
- // Skip dynamic URLs but count them
259
- if (url === '<dynamic>') {
260
- skipped.dynamic++;
261
- continue;
262
- }
263
-
264
- // Only extract absolute URLs (http/https) or relative API paths
265
- if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('/api/')) {
266
- // Calculate correct line number in HTML file
267
- const callLineInScript = call.location.line || 1;
268
- const htmlLineNumber = scriptStartLine + callLineInScript - 1;
269
-
270
- expectations.push({
271
- type: 'network',
272
- promise: {
273
- kind: 'request',
274
- value: url,
275
- method: call.method,
276
- },
277
- source: {
278
- file: relPath,
279
- line: htmlLineNumber,
280
- column: call.location.column || 0,
281
- context: call.context,
282
- astSource: call.astSource || null,
283
- },
284
- confidence: call.isUIBound ? 1.0 : 0.9,
285
- metadata: {
286
- networkKind: call.kind,
287
- isUIBound: call.isUIBound || false,
288
- astSource: call.astSource || null,
289
- },
290
- });
291
- } else {
292
- skipped.external++;
293
- }
294
- }
295
- } catch (error) {
296
- skipped.parseError++;
297
- }
261
+ // Check for action attribute
262
+ const actionMatch = /action=["']([^"']+)["']/.exec(formTag);
263
+ const hasAction = actionMatch && actionMatch[1];
298
264
 
299
- scriptIndex++;
265
+ expectations.push({
266
+ category: 'form',
267
+ type: 'interaction',
268
+ promise: {
269
+ kind: 'submit',
270
+ value: hasAction ? `form submit to ${actionMatch[1]}` : 'form submission',
271
+ },
272
+ source: {
273
+ file: relPath,
274
+ line: lineNum,
275
+ column: match.index - content.lastIndexOf('\n', match.index),
276
+ },
277
+ selector: hasAction ? `form[action="${actionMatch[1]}"]` : 'form',
278
+ action: 'submit',
279
+ expectedOutcome: hasAction ? 'navigation' : 'ui-change',
280
+ confidenceHint: hasAction ? 'medium' : 'low',
281
+ });
300
282
  }
301
283
 
302
- return expectations;
303
- }
304
-
305
- /**
306
- * Extract expectations from JavaScript/TypeScript files
307
- */
308
- function extractJsExpectations(content, filePath, relPath, skipped) {
309
- const expectations = [];
310
- const lines = content.split('\n');
311
-
312
- // PHASE 9: AST-based network detection (handles nested contexts)
313
- const networkCalls = detectNetworkCallsAST(content, filePath, relPath);
314
- for (const call of networkCalls) {
315
- const url = call.url;
316
-
317
- // Skip dynamic URLs but count them
318
- if (url === '<dynamic>') {
319
- skipped.dynamic++;
320
- continue;
321
- }
322
-
323
- // Only extract absolute URLs (http/https)
324
- if (url.startsWith('http://') || url.startsWith('https://')) {
325
- expectations.push({
326
- type: 'network',
327
- promise: {
328
- kind: 'request',
329
- value: url,
330
- method: call.method,
331
- },
332
- source: {
333
- file: relPath,
334
- line: call.location.line,
335
- column: call.location.column,
336
- context: call.context,
337
- // PHASE 9: Include AST source code for evidence
338
- astSource: call.astSource || null,
339
- },
340
- confidence: call.isUIBound ? 1.0 : 0.9, // Higher confidence for UI-bound handlers
341
- metadata: {
342
- networkKind: call.kind,
343
- isUIBound: call.isUIBound || false,
344
- // PHASE 9: Include AST source in metadata for evidence generation
345
- astSource: call.astSource || null,
346
- },
347
- });
348
- } else {
349
- // Relative or non-http URLs
350
- skipped.external++;
351
- }
352
- }
353
-
354
- // PHASE 10: AST-based useState detection (state-driven UI promises)
355
- const statePromises = detectUseStatePromises(content, filePath, relPath);
356
- for (const statePromise of statePromises) {
357
- // PHASE 10: Extract context and AST source from first setter call
358
- const firstSetterCall = statePromise.metadata.setterCalls?.[0];
359
- const context = firstSetterCall?.context || `component:${statePromise.componentName}`;
360
- const astSource = firstSetterCall?.astSource || null;
361
- const isUIBound = firstSetterCall?.isUIBound || false;
284
+ // Extract required input fields (validation promise)
285
+ const requiredRegex = /<input\s+[^>]*required[^>]*>/gi;
286
+ while ((match = requiredRegex.exec(content)) !== null) {
287
+ const lineNum = content.substring(0, match.index).split('\n').length;
288
+ const inputTag = match[0];
289
+
290
+ // Extract name or id for selector
291
+ const nameMatch = /name=["']([^"']+)["']/.exec(inputTag);
292
+ const idMatch = /id=["']([^"']+)["']/.exec(inputTag);
293
+ const selector = nameMatch ? `input[name="${nameMatch[1]}"]` :
294
+ idMatch ? `input#${idMatch[1]}` : 'input[required]';
362
295
 
363
296
  expectations.push({
364
- type: 'state',
297
+ category: 'validation',
298
+ type: 'feedback',
365
299
  promise: {
366
- kind: 'ui_state_change',
367
- value: `${statePromise.stateName} in ${statePromise.componentName}`,
368
- stateName: statePromise.stateName,
369
- setterName: statePromise.setterName,
300
+ kind: 'validation',
301
+ value: 'required field validation',
370
302
  },
371
303
  source: {
372
304
  file: relPath,
373
- line: statePromise.location.line,
374
- column: statePromise.location.column,
375
- context: context, // PHASE 10: Enhanced context (handler/hook/function)
376
- astSource: astSource, // PHASE 10: AST source for evidence
377
- },
378
- confidence: isUIBound ? 1.0 : 0.9, // PHASE 10: Higher confidence for UI-bound handlers
379
- metadata: {
380
- componentName: statePromise.componentName,
381
- setterCallCount: statePromise.setterCallCount,
382
- jsxUsageCount: statePromise.jsxUsageCount,
383
- usageTypes: statePromise.usageTypes,
384
- hasUpdaterFunction: statePromise.metadata.hasUpdaterFunction,
385
- // PHASE 10: Include all setter calls with context and AST source
386
- setterCalls: statePromise.metadata.setterCalls || [],
387
- isUIBound: isUIBound,
388
- astSource: astSource, // PHASE 10: AST source in metadata for evidence generation
305
+ line: lineNum,
306
+ column: match.index - content.lastIndexOf('\n', match.index),
389
307
  },
308
+ selector,
309
+ action: 'observe',
310
+ expectedOutcome: 'feedback',
311
+ confidenceHint: 'medium',
390
312
  });
391
313
  }
392
314
 
393
- // PHASE 11: AST-based interactive element detection (elements without href)
394
- const interactiveElements = detectInteractiveElementsAST(content, filePath, relPath);
395
- for (const element of interactiveElements) {
396
- // Only create expectations for elements with navigation promises
397
- if (element.navigationPromise) {
398
- const promise = element.navigationPromise;
399
-
400
- // Skip dynamic targets
401
- if (promise.target === '<dynamic>') {
402
- skipped.dynamic++;
403
- continue;
404
- }
405
-
406
- expectations.push({
407
- type: 'navigation',
408
- promise: {
409
- kind: 'navigate',
410
- value: promise.target,
411
- method: promise.method || 'push',
412
- },
413
- source: {
414
- file: relPath,
415
- line: element.location.line,
416
- column: element.location.column,
417
- context: element.context,
418
- astSource: element.astSource || promise.astSource || null,
419
- },
420
- confidence: element.isUIBound ? 1.0 : 0.9,
421
- metadata: {
422
- tagName: element.tagName,
423
- role: element.role,
424
- hasOnClick: element.hasOnClick,
425
- hasOnSubmit: element.hasOnSubmit,
426
- isRouterLink: element.isRouterLink,
427
- navigationType: promise.type,
428
- selectorHint: element.selectorHint,
429
- isUIBound: element.isUIBound,
430
- astSource: element.astSource || promise.astSource || null,
431
- },
432
- });
433
- } else if (element.isRouterLink && (element.linkTo || element.linkHref)) {
434
- // Router Link component with static href/to
435
- const target = element.linkTo || element.linkHref;
436
-
437
- if (target && target !== '<dynamic>') {
438
- expectations.push({
439
- type: 'navigation',
440
- promise: {
441
- kind: 'navigate',
442
- value: target,
443
- },
444
- source: {
445
- file: relPath,
446
- line: element.location.line,
447
- column: element.location.column,
448
- context: element.context,
449
- astSource: element.astSource || null,
450
- },
451
- confidence: 1.0,
452
- metadata: {
453
- tagName: element.tagName,
454
- isRouterLink: true,
455
- selectorHint: element.selectorHint,
456
- astSource: element.astSource || null,
457
- },
458
- });
459
- }
460
- }
315
+ // Extract aria-live regions (feedback promise)
316
+ const ariaLiveRegex = /<[^>]+aria-live=["']([^"']+)["'][^>]*>/gi;
317
+ while ((match = ariaLiveRegex.exec(content)) !== null) {
318
+ const lineNum = content.substring(0, match.index).split('\n').length;
319
+
320
+ expectations.push({
321
+ category: 'feedback',
322
+ type: 'feedback',
323
+ promise: {
324
+ kind: 'ui-feedback',
325
+ value: 'live region update',
326
+ },
327
+ source: {
328
+ file: relPath,
329
+ line: lineNum,
330
+ column: match.index - content.lastIndexOf('\n', match.index),
331
+ },
332
+ selector: '[aria-live]',
333
+ action: 'observe',
334
+ expectedOutcome: 'feedback',
335
+ confidenceHint: 'medium',
336
+ });
461
337
  }
462
338
 
339
+ return expectations;
340
+ }
341
+
342
+ /**
343
+ * Extract text content from HTML tag
344
+ */
345
+ function extractTextFromTag(content, startIndex, tagName) {
346
+ const closeTagRegex = new RegExp(`</${tagName}>`, 'i');
347
+ const closeMatch = closeTagRegex.exec(content.substring(startIndex));
348
+
349
+ if (!closeMatch) return '';
350
+
351
+ const endOfOpenTag = content.indexOf('>', startIndex);
352
+ if (endOfOpenTag === -1) return '';
353
+
354
+ const textContent = content.substring(endOfOpenTag + 1, startIndex + closeMatch.index);
355
+
356
+ // Strip HTML tags and trim
357
+ return textContent.replace(/<[^>]+>/g, '').trim();
358
+ }
359
+
360
+ /**
361
+ * Extract expectations from JavaScript/TypeScript files
362
+ */
363
+ function extractJsExpectations(content, filePath, relPath, skipped) {
364
+ const expectations = [];
365
+ const lines = content.split('\n');
366
+
463
367
  lines.forEach((line, lineIdx) => {
464
368
  const lineNum = lineIdx + 1;
465
369
 
@@ -518,6 +422,77 @@ function extractJsExpectations(content, filePath, relPath, skipped) {
518
422
  }
519
423
  }
520
424
 
425
+ // Extract fetch("https://...")
426
+ const fetchRegex = /fetch\(["']([^"']+)["']\)/g;
427
+ while ((match = fetchRegex.exec(line)) !== null) {
428
+ const url = match[1];
429
+
430
+ // Only extract absolute URLs (https://)
431
+ if (url.startsWith('http://') || url.startsWith('https://')) {
432
+ expectations.push({
433
+ type: 'network',
434
+ promise: {
435
+ kind: 'request',
436
+ value: url,
437
+ },
438
+ source: {
439
+ file: relPath,
440
+ line: lineNum,
441
+ column: match.index,
442
+ },
443
+ confidence: 1.0,
444
+ });
445
+ } else if (!url.includes('${') && !url.includes('+') && !url.includes('`')) {
446
+ skipped.external++;
447
+ } else {
448
+ skipped.dynamic++;
449
+ }
450
+ }
451
+
452
+ // Extract axios.get/post("https://...")
453
+ const axiosRegex = /axios\.(get|post|put|delete|patch)\(["']([^"']+)["']\)/g;
454
+ while ((match = axiosRegex.exec(line)) !== null) {
455
+ const url = match[2];
456
+
457
+ if (url.startsWith('http://') || url.startsWith('https://')) {
458
+ expectations.push({
459
+ type: 'network',
460
+ promise: {
461
+ kind: 'request',
462
+ value: url,
463
+ },
464
+ source: {
465
+ file: relPath,
466
+ line: lineNum,
467
+ column: match.index,
468
+ },
469
+ confidence: 1.0,
470
+ });
471
+ } else if (!url.includes('${') && !url.includes('+') && !url.includes('`')) {
472
+ skipped.external++;
473
+ } else {
474
+ skipped.dynamic++;
475
+ }
476
+ }
477
+
478
+ // Extract useState setters
479
+ const useStateRegex = /useState\([^)]*\)/g;
480
+ if ((match = useStateRegex.exec(line)) !== null) {
481
+ expectations.push({
482
+ type: 'state',
483
+ promise: {
484
+ kind: 'state_mutation',
485
+ value: 'state management',
486
+ },
487
+ source: {
488
+ file: relPath,
489
+ line: lineNum,
490
+ column: match.index,
491
+ },
492
+ confidence: 0.8,
493
+ });
494
+ }
495
+
521
496
  // Extract Redux dispatch calls
522
497
  const dispatchRegex = /dispatch\(\{/g;
523
498
  if ((match = dispatchRegex.exec(line)) !== null) {
@@ -557,128 +532,3 @@ function extractJsExpectations(content, filePath, relPath, skipped) {
557
532
 
558
533
  return expectations;
559
534
  }
560
-
561
- /**
562
- * PHASE 20: Extract expectations from Vue SFC files
563
- */
564
- function extractVueExpectations(content, filePath, relPath, skipped) {
565
- const expectations = [];
566
-
567
- try {
568
- // Extract SFC blocks
569
- const sfc = extractVueSFC(content);
570
-
571
- // Extract template bindings
572
- let templateBindings = null;
573
- if (sfc.template) {
574
- templateBindings = extractTemplateBindings(sfc.template.content);
575
- }
576
-
577
- // Process each script block
578
- for (const scriptBlock of sfc.scriptBlocks) {
579
- const scriptContent = scriptBlock.content;
580
- const startLine = scriptBlock.startLine;
581
-
582
- // Map template handlers to script functions
583
- let handlerMap = new Map();
584
- if (templateBindings) {
585
- handlerMap = mapTemplateHandlersToScript(templateBindings, [scriptBlock]);
586
- }
587
-
588
- // PHASE 20: Detect Vue navigation promises (router-link, router.push/replace)
589
- const navigationPromises = detectVueNavigationPromises(
590
- scriptContent,
591
- filePath,
592
- relPath,
593
- scriptBlock,
594
- templateBindings
595
- );
596
-
597
- for (const navPromise of navigationPromises) {
598
- // Adjust line numbers for SFC
599
- navPromise.source.line = startLine + (navPromise.source.line || 1) - 1;
600
- expectations.push(navPromise);
601
- }
602
-
603
- // PHASE 20: Detect network calls in Vue script blocks
604
- const networkCalls = detectNetworkCallsAST(scriptContent, filePath, relPath);
605
- for (const call of networkCalls) {
606
- const url = call.url;
607
-
608
- if (url === '<dynamic>') {
609
- skipped.dynamic++;
610
- continue;
611
- }
612
-
613
- // Check if handler is UI-bound via template
614
- const isUIBound = handlerMap.has(call.handlerName || '');
615
-
616
- if (url.startsWith('http://') || url.startsWith('https://') || url.startsWith('/api/')) {
617
- expectations.push({
618
- type: 'network',
619
- promise: {
620
- kind: 'request',
621
- value: url,
622
- method: call.method,
623
- },
624
- source: {
625
- file: relPath,
626
- line: startLine + (call.location.line || 1) - 1,
627
- column: call.location.column || 0,
628
- context: call.context,
629
- astSource: call.astSource || null,
630
- },
631
- confidence: isUIBound ? 1.0 : 0.9,
632
- metadata: {
633
- networkKind: call.kind,
634
- isUIBound: isUIBound || false,
635
- astSource: call.astSource || null,
636
- },
637
- });
638
- } else {
639
- skipped.external++;
640
- }
641
- }
642
-
643
- // PHASE 20: Detect Vue state promises (ref/reactive)
644
- if (templateBindings) {
645
- const statePromises = detectVueStatePromises(
646
- scriptContent,
647
- filePath,
648
- relPath,
649
- scriptBlock,
650
- templateBindings
651
- );
652
-
653
- for (const statePromise of statePromises) {
654
- statePromise.source.line = startLine + (statePromise.source.line || 1) - 1;
655
- expectations.push(statePromise);
656
- }
657
- }
658
-
659
- // PHASE 20: Detect interactive elements from template
660
- if (templateBindings) {
661
- for (const routerLink of templateBindings.routerLinks) {
662
- expectations.push({
663
- type: 'navigation',
664
- promise: {
665
- kind: 'navigate',
666
- value: routerLink.to,
667
- },
668
- source: {
669
- file: relPath,
670
- line: sfc.template.startLine,
671
- column: 0,
672
- context: 'template',
673
- },
674
- confidence: 1.0,
675
- });
676
- }
677
- }
678
- }
679
- } catch (error) {
680
- skipped.parseError++;
681
- }
682
-
683
- return expectations;
684
- }