@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.
- package/README.md +123 -88
- package/bin/verax.js +11 -452
- package/package.json +24 -36
- package/src/cli/commands/default.js +681 -0
- package/src/cli/commands/doctor.js +197 -0
- package/src/cli/commands/inspect.js +109 -0
- package/src/cli/commands/run.js +586 -0
- package/src/cli/entry.js +196 -0
- package/src/cli/util/atomic-write.js +37 -0
- package/src/cli/util/detection-engine.js +297 -0
- package/src/cli/util/env-url.js +33 -0
- package/src/cli/util/errors.js +44 -0
- package/src/cli/util/events.js +110 -0
- package/src/cli/util/expectation-extractor.js +388 -0
- package/src/cli/util/findings-writer.js +32 -0
- package/src/cli/util/idgen.js +87 -0
- package/src/cli/util/learn-writer.js +39 -0
- package/src/cli/util/observation-engine.js +412 -0
- package/src/cli/util/observe-writer.js +25 -0
- package/src/cli/util/paths.js +30 -0
- package/src/cli/util/project-discovery.js +297 -0
- package/src/cli/util/project-writer.js +26 -0
- package/src/cli/util/redact.js +128 -0
- package/src/cli/util/run-id.js +30 -0
- package/src/cli/util/runtime-budget.js +147 -0
- package/src/cli/util/summary-writer.js +43 -0
- package/src/types/global.d.ts +28 -0
- package/src/types/ts-ast.d.ts +24 -0
- package/src/verax/cli/ci-summary.js +35 -0
- package/src/verax/cli/context-explanation.js +89 -0
- package/src/verax/cli/doctor.js +277 -0
- package/src/verax/cli/error-normalizer.js +154 -0
- package/src/verax/cli/explain-output.js +105 -0
- package/src/verax/cli/finding-explainer.js +130 -0
- package/src/verax/cli/init.js +237 -0
- package/src/verax/cli/run-overview.js +163 -0
- package/src/verax/cli/url-safety.js +111 -0
- package/src/verax/cli/wizard.js +109 -0
- package/src/verax/cli/zero-findings-explainer.js +57 -0
- package/src/verax/cli/zero-interaction-explainer.js +127 -0
- package/src/verax/core/action-classifier.js +86 -0
- package/src/verax/core/budget-engine.js +218 -0
- package/src/verax/core/canonical-outcomes.js +157 -0
- package/src/verax/core/decision-snapshot.js +335 -0
- package/src/verax/core/determinism-model.js +432 -0
- package/src/verax/core/incremental-store.js +245 -0
- package/src/verax/core/invariants.js +356 -0
- package/src/verax/core/promise-model.js +230 -0
- package/src/verax/core/replay-validator.js +350 -0
- package/src/verax/core/replay.js +222 -0
- package/src/verax/core/run-id.js +175 -0
- package/src/verax/core/run-manifest.js +99 -0
- package/src/verax/core/silence-impact.js +369 -0
- package/src/verax/core/silence-model.js +523 -0
- package/src/verax/detect/comparison.js +7 -34
- package/src/verax/detect/confidence-engine.js +764 -329
- package/src/verax/detect/detection-engine.js +293 -0
- package/src/verax/detect/evidence-index.js +127 -0
- package/src/verax/detect/expectation-model.js +241 -168
- package/src/verax/detect/explanation-helpers.js +187 -0
- package/src/verax/detect/finding-detector.js +450 -0
- package/src/verax/detect/findings-writer.js +41 -12
- package/src/verax/detect/flow-detector.js +366 -0
- package/src/verax/detect/index.js +200 -288
- package/src/verax/detect/interactive-findings.js +612 -0
- package/src/verax/detect/signal-mapper.js +308 -0
- package/src/verax/detect/skip-classifier.js +4 -4
- package/src/verax/detect/verdict-engine.js +561 -0
- package/src/verax/evidence-index-writer.js +61 -0
- package/src/verax/flow/flow-engine.js +3 -2
- package/src/verax/flow/flow-spec.js +1 -2
- package/src/verax/index.js +103 -15
- package/src/verax/intel/effect-detector.js +368 -0
- package/src/verax/intel/handler-mapper.js +249 -0
- package/src/verax/intel/index.js +281 -0
- package/src/verax/intel/route-extractor.js +280 -0
- package/src/verax/intel/ts-program.js +256 -0
- package/src/verax/intel/vue-navigation-extractor.js +642 -0
- package/src/verax/intel/vue-router-extractor.js +325 -0
- package/src/verax/learn/action-contract-extractor.js +338 -104
- package/src/verax/learn/ast-contract-extractor.js +148 -6
- package/src/verax/learn/flow-extractor.js +172 -0
- package/src/verax/learn/index.js +36 -2
- package/src/verax/learn/manifest-writer.js +122 -58
- package/src/verax/learn/project-detector.js +40 -0
- package/src/verax/learn/route-extractor.js +28 -97
- package/src/verax/learn/route-validator.js +8 -7
- package/src/verax/learn/state-extractor.js +212 -0
- package/src/verax/learn/static-extractor-navigation.js +114 -0
- package/src/verax/learn/static-extractor-validation.js +88 -0
- package/src/verax/learn/static-extractor.js +119 -10
- package/src/verax/learn/truth-assessor.js +24 -21
- package/src/verax/learn/ts-contract-resolver.js +14 -12
- package/src/verax/observe/aria-sensor.js +211 -0
- package/src/verax/observe/browser.js +30 -6
- package/src/verax/observe/console-sensor.js +2 -18
- package/src/verax/observe/domain-boundary.js +10 -1
- package/src/verax/observe/expectation-executor.js +513 -0
- package/src/verax/observe/flow-matcher.js +143 -0
- package/src/verax/observe/focus-sensor.js +196 -0
- package/src/verax/observe/human-driver.js +660 -273
- package/src/verax/observe/index.js +910 -26
- package/src/verax/observe/interaction-discovery.js +378 -15
- package/src/verax/observe/interaction-runner.js +562 -197
- package/src/verax/observe/loading-sensor.js +145 -0
- package/src/verax/observe/navigation-sensor.js +255 -0
- package/src/verax/observe/network-sensor.js +55 -7
- package/src/verax/observe/observed-expectation-deriver.js +186 -0
- package/src/verax/observe/observed-expectation.js +305 -0
- package/src/verax/observe/page-frontier.js +234 -0
- package/src/verax/observe/settle.js +38 -17
- package/src/verax/observe/state-sensor.js +393 -0
- package/src/verax/observe/state-ui-sensor.js +7 -1
- package/src/verax/observe/timing-sensor.js +228 -0
- package/src/verax/observe/traces-writer.js +73 -21
- package/src/verax/observe/ui-signal-sensor.js +143 -17
- package/src/verax/scan-summary-writer.js +80 -15
- package/src/verax/shared/artifact-manager.js +111 -9
- package/src/verax/shared/budget-profiles.js +136 -0
- package/src/verax/shared/caching.js +1 -1
- package/src/verax/shared/ci-detection.js +39 -0
- package/src/verax/shared/config-loader.js +169 -0
- package/src/verax/shared/dynamic-route-utils.js +224 -0
- package/src/verax/shared/expectation-coverage.js +44 -0
- package/src/verax/shared/expectation-prover.js +81 -0
- package/src/verax/shared/expectation-tracker.js +201 -0
- package/src/verax/shared/expectations-writer.js +60 -0
- package/src/verax/shared/first-run.js +44 -0
- package/src/verax/shared/progress-reporter.js +171 -0
- package/src/verax/shared/retry-policy.js +9 -1
- package/src/verax/shared/root-artifacts.js +49 -0
- package/src/verax/shared/scan-budget.js +86 -0
- package/src/verax/shared/url-normalizer.js +162 -0
- package/src/verax/shared/zip-artifacts.js +66 -0
- 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
|
|
2
|
-
import {
|
|
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
|
-
|
|
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
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
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
|
-
|
|
83
|
-
|
|
84
|
-
|
|
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
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
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
|
-
|
|
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
|
|
102
|
+
// Use redirectChain property if available (Playwright API)
|
|
103
103
|
let redirectChain = [];
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
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
|
+
|