@veraxhq/verax 0.1.0 → 0.2.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (135) hide show
  1. package/README.md +123 -88
  2. package/bin/verax.js +11 -452
  3. package/package.json +24 -36
  4. package/src/cli/commands/default.js +681 -0
  5. package/src/cli/commands/doctor.js +197 -0
  6. package/src/cli/commands/inspect.js +109 -0
  7. package/src/cli/commands/run.js +586 -0
  8. package/src/cli/entry.js +196 -0
  9. package/src/cli/util/atomic-write.js +37 -0
  10. package/src/cli/util/detection-engine.js +297 -0
  11. package/src/cli/util/env-url.js +33 -0
  12. package/src/cli/util/errors.js +44 -0
  13. package/src/cli/util/events.js +110 -0
  14. package/src/cli/util/expectation-extractor.js +388 -0
  15. package/src/cli/util/findings-writer.js +32 -0
  16. package/src/cli/util/idgen.js +87 -0
  17. package/src/cli/util/learn-writer.js +39 -0
  18. package/src/cli/util/observation-engine.js +412 -0
  19. package/src/cli/util/observe-writer.js +25 -0
  20. package/src/cli/util/paths.js +30 -0
  21. package/src/cli/util/project-discovery.js +297 -0
  22. package/src/cli/util/project-writer.js +26 -0
  23. package/src/cli/util/redact.js +128 -0
  24. package/src/cli/util/run-id.js +30 -0
  25. package/src/cli/util/runtime-budget.js +147 -0
  26. package/src/cli/util/summary-writer.js +43 -0
  27. package/src/types/global.d.ts +28 -0
  28. package/src/types/ts-ast.d.ts +24 -0
  29. package/src/verax/cli/ci-summary.js +35 -0
  30. package/src/verax/cli/context-explanation.js +89 -0
  31. package/src/verax/cli/doctor.js +277 -0
  32. package/src/verax/cli/error-normalizer.js +154 -0
  33. package/src/verax/cli/explain-output.js +105 -0
  34. package/src/verax/cli/finding-explainer.js +130 -0
  35. package/src/verax/cli/init.js +237 -0
  36. package/src/verax/cli/run-overview.js +163 -0
  37. package/src/verax/cli/url-safety.js +111 -0
  38. package/src/verax/cli/wizard.js +109 -0
  39. package/src/verax/cli/zero-findings-explainer.js +57 -0
  40. package/src/verax/cli/zero-interaction-explainer.js +127 -0
  41. package/src/verax/core/action-classifier.js +86 -0
  42. package/src/verax/core/budget-engine.js +218 -0
  43. package/src/verax/core/canonical-outcomes.js +157 -0
  44. package/src/verax/core/decision-snapshot.js +335 -0
  45. package/src/verax/core/determinism-model.js +432 -0
  46. package/src/verax/core/incremental-store.js +245 -0
  47. package/src/verax/core/invariants.js +356 -0
  48. package/src/verax/core/promise-model.js +230 -0
  49. package/src/verax/core/replay-validator.js +350 -0
  50. package/src/verax/core/replay.js +222 -0
  51. package/src/verax/core/run-id.js +175 -0
  52. package/src/verax/core/run-manifest.js +99 -0
  53. package/src/verax/core/silence-impact.js +369 -0
  54. package/src/verax/core/silence-model.js +523 -0
  55. package/src/verax/detect/comparison.js +7 -34
  56. package/src/verax/detect/confidence-engine.js +764 -329
  57. package/src/verax/detect/detection-engine.js +293 -0
  58. package/src/verax/detect/evidence-index.js +127 -0
  59. package/src/verax/detect/expectation-model.js +241 -168
  60. package/src/verax/detect/explanation-helpers.js +187 -0
  61. package/src/verax/detect/finding-detector.js +450 -0
  62. package/src/verax/detect/findings-writer.js +41 -12
  63. package/src/verax/detect/flow-detector.js +366 -0
  64. package/src/verax/detect/index.js +200 -288
  65. package/src/verax/detect/interactive-findings.js +612 -0
  66. package/src/verax/detect/signal-mapper.js +308 -0
  67. package/src/verax/detect/skip-classifier.js +4 -4
  68. package/src/verax/detect/verdict-engine.js +561 -0
  69. package/src/verax/evidence-index-writer.js +61 -0
  70. package/src/verax/flow/flow-engine.js +3 -2
  71. package/src/verax/flow/flow-spec.js +1 -2
  72. package/src/verax/index.js +103 -15
  73. package/src/verax/intel/effect-detector.js +368 -0
  74. package/src/verax/intel/handler-mapper.js +249 -0
  75. package/src/verax/intel/index.js +281 -0
  76. package/src/verax/intel/route-extractor.js +280 -0
  77. package/src/verax/intel/ts-program.js +256 -0
  78. package/src/verax/intel/vue-navigation-extractor.js +642 -0
  79. package/src/verax/intel/vue-router-extractor.js +325 -0
  80. package/src/verax/learn/action-contract-extractor.js +338 -104
  81. package/src/verax/learn/ast-contract-extractor.js +148 -6
  82. package/src/verax/learn/flow-extractor.js +172 -0
  83. package/src/verax/learn/index.js +36 -2
  84. package/src/verax/learn/manifest-writer.js +122 -58
  85. package/src/verax/learn/project-detector.js +40 -0
  86. package/src/verax/learn/route-extractor.js +28 -97
  87. package/src/verax/learn/route-validator.js +8 -7
  88. package/src/verax/learn/state-extractor.js +212 -0
  89. package/src/verax/learn/static-extractor-navigation.js +114 -0
  90. package/src/verax/learn/static-extractor-validation.js +88 -0
  91. package/src/verax/learn/static-extractor.js +119 -10
  92. package/src/verax/learn/truth-assessor.js +24 -21
  93. package/src/verax/learn/ts-contract-resolver.js +14 -12
  94. package/src/verax/observe/aria-sensor.js +211 -0
  95. package/src/verax/observe/browser.js +30 -6
  96. package/src/verax/observe/console-sensor.js +2 -18
  97. package/src/verax/observe/domain-boundary.js +10 -1
  98. package/src/verax/observe/expectation-executor.js +513 -0
  99. package/src/verax/observe/flow-matcher.js +143 -0
  100. package/src/verax/observe/focus-sensor.js +196 -0
  101. package/src/verax/observe/human-driver.js +660 -273
  102. package/src/verax/observe/index.js +910 -26
  103. package/src/verax/observe/interaction-discovery.js +378 -15
  104. package/src/verax/observe/interaction-runner.js +562 -197
  105. package/src/verax/observe/loading-sensor.js +145 -0
  106. package/src/verax/observe/navigation-sensor.js +255 -0
  107. package/src/verax/observe/network-sensor.js +55 -7
  108. package/src/verax/observe/observed-expectation-deriver.js +186 -0
  109. package/src/verax/observe/observed-expectation.js +305 -0
  110. package/src/verax/observe/page-frontier.js +234 -0
  111. package/src/verax/observe/settle.js +38 -17
  112. package/src/verax/observe/state-sensor.js +393 -0
  113. package/src/verax/observe/state-ui-sensor.js +7 -1
  114. package/src/verax/observe/timing-sensor.js +228 -0
  115. package/src/verax/observe/traces-writer.js +73 -21
  116. package/src/verax/observe/ui-signal-sensor.js +143 -17
  117. package/src/verax/scan-summary-writer.js +80 -15
  118. package/src/verax/shared/artifact-manager.js +111 -9
  119. package/src/verax/shared/budget-profiles.js +136 -0
  120. package/src/verax/shared/caching.js +1 -1
  121. package/src/verax/shared/ci-detection.js +39 -0
  122. package/src/verax/shared/config-loader.js +169 -0
  123. package/src/verax/shared/dynamic-route-utils.js +224 -0
  124. package/src/verax/shared/expectation-coverage.js +44 -0
  125. package/src/verax/shared/expectation-prover.js +81 -0
  126. package/src/verax/shared/expectation-tracker.js +201 -0
  127. package/src/verax/shared/expectations-writer.js +60 -0
  128. package/src/verax/shared/first-run.js +44 -0
  129. package/src/verax/shared/progress-reporter.js +171 -0
  130. package/src/verax/shared/retry-policy.js +9 -1
  131. package/src/verax/shared/root-artifacts.js +49 -0
  132. package/src/verax/shared/scan-budget.js +86 -0
  133. package/src/verax/shared/url-normalizer.js +162 -0
  134. package/src/verax/shared/zip-artifacts.js +66 -0
  135. package/src/verax/validate/context-validator.js +244 -0
@@ -44,6 +44,34 @@ async function hasReactRouter(projectDir) {
44
44
  }
45
45
  }
46
46
 
47
+ async function hasVue(projectDir) {
48
+ try {
49
+ const packageJsonPath = resolve(projectDir, 'package.json');
50
+ if (!existsSync(packageJsonPath)) return false;
51
+
52
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
53
+ const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
54
+
55
+ return !!deps.vue;
56
+ } catch (error) {
57
+ return false;
58
+ }
59
+ }
60
+
61
+ async function hasVueRouter(projectDir) {
62
+ try {
63
+ const packageJsonPath = resolve(projectDir, 'package.json');
64
+ if (!existsSync(packageJsonPath)) return false;
65
+
66
+ const packageJson = JSON.parse(readFileSync(packageJsonPath, 'utf-8'));
67
+ const deps = { ...packageJson.dependencies, ...packageJson.devDependencies };
68
+
69
+ return !!deps['vue-router'];
70
+ } catch (error) {
71
+ return false;
72
+ }
73
+ }
74
+
47
75
  export async function detectProjectType(projectDir) {
48
76
  const appDir = resolve(projectDir, 'app');
49
77
  const pagesDir = resolve(projectDir, 'pages');
@@ -62,6 +90,17 @@ export async function detectProjectType(projectDir) {
62
90
  return 'nextjs_pages_router';
63
91
  }
64
92
  }
93
+
94
+ const hasVueDep = await hasVue(projectDir);
95
+ const hasVueRouterDep = await hasVueRouter(projectDir);
96
+
97
+ if (hasVueDep && hasVueRouterDep) {
98
+ return 'vue_router';
99
+ }
100
+
101
+ if (hasVueDep && !hasVueRouterDep) {
102
+ return 'vue_spa';
103
+ }
65
104
 
66
105
  const hasReact = await hasReactDependency(projectDir);
67
106
  const hasNext = await hasNextJs(projectDir);
@@ -85,3 +124,4 @@ export async function hasReactRouterDom(projectDir) {
85
124
  return await hasReactRouter(projectDir);
86
125
  }
87
126
 
127
+
@@ -1,5 +1,7 @@
1
- import { glob } from 'glob';
2
- import { resolve } from 'path';
1
+ // resolve import removed - currently unused
2
+ import { extractStaticRoutes } from './static-extractor.js';
3
+ import { createTSProgram } from '../intel/ts-program.js';
4
+ import { extractRoutes as extractRoutesAST } from '../intel/route-extractor.js';
3
5
 
4
6
  const INTERNAL_PATH_PATTERNS = [
5
7
  /^\/admin/,
@@ -14,109 +16,38 @@ function isInternalRoute(path) {
14
16
  return INTERNAL_PATH_PATTERNS.some(pattern => pattern.test(path));
15
17
  }
16
18
 
17
- function fileToAppRouterPath(file) {
18
- let path = file.replace(/[\\\/]/g, '/');
19
- path = path.replace(/(^|\/)page\.(js|jsx|ts|tsx)$/, '');
20
- path = path.replace(/^\./, '');
21
-
22
- if (path === '' || path === '/') {
23
- return '/';
24
- }
25
-
26
- if (!path.startsWith('/')) {
27
- path = '/' + path;
28
- }
29
-
30
- path = path.replace(/\[([^\]]+)\]/g, ':$1');
31
- path = path.replace(/\([^)]+\)/g, '');
32
-
33
- return path || '/';
34
- }
35
-
36
- function fileToPagesRouterPath(file) {
37
- let path = file.replace(/[\\\/]/g, '/');
38
- path = path.replace(/\.(js|jsx|ts|tsx)$/, '');
39
- path = path.replace(/^index$/, '');
40
- path = path.replace(/^\./, '');
41
-
42
- if (path === '' || path === '/') {
43
- return '/';
44
- }
45
- if (!path.startsWith('/')) {
46
- path = '/' + path;
47
- }
48
-
49
- path = path.replace(/\[([^\]]+)\]/g, ':$1');
50
-
51
- return path || '/';
52
- }
53
-
54
- import { extractStaticRoutes } from './static-extractor.js';
55
- import { extractReactRouterRoutes } from './react-router-extractor.js';
56
- import { hasReactRouterDom } from './project-detector.js';
57
-
58
19
  export async function extractRoutes(projectDir, projectType) {
59
- const routes = [];
60
- const routeSet = new Set();
61
-
20
+ // Static sites: use file-based extractor (no regex, just file system)
62
21
  if (projectType === 'static') {
63
22
  return await extractStaticRoutes(projectDir);
64
23
  }
65
-
66
- if (projectType === 'react_spa') {
67
- const hasRouter = await hasReactRouterDom(projectDir);
68
- if (hasRouter) {
69
- return await extractReactRouterRoutes(projectDir);
70
- }
71
- return [];
72
- }
73
-
74
- if (projectType === 'nextjs_app_router') {
75
- const appDir = resolve(projectDir, 'app');
76
- const pageFiles = await glob('**/page.{js,jsx,ts,tsx}', {
77
- cwd: appDir,
78
- absolute: false,
79
- ignore: ['node_modules/**']
80
- });
24
+
25
+ // React SPAs, Next.js, and Vue: use AST-based intel module (NO REGEX)
26
+ if (projectType === 'react_spa' ||
27
+ projectType === 'nextjs_app_router' ||
28
+ projectType === 'nextjs_pages_router' ||
29
+ projectType === 'vue_router' ||
30
+ projectType === 'vue_spa') {
31
+ const program = createTSProgram(projectDir, { includeJs: true });
81
32
 
82
- for (const file of pageFiles) {
83
- const routePath = fileToAppRouterPath(file);
84
- const routeKey = routePath;
85
-
86
- if (!routeSet.has(routeKey)) {
87
- routeSet.add(routeKey);
88
- routes.push({
89
- path: routePath,
90
- source: `app/${file}`,
91
- public: !isInternalRoute(routePath)
92
- });
93
- }
33
+ if (program.error) {
34
+ // Fallback: return empty routes if TS program creation fails
35
+ return [];
94
36
  }
95
- } else if (projectType === 'nextjs_pages_router') {
96
- const pagesDir = resolve(projectDir, 'pages');
97
- const pageFiles = await glob('**/*.{js,jsx,ts,tsx}', {
98
- cwd: pagesDir,
99
- absolute: false,
100
- ignore: ['node_modules/**', '_app.*', '_document.*', '_error.*']
101
- });
102
37
 
103
- for (const file of pageFiles) {
104
- const routePath = fileToPagesRouterPath(file);
105
- const routeKey = routePath;
106
-
107
- if (!routeSet.has(routeKey)) {
108
- routeSet.add(routeKey);
109
- routes.push({
110
- path: routePath,
111
- source: `pages/${file}`,
112
- public: !isInternalRoute(routePath)
113
- });
114
- }
115
- }
38
+ const astRoutes = extractRoutesAST(projectDir, program);
39
+
40
+ // Convert AST routes to manifest format and sort for determinism
41
+ const routes = astRoutes.map(r => ({
42
+ path: r.path,
43
+ source: r.sourceRef || r.file || 'unknown',
44
+ public: r.public !== undefined ? r.public : !isInternalRoute(r.path),
45
+ sourceRef: r.sourceRef
46
+ }));
47
+ routes.sort((a, b) => a.path.localeCompare(b.path));
48
+ return routes;
116
49
  }
117
50
 
118
- routes.sort((a, b) => a.path.localeCompare(b.path));
119
-
120
- return routes;
51
+ return [];
121
52
  }
122
53
 
@@ -99,14 +99,15 @@ export async function validateRoutes(manifest, baseUrl) {
99
99
 
100
100
  const request = response.request();
101
101
 
102
- // Use redirectChain() if available (preferred), otherwise use redirectedFrom()
102
+ // Use redirectChain property if available (Playwright API)
103
103
  let redirectChain = [];
104
- if (typeof request.redirectChain === 'function') {
105
- try {
106
- redirectChain = request.redirectChain();
107
- } catch (e) {
108
- // redirectChain may not be available, fall back to redirectedFrom
109
- }
104
+ // @ts-expect-error - redirectChain exists in Playwright runtime but not in TypeScript types
105
+ if (request.redirectChain && Array.isArray(request.redirectChain)) {
106
+ // @ts-expect-error - redirectChain exists in Playwright runtime but not in TypeScript types
107
+ redirectChain = request.redirectChain;
108
+ } else if (request.redirectedFrom) {
109
+ // Fall back to redirectedFrom if redirectChain not available
110
+ redirectChain = [request.redirectedFrom];
110
111
  }
111
112
 
112
113
  // If redirectChain is empty or not available, build chain from redirectedFrom
@@ -0,0 +1,212 @@
1
+ import { parse } from '@babel/parser';
2
+ import traverse from '@babel/traverse';
3
+ import { readFileSync } from 'fs';
4
+ import { glob } from 'glob';
5
+ import { resolve } from 'path';
6
+ import { ExpectationProof } from '../shared/expectation-proof.js';
7
+
8
+ const MAX_FILES_TO_SCAN = 200;
9
+
10
+ /**
11
+ * Extracts static string value from call expression arguments.
12
+ * Returns null if value is dynamic.
13
+ */
14
+ function _extractStaticActionName(node) {
15
+ if (!node) return null;
16
+
17
+ // String literal: dispatch('increment')
18
+ if (node.type === 'StringLiteral') {
19
+ return node.value;
20
+ }
21
+
22
+ // Template literal without interpolation: `increment`
23
+ if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {
24
+ if (node.quasis.length === 1) {
25
+ return node.quasis[0].value.cooked;
26
+ }
27
+ }
28
+
29
+ return null;
30
+ }
31
+
32
+ /**
33
+ * Extracts PROVEN state action expectations from Redux and Zustand patterns.
34
+ *
35
+ * Supported patterns (all require static string literals):
36
+ * - Redux Toolkit: dispatch(increment()), dispatch(decrement())
37
+ * - Redux Toolkit: dispatch(slice.actions.increment())
38
+ * - Zustand: set((state) => ({ ... })) with key names
39
+ *
40
+ * Returns array of state expectations with:
41
+ * - type: 'state_action'
42
+ * - expectedTarget: action name or store key
43
+ * - storeType: 'redux' | 'zustand'
44
+ * - sourceFile: string
45
+ * - proof: PROVEN_EXPECTATION
46
+ * - line: number
47
+ */
48
+ function extractStateExpectations(filePath, fileContent) {
49
+ const expectations = [];
50
+
51
+ try {
52
+ const ast = parse(fileContent, {
53
+ sourceType: 'module',
54
+ plugins: ['jsx', 'typescript']
55
+ });
56
+
57
+ traverse.default(ast, {
58
+ // Redux dispatch(action())
59
+ CallExpression(path) {
60
+ const callee = path.node.callee;
61
+
62
+ // dispatch(someAction())
63
+ if (callee.type === 'Identifier' && callee.name === 'dispatch') {
64
+ const firstArg = path.node.arguments[0];
65
+
66
+ // dispatch(increment()) - action creator call
67
+ if (firstArg && firstArg.type === 'CallExpression') {
68
+ let actionName = null;
69
+
70
+ // Simple action creator: increment()
71
+ if (firstArg.callee.type === 'Identifier') {
72
+ actionName = firstArg.callee.name;
73
+ }
74
+
75
+ // Slice action: counterSlice.actions.increment()
76
+ else if (firstArg.callee.type === 'MemberExpression') {
77
+ const property = firstArg.callee.property;
78
+ if (property.type === 'Identifier') {
79
+ actionName = property.name;
80
+ }
81
+ }
82
+
83
+ if (actionName) {
84
+ expectations.push({
85
+ type: 'state_action',
86
+ expectedTarget: actionName,
87
+ storeType: 'redux',
88
+ sourceFile: filePath,
89
+ proof: ExpectationProof.PROVEN_EXPECTATION,
90
+ line: path.node.loc?.start.line || null
91
+ });
92
+ }
93
+ }
94
+ }
95
+
96
+ // Zustand set((state) => ({ key: value }))
97
+ if (callee.type === 'Identifier' && callee.name === 'set') {
98
+ const firstArg = path.node.arguments[0];
99
+
100
+ // set((state) => ({ ... })) - arrow function returning object
101
+ if (firstArg && firstArg.type === 'ArrowFunctionExpression') {
102
+ const body = firstArg.body;
103
+
104
+ // Arrow function body is object expression
105
+ if (body.type === 'ObjectExpression') {
106
+ for (const prop of body.properties) {
107
+ if (prop.type === 'ObjectProperty' && prop.key.type === 'Identifier') {
108
+ const keyName = prop.key.name;
109
+ expectations.push({
110
+ type: 'state_action',
111
+ expectedTarget: keyName,
112
+ storeType: 'zustand',
113
+ sourceFile: filePath,
114
+ proof: ExpectationProof.PROVEN_EXPECTATION,
115
+ line: path.node.loc?.start.line || null
116
+ });
117
+ }
118
+ }
119
+ }
120
+ }
121
+ }
122
+ }
123
+ });
124
+ } catch (error) {
125
+ // Parse error - skip this file
126
+ return [];
127
+ }
128
+
129
+ return expectations;
130
+ }
131
+
132
+ /**
133
+ * Detects if project uses Redux or Zustand by scanning package.json and imports.
134
+ */
135
+ function detectStateStores(projectDir) {
136
+ const stores = {
137
+ redux: false,
138
+ zustand: false
139
+ };
140
+
141
+ try {
142
+ const pkgPath = resolve(projectDir, 'package.json');
143
+ const pkgContent = readFileSync(pkgPath, 'utf-8');
144
+ const pkg = JSON.parse(pkgContent);
145
+
146
+ const allDeps = {
147
+ ...(pkg.dependencies || {}),
148
+ ...(pkg.devDependencies || {})
149
+ };
150
+
151
+ if (allDeps['@reduxjs/toolkit'] || allDeps['redux']) {
152
+ stores.redux = true;
153
+ }
154
+ if (allDeps['zustand']) {
155
+ stores.zustand = true;
156
+ }
157
+ } catch (error) {
158
+ // No package.json or parse error
159
+ }
160
+
161
+ return stores;
162
+ }
163
+
164
+ /**
165
+ * Scans project for state action expectations.
166
+ * Returns { expectations: [], storesDetected: { redux: bool, zustand: bool } }
167
+ */
168
+ export async function extractStateExpectationsFromAST(projectDir) {
169
+ const storesDetected = detectStateStores(projectDir);
170
+ const expectations = [];
171
+
172
+ // Only scan if supported stores are detected
173
+ if (!storesDetected.redux && !storesDetected.zustand) {
174
+ return { expectations: [], storesDetected };
175
+ }
176
+
177
+ try {
178
+ const files = await glob('**/*.{js,jsx,ts,tsx}', {
179
+ cwd: projectDir,
180
+ absolute: false,
181
+ ignore: ['node_modules/**', 'dist/**', 'build/**', '.next/**', 'out/**']
182
+ });
183
+
184
+ const filesToScan = files.slice(0, MAX_FILES_TO_SCAN);
185
+
186
+ for (const file of filesToScan) {
187
+ try {
188
+ const filePath = resolve(projectDir, file);
189
+ const content = readFileSync(filePath, 'utf-8');
190
+ const fileExpectations = extractStateExpectations(file, content);
191
+ expectations.push(...fileExpectations);
192
+ } catch (error) {
193
+ continue;
194
+ }
195
+ }
196
+ } catch (error) {
197
+ return { expectations: [], storesDetected };
198
+ }
199
+
200
+ // Deduplicate by target name
201
+ const seen = new Set();
202
+ const unique = [];
203
+ for (const exp of expectations) {
204
+ const key = `${exp.storeType}:${exp.expectedTarget}`;
205
+ if (!seen.has(key)) {
206
+ seen.add(key);
207
+ unique.push(exp);
208
+ }
209
+ }
210
+
211
+ return { expectations: unique, storesDetected };
212
+ }
@@ -0,0 +1,114 @@
1
+ /**
2
+ * Extract navigation expectations from static HTML files.
3
+ * Detects router.push/replace/navigate calls in inline scripts.
4
+ */
5
+
6
+ function extractNavigationExpectations(root, fromPath, file, _projectDir) {
7
+ const expectations = [];
8
+
9
+ // Extract from inline scripts
10
+ const scripts = root.querySelectorAll('script');
11
+ for (const script of scripts) {
12
+ const scriptContent = script.textContent || '';
13
+ if (!scriptContent) continue;
14
+
15
+ // Find router.push/replace/navigate calls with string literals
16
+ const routerPushMatches = scriptContent.matchAll(/router\.push\s*\(\s*['"]([^'"]+)['"]/g);
17
+ const routerReplaceMatches = scriptContent.matchAll(/router\.replace\s*\(\s*['"]([^'"]+)['"]/g);
18
+ const navigateMatches = scriptContent.matchAll(/navigate\s*\(\s*['"]([^'"]+)['"]/g);
19
+
20
+ for (const match of [...routerPushMatches, ...routerReplaceMatches, ...navigateMatches]) {
21
+ const target = match[1];
22
+ if (!target) continue;
23
+
24
+ // Only extract if it's a relative path (starts with /)
25
+ if (!target.startsWith('/')) continue;
26
+
27
+ // Find the button/function that triggers this
28
+ const buttonId = scriptContent.match(/getElementById\s*\(\s*['"]([^'"]+)['"]/)?.[1];
29
+ const functionName = scriptContent.match(/function\s+(\w+)\s*\(/)?.[1];
30
+
31
+ let selectorHint = null;
32
+ if (buttonId) {
33
+ selectorHint = `#${buttonId}`;
34
+ } else if (functionName) {
35
+ // Try to find button with onclick that calls this function
36
+ const buttons = root.querySelectorAll(`button[onclick*="${functionName}"]`);
37
+ if (buttons.length > 0) {
38
+ const btn = buttons[0];
39
+ selectorHint = btn.id ? `#${btn.id}` : `button[onclick*="${functionName}"]`;
40
+ }
41
+ }
42
+
43
+ if (selectorHint) {
44
+ // Determine method from match
45
+ let method = 'push';
46
+ if (match[0].includes('replace')) {
47
+ method = 'replace';
48
+ } else if (match[0].includes('navigate')) {
49
+ method = 'navigate';
50
+ }
51
+
52
+ expectations.push({
53
+ fromPath: fromPath,
54
+ type: 'spa_navigation',
55
+ targetPath: target,
56
+ expectedTarget: target,
57
+ navigationMethod: method,
58
+ proof: 'PROVEN_EXPECTATION',
59
+ sourceRef: `${file}:${scriptContent.indexOf(match[0])}`,
60
+ selectorHint: selectorHint,
61
+ evidence: {
62
+ source: file,
63
+ selectorHint: selectorHint
64
+ }
65
+ });
66
+ }
67
+ }
68
+ }
69
+
70
+ // Also check onclick attributes directly
71
+ const buttons = root.querySelectorAll('button[onclick]');
72
+ for (const button of buttons) {
73
+ const onclick = button.getAttribute('onclick') || '';
74
+ const routerPushMatch = onclick.match(/router\.push\s*\(\s*['"]([^'"]+)['"]/);
75
+ const routerReplaceMatch = onclick.match(/router\.replace\s*\(\s*['"]([^'"]+)['"]/);
76
+ const navigateMatch = onclick.match(/navigate\s*\(\s*['"]([^'"]+)['"]/);
77
+
78
+ const match = routerPushMatch || routerReplaceMatch || navigateMatch;
79
+ if (match) {
80
+ const target = match[1];
81
+ if (target.startsWith('/')) {
82
+ const buttonId = button.getAttribute('id');
83
+ const selectorHint = buttonId ? `#${buttonId}` : `button[onclick*="${target}"]`;
84
+
85
+ let method = 'push';
86
+ if (routerReplaceMatch) {
87
+ method = 'replace';
88
+ } else if (navigateMatch) {
89
+ method = 'navigate';
90
+ }
91
+
92
+ expectations.push({
93
+ fromPath: fromPath,
94
+ type: 'spa_navigation',
95
+ targetPath: target,
96
+ expectedTarget: target,
97
+ navigationMethod: method,
98
+ proof: 'PROVEN_EXPECTATION',
99
+ sourceRef: `${file}:onclick`,
100
+ selectorHint: selectorHint,
101
+ evidence: {
102
+ source: file,
103
+ selectorHint: selectorHint
104
+ }
105
+ });
106
+ }
107
+ }
108
+ }
109
+
110
+ return expectations;
111
+ }
112
+
113
+ export { extractNavigationExpectations };
114
+
@@ -0,0 +1,88 @@
1
+ /**
2
+ * Extract validation_block expectations from static HTML files.
3
+ * Detects preventDefault() calls in onSubmit handlers.
4
+ */
5
+
6
+ function extractValidationExpectations(root, fromPath, file, _projectDir) {
7
+ const expectations = [];
8
+
9
+ // Extract from inline scripts
10
+ const scripts = root.querySelectorAll('script');
11
+ for (const script of scripts) {
12
+ const scriptContent = script.textContent || '';
13
+ if (!scriptContent) continue;
14
+
15
+ // Find preventDefault() calls in functions that might be onSubmit handlers
16
+ const preventDefaultMatches = scriptContent.matchAll(/preventDefault\s*\(\s*\)/g);
17
+
18
+ for (const match of preventDefaultMatches) {
19
+ // Check if this is in an onSubmit context
20
+ // Look for function definitions that might be onSubmit handlers
21
+ const beforeMatch = scriptContent.substring(0, match.index);
22
+ const _afterMatch = scriptContent.substring(match.index);
23
+
24
+ // Check if there's a function definition before this preventDefault
25
+ const functionMatch = beforeMatch.match(/function\s+(\w+)\s*\([^)]*event[^)]*\)/);
26
+ const onSubmitMatch = beforeMatch.match(/onsubmit\s*=\s*["'](\w+)["']/);
27
+
28
+ let functionName = null;
29
+ if (functionMatch) {
30
+ functionName = functionMatch[1];
31
+ } else if (onSubmitMatch) {
32
+ functionName = onSubmitMatch[1];
33
+ }
34
+
35
+ if (functionName) {
36
+ // Find form with onsubmit that calls this function
37
+ const forms = root.querySelectorAll(`form[onsubmit*="${functionName}"]`);
38
+ for (const form of forms) {
39
+ const formId = form.getAttribute('id');
40
+ const selectorHint = formId ? `#${formId}` : `form[onsubmit*="${functionName}"]`;
41
+
42
+ expectations.push({
43
+ fromPath: fromPath,
44
+ type: 'validation_block',
45
+ proof: 'PROVEN_EXPECTATION',
46
+ sourceRef: `${file}:${scriptContent.substring(0, match.index).split('\n').length}`,
47
+ selectorHint: selectorHint,
48
+ handlerRef: `${file}:${functionName}`,
49
+ evidence: {
50
+ source: file,
51
+ selectorHint: selectorHint
52
+ }
53
+ });
54
+ }
55
+ }
56
+ }
57
+ }
58
+
59
+ // Also check inline onsubmit attributes directly
60
+ const forms = root.querySelectorAll('form[onsubmit]');
61
+ for (const form of forms) {
62
+ const onsubmit = form.getAttribute('onsubmit') || '';
63
+ const preventDefaultMatch = onsubmit.match(/preventDefault\s*\(\s*\)/);
64
+
65
+ if (preventDefaultMatch) {
66
+ const formId = form.getAttribute('id');
67
+ const selectorHint = formId ? `#${formId}` : `form[onsubmit*="preventDefault"]`;
68
+
69
+ expectations.push({
70
+ fromPath: fromPath,
71
+ type: 'validation_block',
72
+ proof: 'PROVEN_EXPECTATION',
73
+ sourceRef: `${file}:onsubmit`,
74
+ selectorHint: selectorHint,
75
+ handlerRef: `${file}:onsubmit`,
76
+ evidence: {
77
+ source: file,
78
+ selectorHint: selectorHint
79
+ }
80
+ });
81
+ }
82
+ }
83
+
84
+ return expectations;
85
+ }
86
+
87
+ export { extractValidationExpectations };
88
+