@veraxhq/verax 0.2.1 → 0.3.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (152) hide show
  1. package/README.md +14 -18
  2. package/bin/verax.js +7 -0
  3. package/package.json +3 -3
  4. package/src/cli/commands/baseline.js +104 -0
  5. package/src/cli/commands/default.js +79 -25
  6. package/src/cli/commands/ga.js +243 -0
  7. package/src/cli/commands/gates.js +95 -0
  8. package/src/cli/commands/inspect.js +131 -2
  9. package/src/cli/commands/release-check.js +213 -0
  10. package/src/cli/commands/run.js +246 -35
  11. package/src/cli/commands/security-check.js +211 -0
  12. package/src/cli/commands/truth.js +114 -0
  13. package/src/cli/entry.js +304 -67
  14. package/src/cli/util/angular-component-extractor.js +179 -0
  15. package/src/cli/util/angular-navigation-detector.js +141 -0
  16. package/src/cli/util/angular-network-detector.js +161 -0
  17. package/src/cli/util/angular-state-detector.js +162 -0
  18. package/src/cli/util/ast-interactive-detector.js +546 -0
  19. package/src/cli/util/ast-network-detector.js +603 -0
  20. package/src/cli/util/ast-usestate-detector.js +602 -0
  21. package/src/cli/util/bootstrap-guard.js +86 -0
  22. package/src/cli/util/determinism-runner.js +123 -0
  23. package/src/cli/util/determinism-writer.js +129 -0
  24. package/src/cli/util/env-url.js +4 -0
  25. package/src/cli/util/expectation-extractor.js +369 -73
  26. package/src/cli/util/findings-writer.js +126 -16
  27. package/src/cli/util/learn-writer.js +3 -1
  28. package/src/cli/util/observe-writer.js +3 -1
  29. package/src/cli/util/paths.js +3 -12
  30. package/src/cli/util/project-discovery.js +3 -0
  31. package/src/cli/util/project-writer.js +3 -1
  32. package/src/cli/util/run-resolver.js +64 -0
  33. package/src/cli/util/source-requirement.js +55 -0
  34. package/src/cli/util/summary-writer.js +1 -0
  35. package/src/cli/util/svelte-navigation-detector.js +163 -0
  36. package/src/cli/util/svelte-network-detector.js +80 -0
  37. package/src/cli/util/svelte-sfc-extractor.js +147 -0
  38. package/src/cli/util/svelte-state-detector.js +243 -0
  39. package/src/cli/util/vue-navigation-detector.js +177 -0
  40. package/src/cli/util/vue-sfc-extractor.js +162 -0
  41. package/src/cli/util/vue-state-detector.js +215 -0
  42. package/src/verax/cli/finding-explainer.js +56 -3
  43. package/src/verax/core/artifacts/registry.js +154 -0
  44. package/src/verax/core/artifacts/verifier.js +980 -0
  45. package/src/verax/core/baseline/baseline.enforcer.js +137 -0
  46. package/src/verax/core/baseline/baseline.snapshot.js +231 -0
  47. package/src/verax/core/capabilities/gates.js +499 -0
  48. package/src/verax/core/capabilities/registry.js +475 -0
  49. package/src/verax/core/confidence/confidence-compute.js +137 -0
  50. package/src/verax/core/confidence/confidence-invariants.js +234 -0
  51. package/src/verax/core/confidence/confidence-report-writer.js +112 -0
  52. package/src/verax/core/confidence/confidence-weights.js +44 -0
  53. package/src/verax/core/confidence/confidence.defaults.js +65 -0
  54. package/src/verax/core/confidence/confidence.loader.js +79 -0
  55. package/src/verax/core/confidence/confidence.schema.js +94 -0
  56. package/src/verax/core/confidence-engine-refactor.js +484 -0
  57. package/src/verax/core/confidence-engine.js +486 -0
  58. package/src/verax/core/confidence-engine.js.backup +471 -0
  59. package/src/verax/core/contracts/index.js +29 -0
  60. package/src/verax/core/contracts/types.js +185 -0
  61. package/src/verax/core/contracts/validators.js +381 -0
  62. package/src/verax/core/decision-snapshot.js +30 -3
  63. package/src/verax/core/decisions/decision.trace.js +276 -0
  64. package/src/verax/core/determinism/contract-writer.js +89 -0
  65. package/src/verax/core/determinism/contract.js +139 -0
  66. package/src/verax/core/determinism/diff.js +364 -0
  67. package/src/verax/core/determinism/engine.js +221 -0
  68. package/src/verax/core/determinism/finding-identity.js +148 -0
  69. package/src/verax/core/determinism/normalize.js +438 -0
  70. package/src/verax/core/determinism/report-writer.js +92 -0
  71. package/src/verax/core/determinism/run-fingerprint.js +118 -0
  72. package/src/verax/core/dynamic-route-intelligence.js +528 -0
  73. package/src/verax/core/evidence/evidence-capture-service.js +307 -0
  74. package/src/verax/core/evidence/evidence-intent-ledger.js +165 -0
  75. package/src/verax/core/evidence-builder.js +487 -0
  76. package/src/verax/core/execution-mode-context.js +77 -0
  77. package/src/verax/core/execution-mode-detector.js +190 -0
  78. package/src/verax/core/failures/exit-codes.js +86 -0
  79. package/src/verax/core/failures/failure-summary.js +76 -0
  80. package/src/verax/core/failures/failure.factory.js +225 -0
  81. package/src/verax/core/failures/failure.ledger.js +132 -0
  82. package/src/verax/core/failures/failure.types.js +196 -0
  83. package/src/verax/core/failures/index.js +10 -0
  84. package/src/verax/core/ga/ga-report-writer.js +43 -0
  85. package/src/verax/core/ga/ga.artifact.js +49 -0
  86. package/src/verax/core/ga/ga.contract.js +434 -0
  87. package/src/verax/core/ga/ga.enforcer.js +86 -0
  88. package/src/verax/core/guardrails/guardrails-report-writer.js +109 -0
  89. package/src/verax/core/guardrails/policy.defaults.js +210 -0
  90. package/src/verax/core/guardrails/policy.loader.js +83 -0
  91. package/src/verax/core/guardrails/policy.schema.js +110 -0
  92. package/src/verax/core/guardrails/truth-reconciliation.js +136 -0
  93. package/src/verax/core/guardrails-engine.js +505 -0
  94. package/src/verax/core/observe/run-timeline.js +316 -0
  95. package/src/verax/core/perf/perf.contract.js +186 -0
  96. package/src/verax/core/perf/perf.display.js +65 -0
  97. package/src/verax/core/perf/perf.enforcer.js +91 -0
  98. package/src/verax/core/perf/perf.monitor.js +209 -0
  99. package/src/verax/core/perf/perf.report.js +198 -0
  100. package/src/verax/core/pipeline-tracker.js +238 -0
  101. package/src/verax/core/product-definition.js +127 -0
  102. package/src/verax/core/release/provenance.builder.js +271 -0
  103. package/src/verax/core/release/release-report-writer.js +40 -0
  104. package/src/verax/core/release/release.enforcer.js +159 -0
  105. package/src/verax/core/release/reproducibility.check.js +221 -0
  106. package/src/verax/core/release/sbom.builder.js +283 -0
  107. package/src/verax/core/report/cross-index.js +192 -0
  108. package/src/verax/core/report/human-summary.js +222 -0
  109. package/src/verax/core/route-intelligence.js +419 -0
  110. package/src/verax/core/security/secrets.scan.js +326 -0
  111. package/src/verax/core/security/security-report.js +50 -0
  112. package/src/verax/core/security/security.enforcer.js +124 -0
  113. package/src/verax/core/security/supplychain.defaults.json +38 -0
  114. package/src/verax/core/security/supplychain.policy.js +326 -0
  115. package/src/verax/core/security/vuln.scan.js +265 -0
  116. package/src/verax/core/truth/truth.certificate.js +250 -0
  117. package/src/verax/core/ui-feedback-intelligence.js +515 -0
  118. package/src/verax/detect/confidence-engine.js +628 -40
  119. package/src/verax/detect/confidence-helper.js +33 -0
  120. package/src/verax/detect/detection-engine.js +18 -1
  121. package/src/verax/detect/dynamic-route-findings.js +335 -0
  122. package/src/verax/detect/expectation-chain-detector.js +417 -0
  123. package/src/verax/detect/expectation-model.js +3 -1
  124. package/src/verax/detect/findings-writer.js +141 -5
  125. package/src/verax/detect/index.js +229 -5
  126. package/src/verax/detect/journey-stall-detector.js +558 -0
  127. package/src/verax/detect/route-findings.js +218 -0
  128. package/src/verax/detect/ui-feedback-findings.js +207 -0
  129. package/src/verax/detect/verdict-engine.js +57 -3
  130. package/src/verax/detect/view-switch-correlator.js +242 -0
  131. package/src/verax/index.js +413 -45
  132. package/src/verax/learn/action-contract-extractor.js +682 -64
  133. package/src/verax/learn/route-validator.js +4 -1
  134. package/src/verax/observe/index.js +88 -843
  135. package/src/verax/observe/interaction-runner.js +25 -8
  136. package/src/verax/observe/observe-context.js +205 -0
  137. package/src/verax/observe/observe-helpers.js +191 -0
  138. package/src/verax/observe/observe-runner.js +226 -0
  139. package/src/verax/observe/observers/budget-observer.js +185 -0
  140. package/src/verax/observe/observers/console-observer.js +102 -0
  141. package/src/verax/observe/observers/coverage-observer.js +107 -0
  142. package/src/verax/observe/observers/interaction-observer.js +471 -0
  143. package/src/verax/observe/observers/navigation-observer.js +132 -0
  144. package/src/verax/observe/observers/network-observer.js +87 -0
  145. package/src/verax/observe/observers/safety-observer.js +82 -0
  146. package/src/verax/observe/observers/ui-feedback-observer.js +99 -0
  147. package/src/verax/observe/ui-feedback-detector.js +742 -0
  148. package/src/verax/observe/ui-signal-sensor.js +148 -2
  149. package/src/verax/scan-summary-writer.js +42 -8
  150. package/src/verax/shared/artifact-manager.js +8 -5
  151. package/src/verax/shared/css-spinner-rules.js +204 -0
  152. package/src/verax/shared/view-switch-rules.js +208 -0
@@ -1,6 +1,20 @@
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
18
 
5
19
  /**
6
20
  * Static Expectation Extractor
@@ -148,8 +162,8 @@ function shouldSkipDirectory(name) {
148
162
  * Check if file should be scanned
149
163
  */
150
164
  function shouldScanFile(name) {
151
- const extensions = ['.js', '.jsx', '.ts', '.tsx', '.html', '.mjs'];
152
- return extensions.some(ext => name.endsWith(ext));
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'));
153
167
  }
154
168
 
155
169
  /**
@@ -165,6 +179,15 @@ function scanFile(filePath, sourceRoot, skipped) {
165
179
  if (filePath.endsWith('.html')) {
166
180
  const htmlExpectations = extractHtmlExpectations(content, filePath, relPath);
167
181
  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);
168
191
  } else {
169
192
  const jsExpectations = extractJsExpectations(content, filePath, relPath, skipped);
170
193
  expectations.push(...jsExpectations);
@@ -178,9 +201,11 @@ function scanFile(filePath, sourceRoot, skipped) {
178
201
 
179
202
  /**
180
203
  * Extract expectations from HTML files
204
+ * PHASE 9: Enhanced to extract network calls from <script> tags using AST
181
205
  */
182
206
  function extractHtmlExpectations(content, filePath, relPath) {
183
207
  const expectations = [];
208
+ const skipped = { dynamic: 0, computed: 0, external: 0, parseError: 0, other: 0 };
184
209
 
185
210
  // Extract <a href="/path"> links
186
211
  const hrefRegex = /<a\s+[^>]*href=["']([^"']+)["']/gi;
@@ -208,6 +233,72 @@ function extractHtmlExpectations(content, filePath, relPath) {
208
233
  }
209
234
  }
210
235
 
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;
245
+
246
+ // Skip empty scripts and external scripts
247
+ if (!scriptContent || scriptMatch[0].includes('src=')) {
248
+ continue;
249
+ }
250
+
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
+ }
298
+
299
+ scriptIndex++;
300
+ }
301
+
211
302
  return expectations;
212
303
  }
213
304
 
@@ -218,76 +309,178 @@ function extractJsExpectations(content, filePath, relPath, skipped) {
218
309
  const expectations = [];
219
310
  const lines = content.split('\n');
220
311
 
221
- lines.forEach((line, lineIdx) => {
222
- const lineNum = lineIdx + 1;
223
-
224
- // Skip comments
225
- if (line.trim().startsWith('//') || line.trim().startsWith('*')) {
226
- return;
227
- }
228
-
229
- // Extract Next.js <Link href="/path">
230
- const linkRegex = /<Link\s+[^>]*href=["']([^"']+)["']/g;
231
- let match;
232
- while ((match = linkRegex.exec(line)) !== null) {
233
- const href = match[1];
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;
234
316
 
235
- // Skip dynamic hrefs
236
- if (!href.includes('${') && !href.includes('+') && !href.includes('`')) {
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://')) {
237
325
  expectations.push({
238
- type: 'navigation',
326
+ type: 'network',
239
327
  promise: {
240
- kind: 'navigate',
241
- value: href,
328
+ kind: 'request',
329
+ value: url,
330
+ method: call.method,
242
331
  },
243
332
  source: {
244
333
  file: relPath,
245
- line: lineNum,
246
- column: match.index,
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,
247
346
  },
248
- confidence: 1.0,
249
347
  });
250
348
  } else {
251
- skipped.dynamic++;
349
+ // Relative or non-http URLs
350
+ skipped.external++;
252
351
  }
253
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;
254
362
 
255
- // Extract router.push("/path")
256
- const routerPushRegex = /router\.push\(["']([^"']+)["']\)/g;
257
- while ((match = routerPushRegex.exec(line)) !== null) {
258
- const path = match[1];
363
+ expectations.push({
364
+ type: 'state',
365
+ promise: {
366
+ kind: 'ui_state_change',
367
+ value: `${statePromise.stateName} in ${statePromise.componentName}`,
368
+ stateName: statePromise.stateName,
369
+ setterName: statePromise.setterName,
370
+ },
371
+ source: {
372
+ 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
389
+ },
390
+ });
391
+ }
392
+
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;
259
399
 
260
- if (!path.includes('${') && !path.includes('+') && !path.includes('`')) {
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>') {
261
438
  expectations.push({
262
439
  type: 'navigation',
263
440
  promise: {
264
441
  kind: 'navigate',
265
- value: path,
442
+ value: target,
266
443
  },
267
444
  source: {
268
445
  file: relPath,
269
- line: lineNum,
270
- column: match.index,
446
+ line: element.location.line,
447
+ column: element.location.column,
448
+ context: element.context,
449
+ astSource: element.astSource || null,
271
450
  },
272
451
  confidence: 1.0,
452
+ metadata: {
453
+ tagName: element.tagName,
454
+ isRouterLink: true,
455
+ selectorHint: element.selectorHint,
456
+ astSource: element.astSource || null,
457
+ },
273
458
  });
274
- } else {
275
- skipped.dynamic++;
276
459
  }
277
460
  }
461
+ }
462
+
463
+ lines.forEach((line, lineIdx) => {
464
+ const lineNum = lineIdx + 1;
465
+
466
+ // Skip comments
467
+ if (line.trim().startsWith('//') || line.trim().startsWith('*')) {
468
+ return;
469
+ }
278
470
 
279
- // Extract fetch("https://...")
280
- const fetchRegex = /fetch\(["']([^"']+)["']\)/g;
281
- while ((match = fetchRegex.exec(line)) !== null) {
282
- const url = match[1];
471
+ // Extract Next.js <Link href="/path">
472
+ const linkRegex = /<Link\s+[^>]*href=["']([^"']+)["']/g;
473
+ let match;
474
+ while ((match = linkRegex.exec(line)) !== null) {
475
+ const href = match[1];
283
476
 
284
- // Only extract absolute URLs (https://)
285
- if (url.startsWith('http://') || url.startsWith('https://')) {
477
+ // Skip dynamic hrefs
478
+ if (!href.includes('${') && !href.includes('+') && !href.includes('`')) {
286
479
  expectations.push({
287
- type: 'network',
480
+ type: 'navigation',
288
481
  promise: {
289
- kind: 'request',
290
- value: url,
482
+ kind: 'navigate',
483
+ value: href,
291
484
  },
292
485
  source: {
293
486
  file: relPath,
@@ -296,24 +489,22 @@ function extractJsExpectations(content, filePath, relPath, skipped) {
296
489
  },
297
490
  confidence: 1.0,
298
491
  });
299
- } else if (!url.includes('${') && !url.includes('+') && !url.includes('`')) {
300
- skipped.external++;
301
492
  } else {
302
493
  skipped.dynamic++;
303
494
  }
304
495
  }
305
496
 
306
- // Extract axios.get/post("https://...")
307
- const axiosRegex = /axios\.(get|post|put|delete|patch)\(["']([^"']+)["']\)/g;
308
- while ((match = axiosRegex.exec(line)) !== null) {
309
- const url = match[2];
497
+ // Extract router.push("/path")
498
+ const routerPushRegex = /router\.push\(["']([^"']+)["']\)/g;
499
+ while ((match = routerPushRegex.exec(line)) !== null) {
500
+ const path = match[1];
310
501
 
311
- if (url.startsWith('http://') || url.startsWith('https://')) {
502
+ if (!path.includes('${') && !path.includes('+') && !path.includes('`')) {
312
503
  expectations.push({
313
- type: 'network',
504
+ type: 'navigation',
314
505
  promise: {
315
- kind: 'request',
316
- value: url,
506
+ kind: 'navigate',
507
+ value: path,
317
508
  },
318
509
  source: {
319
510
  file: relPath,
@@ -322,31 +513,11 @@ function extractJsExpectations(content, filePath, relPath, skipped) {
322
513
  },
323
514
  confidence: 1.0,
324
515
  });
325
- } else if (!url.includes('${') && !url.includes('+') && !url.includes('`')) {
326
- skipped.external++;
327
516
  } else {
328
517
  skipped.dynamic++;
329
518
  }
330
519
  }
331
520
 
332
- // Extract useState setters
333
- const useStateRegex = /useState\([^)]*\)/g;
334
- if ((match = useStateRegex.exec(line)) !== null) {
335
- expectations.push({
336
- type: 'state',
337
- promise: {
338
- kind: 'state_mutation',
339
- value: 'state management',
340
- },
341
- source: {
342
- file: relPath,
343
- line: lineNum,
344
- column: match.index,
345
- },
346
- confidence: 0.8,
347
- });
348
- }
349
-
350
521
  // Extract Redux dispatch calls
351
522
  const dispatchRegex = /dispatch\(\{/g;
352
523
  if ((match = dispatchRegex.exec(line)) !== null) {
@@ -386,3 +557,128 @@ function extractJsExpectations(content, filePath, relPath, skipped) {
386
557
 
387
558
  return expectations;
388
559
  }
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
+ }