@veraxhq/verax 0.2.0 → 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 (217) hide show
  1. package/README.md +14 -18
  2. package/bin/verax.js +7 -0
  3. package/package.json +15 -5
  4. package/src/cli/commands/baseline.js +104 -0
  5. package/src/cli/commands/default.js +323 -111
  6. package/src/cli/commands/doctor.js +36 -4
  7. package/src/cli/commands/ga.js +243 -0
  8. package/src/cli/commands/gates.js +95 -0
  9. package/src/cli/commands/inspect.js +131 -2
  10. package/src/cli/commands/release-check.js +213 -0
  11. package/src/cli/commands/run.js +498 -103
  12. package/src/cli/commands/security-check.js +211 -0
  13. package/src/cli/commands/truth.js +114 -0
  14. package/src/cli/entry.js +305 -68
  15. package/src/cli/util/angular-component-extractor.js +179 -0
  16. package/src/cli/util/angular-navigation-detector.js +141 -0
  17. package/src/cli/util/angular-network-detector.js +161 -0
  18. package/src/cli/util/angular-state-detector.js +162 -0
  19. package/src/cli/util/ast-interactive-detector.js +546 -0
  20. package/src/cli/util/ast-network-detector.js +603 -0
  21. package/src/cli/util/ast-usestate-detector.js +602 -0
  22. package/src/cli/util/bootstrap-guard.js +86 -0
  23. package/src/cli/util/detection-engine.js +4 -3
  24. package/src/cli/util/determinism-runner.js +123 -0
  25. package/src/cli/util/determinism-writer.js +129 -0
  26. package/src/cli/util/env-url.js +4 -0
  27. package/src/cli/util/events.js +76 -0
  28. package/src/cli/util/expectation-extractor.js +380 -74
  29. package/src/cli/util/findings-writer.js +126 -15
  30. package/src/cli/util/learn-writer.js +3 -1
  31. package/src/cli/util/observation-engine.js +69 -23
  32. package/src/cli/util/observe-writer.js +3 -1
  33. package/src/cli/util/paths.js +6 -14
  34. package/src/cli/util/project-discovery.js +23 -0
  35. package/src/cli/util/project-writer.js +3 -1
  36. package/src/cli/util/redact.js +2 -2
  37. package/src/cli/util/run-resolver.js +64 -0
  38. package/src/cli/util/runtime-budget.js +147 -0
  39. package/src/cli/util/source-requirement.js +55 -0
  40. package/src/cli/util/summary-writer.js +13 -1
  41. package/src/cli/util/svelte-navigation-detector.js +163 -0
  42. package/src/cli/util/svelte-network-detector.js +80 -0
  43. package/src/cli/util/svelte-sfc-extractor.js +147 -0
  44. package/src/cli/util/svelte-state-detector.js +243 -0
  45. package/src/cli/util/vue-navigation-detector.js +177 -0
  46. package/src/cli/util/vue-sfc-extractor.js +162 -0
  47. package/src/cli/util/vue-state-detector.js +215 -0
  48. package/src/types/global.d.ts +28 -0
  49. package/src/types/ts-ast.d.ts +24 -0
  50. package/src/verax/cli/doctor.js +2 -2
  51. package/src/verax/cli/finding-explainer.js +56 -3
  52. package/src/verax/cli/init.js +1 -1
  53. package/src/verax/cli/url-safety.js +12 -2
  54. package/src/verax/cli/wizard.js +13 -2
  55. package/src/verax/core/artifacts/registry.js +154 -0
  56. package/src/verax/core/artifacts/verifier.js +980 -0
  57. package/src/verax/core/baseline/baseline.enforcer.js +137 -0
  58. package/src/verax/core/baseline/baseline.snapshot.js +231 -0
  59. package/src/verax/core/budget-engine.js +1 -1
  60. package/src/verax/core/capabilities/gates.js +499 -0
  61. package/src/verax/core/capabilities/registry.js +475 -0
  62. package/src/verax/core/confidence/confidence-compute.js +137 -0
  63. package/src/verax/core/confidence/confidence-invariants.js +234 -0
  64. package/src/verax/core/confidence/confidence-report-writer.js +112 -0
  65. package/src/verax/core/confidence/confidence-weights.js +44 -0
  66. package/src/verax/core/confidence/confidence.defaults.js +65 -0
  67. package/src/verax/core/confidence/confidence.loader.js +79 -0
  68. package/src/verax/core/confidence/confidence.schema.js +94 -0
  69. package/src/verax/core/confidence-engine-refactor.js +484 -0
  70. package/src/verax/core/confidence-engine.js +486 -0
  71. package/src/verax/core/confidence-engine.js.backup +471 -0
  72. package/src/verax/core/contracts/index.js +29 -0
  73. package/src/verax/core/contracts/types.js +185 -0
  74. package/src/verax/core/contracts/validators.js +381 -0
  75. package/src/verax/core/decision-snapshot.js +31 -4
  76. package/src/verax/core/decisions/decision.trace.js +276 -0
  77. package/src/verax/core/determinism/contract-writer.js +89 -0
  78. package/src/verax/core/determinism/contract.js +139 -0
  79. package/src/verax/core/determinism/diff.js +364 -0
  80. package/src/verax/core/determinism/engine.js +221 -0
  81. package/src/verax/core/determinism/finding-identity.js +148 -0
  82. package/src/verax/core/determinism/normalize.js +438 -0
  83. package/src/verax/core/determinism/report-writer.js +92 -0
  84. package/src/verax/core/determinism/run-fingerprint.js +118 -0
  85. package/src/verax/core/determinism-model.js +35 -6
  86. package/src/verax/core/dynamic-route-intelligence.js +528 -0
  87. package/src/verax/core/evidence/evidence-capture-service.js +307 -0
  88. package/src/verax/core/evidence/evidence-intent-ledger.js +165 -0
  89. package/src/verax/core/evidence-builder.js +487 -0
  90. package/src/verax/core/execution-mode-context.js +77 -0
  91. package/src/verax/core/execution-mode-detector.js +190 -0
  92. package/src/verax/core/failures/exit-codes.js +86 -0
  93. package/src/verax/core/failures/failure-summary.js +76 -0
  94. package/src/verax/core/failures/failure.factory.js +225 -0
  95. package/src/verax/core/failures/failure.ledger.js +132 -0
  96. package/src/verax/core/failures/failure.types.js +196 -0
  97. package/src/verax/core/failures/index.js +10 -0
  98. package/src/verax/core/ga/ga-report-writer.js +43 -0
  99. package/src/verax/core/ga/ga.artifact.js +49 -0
  100. package/src/verax/core/ga/ga.contract.js +434 -0
  101. package/src/verax/core/ga/ga.enforcer.js +86 -0
  102. package/src/verax/core/guardrails/guardrails-report-writer.js +109 -0
  103. package/src/verax/core/guardrails/policy.defaults.js +210 -0
  104. package/src/verax/core/guardrails/policy.loader.js +83 -0
  105. package/src/verax/core/guardrails/policy.schema.js +110 -0
  106. package/src/verax/core/guardrails/truth-reconciliation.js +136 -0
  107. package/src/verax/core/guardrails-engine.js +505 -0
  108. package/src/verax/core/incremental-store.js +15 -7
  109. package/src/verax/core/observe/run-timeline.js +316 -0
  110. package/src/verax/core/perf/perf.contract.js +186 -0
  111. package/src/verax/core/perf/perf.display.js +65 -0
  112. package/src/verax/core/perf/perf.enforcer.js +91 -0
  113. package/src/verax/core/perf/perf.monitor.js +209 -0
  114. package/src/verax/core/perf/perf.report.js +198 -0
  115. package/src/verax/core/pipeline-tracker.js +238 -0
  116. package/src/verax/core/product-definition.js +127 -0
  117. package/src/verax/core/release/provenance.builder.js +271 -0
  118. package/src/verax/core/release/release-report-writer.js +40 -0
  119. package/src/verax/core/release/release.enforcer.js +159 -0
  120. package/src/verax/core/release/reproducibility.check.js +221 -0
  121. package/src/verax/core/release/sbom.builder.js +283 -0
  122. package/src/verax/core/replay-validator.js +4 -4
  123. package/src/verax/core/replay.js +1 -1
  124. package/src/verax/core/report/cross-index.js +192 -0
  125. package/src/verax/core/report/human-summary.js +222 -0
  126. package/src/verax/core/route-intelligence.js +419 -0
  127. package/src/verax/core/security/secrets.scan.js +326 -0
  128. package/src/verax/core/security/security-report.js +50 -0
  129. package/src/verax/core/security/security.enforcer.js +124 -0
  130. package/src/verax/core/security/supplychain.defaults.json +38 -0
  131. package/src/verax/core/security/supplychain.policy.js +326 -0
  132. package/src/verax/core/security/vuln.scan.js +265 -0
  133. package/src/verax/core/silence-impact.js +1 -1
  134. package/src/verax/core/silence-model.js +9 -7
  135. package/src/verax/core/truth/truth.certificate.js +250 -0
  136. package/src/verax/core/ui-feedback-intelligence.js +515 -0
  137. package/src/verax/detect/comparison.js +8 -3
  138. package/src/verax/detect/confidence-engine.js +645 -57
  139. package/src/verax/detect/confidence-helper.js +33 -0
  140. package/src/verax/detect/detection-engine.js +19 -2
  141. package/src/verax/detect/dynamic-route-findings.js +335 -0
  142. package/src/verax/detect/evidence-index.js +15 -65
  143. package/src/verax/detect/expectation-chain-detector.js +417 -0
  144. package/src/verax/detect/expectation-model.js +56 -3
  145. package/src/verax/detect/explanation-helpers.js +1 -1
  146. package/src/verax/detect/finding-detector.js +2 -2
  147. package/src/verax/detect/findings-writer.js +149 -20
  148. package/src/verax/detect/flow-detector.js +4 -4
  149. package/src/verax/detect/index.js +265 -15
  150. package/src/verax/detect/interactive-findings.js +3 -4
  151. package/src/verax/detect/journey-stall-detector.js +558 -0
  152. package/src/verax/detect/route-findings.js +218 -0
  153. package/src/verax/detect/signal-mapper.js +2 -2
  154. package/src/verax/detect/skip-classifier.js +4 -4
  155. package/src/verax/detect/ui-feedback-findings.js +207 -0
  156. package/src/verax/detect/verdict-engine.js +61 -9
  157. package/src/verax/detect/view-switch-correlator.js +242 -0
  158. package/src/verax/flow/flow-engine.js +3 -2
  159. package/src/verax/flow/flow-spec.js +1 -2
  160. package/src/verax/index.js +413 -33
  161. package/src/verax/intel/effect-detector.js +1 -1
  162. package/src/verax/intel/index.js +2 -2
  163. package/src/verax/intel/route-extractor.js +3 -3
  164. package/src/verax/intel/vue-navigation-extractor.js +81 -18
  165. package/src/verax/intel/vue-router-extractor.js +4 -2
  166. package/src/verax/learn/action-contract-extractor.js +684 -66
  167. package/src/verax/learn/ast-contract-extractor.js +53 -1
  168. package/src/verax/learn/index.js +36 -2
  169. package/src/verax/learn/manifest-writer.js +28 -14
  170. package/src/verax/learn/route-extractor.js +1 -1
  171. package/src/verax/learn/route-validator.js +12 -8
  172. package/src/verax/learn/state-extractor.js +1 -1
  173. package/src/verax/learn/static-extractor-navigation.js +1 -1
  174. package/src/verax/learn/static-extractor-validation.js +2 -2
  175. package/src/verax/learn/static-extractor.js +8 -7
  176. package/src/verax/learn/ts-contract-resolver.js +14 -12
  177. package/src/verax/observe/browser.js +22 -3
  178. package/src/verax/observe/console-sensor.js +2 -2
  179. package/src/verax/observe/expectation-executor.js +2 -1
  180. package/src/verax/observe/focus-sensor.js +1 -1
  181. package/src/verax/observe/human-driver.js +29 -10
  182. package/src/verax/observe/index.js +92 -844
  183. package/src/verax/observe/interaction-discovery.js +27 -15
  184. package/src/verax/observe/interaction-runner.js +31 -14
  185. package/src/verax/observe/loading-sensor.js +6 -0
  186. package/src/verax/observe/navigation-sensor.js +1 -1
  187. package/src/verax/observe/observe-context.js +205 -0
  188. package/src/verax/observe/observe-helpers.js +191 -0
  189. package/src/verax/observe/observe-runner.js +226 -0
  190. package/src/verax/observe/observers/budget-observer.js +185 -0
  191. package/src/verax/observe/observers/console-observer.js +102 -0
  192. package/src/verax/observe/observers/coverage-observer.js +107 -0
  193. package/src/verax/observe/observers/interaction-observer.js +471 -0
  194. package/src/verax/observe/observers/navigation-observer.js +132 -0
  195. package/src/verax/observe/observers/network-observer.js +87 -0
  196. package/src/verax/observe/observers/safety-observer.js +82 -0
  197. package/src/verax/observe/observers/ui-feedback-observer.js +99 -0
  198. package/src/verax/observe/settle.js +1 -0
  199. package/src/verax/observe/state-sensor.js +8 -4
  200. package/src/verax/observe/state-ui-sensor.js +7 -1
  201. package/src/verax/observe/traces-writer.js +27 -16
  202. package/src/verax/observe/ui-feedback-detector.js +742 -0
  203. package/src/verax/observe/ui-signal-sensor.js +155 -2
  204. package/src/verax/scan-summary-writer.js +46 -9
  205. package/src/verax/shared/artifact-manager.js +9 -6
  206. package/src/verax/shared/budget-profiles.js +2 -2
  207. package/src/verax/shared/caching.js +1 -1
  208. package/src/verax/shared/config-loader.js +1 -2
  209. package/src/verax/shared/css-spinner-rules.js +204 -0
  210. package/src/verax/shared/dynamic-route-utils.js +12 -6
  211. package/src/verax/shared/retry-policy.js +1 -6
  212. package/src/verax/shared/root-artifacts.js +1 -1
  213. package/src/verax/shared/view-switch-rules.js +208 -0
  214. package/src/verax/shared/zip-artifacts.js +1 -0
  215. package/src/verax/validate/context-validator.js +1 -1
  216. package/src/verax/observe/index.js.backup +0 -1
  217. package/src/verax/validate/context-validator.js.bak +0 -0
@@ -1,13 +1,27 @@
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
7
21
  * Extracts explicit, static expectations from source files
8
22
  */
9
23
 
10
- export async function extractExpectations(projectProfile, srcPath) {
24
+ export async function extractExpectations(projectProfile, _srcPath) {
11
25
  const expectations = [];
12
26
  const skipped = {
13
27
  dynamic: 0,
@@ -68,6 +82,16 @@ function getScanPaths(projectProfile, sourceRoot) {
68
82
  return [sourceRoot];
69
83
  }
70
84
 
85
+ // Unknown framework or no framework detected - check if it's a static HTML project
86
+ // (This handles cases where framework detection failed but HTML files exist)
87
+ if (framework === 'unknown') {
88
+ const htmlFiles = readdirSync(sourceRoot, { withFileTypes: true })
89
+ .filter(e => e.isFile() && e.name.endsWith('.html'));
90
+ if (htmlFiles.length > 0) {
91
+ return [sourceRoot];
92
+ }
93
+ }
94
+
71
95
  // Unknown framework - scan src if it exists
72
96
  const srcPath = resolve(sourceRoot, 'src');
73
97
  try {
@@ -138,8 +162,8 @@ function shouldSkipDirectory(name) {
138
162
  * Check if file should be scanned
139
163
  */
140
164
  function shouldScanFile(name) {
141
- const extensions = ['.js', '.jsx', '.ts', '.tsx', '.html', '.mjs'];
142
- 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'));
143
167
  }
144
168
 
145
169
  /**
@@ -155,6 +179,15 @@ function scanFile(filePath, sourceRoot, skipped) {
155
179
  if (filePath.endsWith('.html')) {
156
180
  const htmlExpectations = extractHtmlExpectations(content, filePath, relPath);
157
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);
158
191
  } else {
159
192
  const jsExpectations = extractJsExpectations(content, filePath, relPath, skipped);
160
193
  expectations.push(...jsExpectations);
@@ -168,9 +201,11 @@ function scanFile(filePath, sourceRoot, skipped) {
168
201
 
169
202
  /**
170
203
  * Extract expectations from HTML files
204
+ * PHASE 9: Enhanced to extract network calls from <script> tags using AST
171
205
  */
172
206
  function extractHtmlExpectations(content, filePath, relPath) {
173
207
  const expectations = [];
208
+ const skipped = { dynamic: 0, computed: 0, external: 0, parseError: 0, other: 0 };
174
209
 
175
210
  // Extract <a href="/path"> links
176
211
  const hrefRegex = /<a\s+[^>]*href=["']([^"']+)["']/gi;
@@ -198,6 +233,72 @@ function extractHtmlExpectations(content, filePath, relPath) {
198
233
  }
199
234
  }
200
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
+
201
302
  return expectations;
202
303
  }
203
304
 
@@ -208,76 +309,178 @@ function extractJsExpectations(content, filePath, relPath, skipped) {
208
309
  const expectations = [];
209
310
  const lines = content.split('\n');
210
311
 
211
- lines.forEach((line, lineIdx) => {
212
- const lineNum = lineIdx + 1;
213
-
214
- // Skip comments
215
- if (line.trim().startsWith('//') || line.trim().startsWith('*')) {
216
- return;
217
- }
218
-
219
- // Extract Next.js <Link href="/path">
220
- const linkRegex = /<Link\s+[^>]*href=["']([^"']+)["']/g;
221
- let match;
222
- while ((match = linkRegex.exec(line)) !== null) {
223
- 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;
224
316
 
225
- // Skip dynamic hrefs
226
- 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://')) {
227
325
  expectations.push({
228
- type: 'navigation',
326
+ type: 'network',
229
327
  promise: {
230
- kind: 'navigate',
231
- value: href,
328
+ kind: 'request',
329
+ value: url,
330
+ method: call.method,
232
331
  },
233
332
  source: {
234
333
  file: relPath,
235
- line: lineNum,
236
- 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,
237
346
  },
238
- confidence: 1.0,
239
347
  });
240
348
  } else {
241
- skipped.dynamic++;
349
+ // Relative or non-http URLs
350
+ skipped.external++;
242
351
  }
243
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;
244
362
 
245
- // Extract router.push("/path")
246
- const routerPushRegex = /router\.push\(["']([^"']+)["']\)/g;
247
- while ((match = routerPushRegex.exec(line)) !== null) {
248
- 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;
249
399
 
250
- 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>') {
251
438
  expectations.push({
252
439
  type: 'navigation',
253
440
  promise: {
254
441
  kind: 'navigate',
255
- value: path,
442
+ value: target,
256
443
  },
257
444
  source: {
258
445
  file: relPath,
259
- line: lineNum,
260
- column: match.index,
446
+ line: element.location.line,
447
+ column: element.location.column,
448
+ context: element.context,
449
+ astSource: element.astSource || null,
261
450
  },
262
451
  confidence: 1.0,
452
+ metadata: {
453
+ tagName: element.tagName,
454
+ isRouterLink: true,
455
+ selectorHint: element.selectorHint,
456
+ astSource: element.astSource || null,
457
+ },
263
458
  });
264
- } else {
265
- skipped.dynamic++;
266
459
  }
267
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
+ }
268
470
 
269
- // Extract fetch("https://...")
270
- const fetchRegex = /fetch\(["']([^"']+)["']\)/g;
271
- while ((match = fetchRegex.exec(line)) !== null) {
272
- 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];
273
476
 
274
- // Only extract absolute URLs (https://)
275
- if (url.startsWith('http://') || url.startsWith('https://')) {
477
+ // Skip dynamic hrefs
478
+ if (!href.includes('${') && !href.includes('+') && !href.includes('`')) {
276
479
  expectations.push({
277
- type: 'network',
480
+ type: 'navigation',
278
481
  promise: {
279
- kind: 'request',
280
- value: url,
482
+ kind: 'navigate',
483
+ value: href,
281
484
  },
282
485
  source: {
283
486
  file: relPath,
@@ -286,24 +489,22 @@ function extractJsExpectations(content, filePath, relPath, skipped) {
286
489
  },
287
490
  confidence: 1.0,
288
491
  });
289
- } else if (!url.includes('${') && !url.includes('+') && !url.includes('`')) {
290
- skipped.external++;
291
492
  } else {
292
493
  skipped.dynamic++;
293
494
  }
294
495
  }
295
496
 
296
- // Extract axios.get/post("https://...")
297
- const axiosRegex = /axios\.(get|post|put|delete|patch)\(["']([^"']+)["']\)/g;
298
- while ((match = axiosRegex.exec(line)) !== null) {
299
- 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];
300
501
 
301
- if (url.startsWith('http://') || url.startsWith('https://')) {
502
+ if (!path.includes('${') && !path.includes('+') && !path.includes('`')) {
302
503
  expectations.push({
303
- type: 'network',
504
+ type: 'navigation',
304
505
  promise: {
305
- kind: 'request',
306
- value: url,
506
+ kind: 'navigate',
507
+ value: path,
307
508
  },
308
509
  source: {
309
510
  file: relPath,
@@ -312,31 +513,11 @@ function extractJsExpectations(content, filePath, relPath, skipped) {
312
513
  },
313
514
  confidence: 1.0,
314
515
  });
315
- } else if (!url.includes('${') && !url.includes('+') && !url.includes('`')) {
316
- skipped.external++;
317
516
  } else {
318
517
  skipped.dynamic++;
319
518
  }
320
519
  }
321
520
 
322
- // Extract useState setters
323
- const useStateRegex = /useState\([^)]*\)/g;
324
- if ((match = useStateRegex.exec(line)) !== null) {
325
- expectations.push({
326
- type: 'state',
327
- promise: {
328
- kind: 'state_mutation',
329
- value: 'state management',
330
- },
331
- source: {
332
- file: relPath,
333
- line: lineNum,
334
- column: match.index,
335
- },
336
- confidence: 0.8,
337
- });
338
- }
339
-
340
521
  // Extract Redux dispatch calls
341
522
  const dispatchRegex = /dispatch\(\{/g;
342
523
  if ((match = dispatchRegex.exec(line)) !== null) {
@@ -376,3 +557,128 @@ function extractJsExpectations(content, filePath, relPath, skipped) {
376
557
 
377
558
  return expectations;
378
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
+ }