@veraxhq/verax 0.1.0 → 0.2.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.
- package/README.md +123 -88
- package/bin/verax.js +11 -452
- package/package.json +14 -36
- package/src/cli/commands/default.js +523 -0
- package/src/cli/commands/doctor.js +165 -0
- package/src/cli/commands/inspect.js +109 -0
- package/src/cli/commands/run.js +402 -0
- package/src/cli/entry.js +196 -0
- package/src/cli/util/atomic-write.js +37 -0
- package/src/cli/util/detection-engine.js +296 -0
- package/src/cli/util/env-url.js +33 -0
- package/src/cli/util/errors.js +44 -0
- package/src/cli/util/events.js +34 -0
- package/src/cli/util/expectation-extractor.js +378 -0
- package/src/cli/util/findings-writer.js +31 -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 +366 -0
- package/src/cli/util/observe-writer.js +25 -0
- package/src/cli/util/paths.js +29 -0
- package/src/cli/util/project-discovery.js +277 -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/summary-writer.js +32 -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 +101 -0
- package/src/verax/cli/wizard.js +98 -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 +403 -0
- package/src/verax/core/incremental-store.js +237 -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 +521 -0
- package/src/verax/detect/comparison.js +2 -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 +177 -0
- package/src/verax/detect/expectation-model.js +194 -172
- 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 +44 -8
- package/src/verax/detect/flow-detector.js +366 -0
- package/src/verax/detect/index.js +172 -286
- package/src/verax/detect/interactive-findings.js +613 -0
- package/src/verax/detect/signal-mapper.js +308 -0
- package/src/verax/detect/verdict-engine.js +563 -0
- package/src/verax/evidence-index-writer.js +61 -0
- package/src/verax/index.js +90 -14
- 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 +579 -0
- package/src/verax/intel/vue-router-extractor.js +323 -0
- package/src/verax/learn/action-contract-extractor.js +335 -101
- package/src/verax/learn/ast-contract-extractor.js +95 -5
- package/src/verax/learn/flow-extractor.js +172 -0
- package/src/verax/learn/manifest-writer.js +97 -47
- package/src/verax/learn/project-detector.js +40 -0
- package/src/verax/learn/route-extractor.js +27 -96
- 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 +112 -4
- package/src/verax/learn/truth-assessor.js +24 -21
- package/src/verax/observe/aria-sensor.js +211 -0
- package/src/verax/observe/browser.js +10 -5
- package/src/verax/observe/console-sensor.js +1 -17
- package/src/verax/observe/domain-boundary.js +10 -1
- package/src/verax/observe/expectation-executor.js +512 -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 +643 -275
- package/src/verax/observe/index.js +908 -27
- package/src/verax/observe/index.js.backup +1 -0
- package/src/verax/observe/interaction-discovery.js +365 -14
- package/src/verax/observe/interaction-runner.js +563 -198
- package/src/verax/observe/loading-sensor.js +139 -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 +37 -17
- package/src/verax/observe/state-sensor.js +389 -0
- package/src/verax/observe/timing-sensor.js +228 -0
- package/src/verax/observe/traces-writer.js +61 -20
- package/src/verax/observe/ui-signal-sensor.js +136 -17
- package/src/verax/scan-summary-writer.js +77 -15
- package/src/verax/shared/artifact-manager.js +110 -8
- package/src/verax/shared/budget-profiles.js +136 -0
- package/src/verax/shared/ci-detection.js +39 -0
- package/src/verax/shared/config-loader.js +170 -0
- package/src/verax/shared/dynamic-route-utils.js +218 -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 +14 -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 +65 -0
- package/src/verax/validate/context-validator.js +244 -0
- package/src/verax/validate/context-validator.js.bak +0 -0
|
@@ -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
|
+
|
|
@@ -2,7 +2,6 @@ import { glob } from 'glob';
|
|
|
2
2
|
import { resolve, dirname, join, relative } from 'path';
|
|
3
3
|
import { readFileSync, existsSync } from 'fs';
|
|
4
4
|
import { parse } from 'node-html-parser';
|
|
5
|
-
import { ExpectationProof } from '../shared/expectation-proof.js';
|
|
6
5
|
|
|
7
6
|
const MAX_HTML_FILES = 200;
|
|
8
7
|
|
|
@@ -121,7 +120,6 @@ function extractButtonNavigationExpectations(root, fromPath, file, routeMap, pro
|
|
|
121
120
|
fromPath: fromPath,
|
|
122
121
|
type: 'navigation',
|
|
123
122
|
targetPath: targetPath,
|
|
124
|
-
proof: ExpectationProof.PROVEN_EXPECTATION,
|
|
125
123
|
evidence: {
|
|
126
124
|
source: file,
|
|
127
125
|
selectorHint: selectorHint
|
|
@@ -151,7 +149,6 @@ function extractFormSubmissionExpectations(root, fromPath, file, routeMap, proje
|
|
|
151
149
|
fromPath: fromPath,
|
|
152
150
|
type: 'form_submission',
|
|
153
151
|
targetPath: targetPath,
|
|
154
|
-
proof: ExpectationProof.PROVEN_EXPECTATION,
|
|
155
152
|
evidence: {
|
|
156
153
|
source: file,
|
|
157
154
|
selectorHint: selectorHint
|
|
@@ -162,6 +159,105 @@ function extractFormSubmissionExpectations(root, fromPath, file, routeMap, proje
|
|
|
162
159
|
return expectations;
|
|
163
160
|
}
|
|
164
161
|
|
|
162
|
+
function extractNetworkExpectations(root, fromPath, file, projectDir) {
|
|
163
|
+
const expectations = [];
|
|
164
|
+
|
|
165
|
+
// Extract from inline scripts
|
|
166
|
+
const scripts = root.querySelectorAll('script');
|
|
167
|
+
for (const script of scripts) {
|
|
168
|
+
const scriptContent = script.textContent || '';
|
|
169
|
+
if (!scriptContent) continue;
|
|
170
|
+
|
|
171
|
+
// Find fetch() calls with string literals
|
|
172
|
+
const fetchMatches = scriptContent.matchAll(/fetch\s*\(\s*['"]([^'"]+)['"]/g);
|
|
173
|
+
for (const match of fetchMatches) {
|
|
174
|
+
const endpoint = match[1];
|
|
175
|
+
if (!endpoint) continue;
|
|
176
|
+
|
|
177
|
+
// Only extract if it's an API endpoint (starts with /api/)
|
|
178
|
+
if (!endpoint.startsWith('/api/')) continue;
|
|
179
|
+
|
|
180
|
+
// Find the button/function that triggers this
|
|
181
|
+
// Look for onclick handlers or function definitions
|
|
182
|
+
const buttonId = scriptContent.match(/getElementById\s*\(\s*['"]([^'"]+)['"]/)?.[1];
|
|
183
|
+
const functionName = scriptContent.match(/function\s+(\w+)\s*\(/)?.[1];
|
|
184
|
+
|
|
185
|
+
let selectorHint = null;
|
|
186
|
+
if (buttonId) {
|
|
187
|
+
selectorHint = `#${buttonId}`;
|
|
188
|
+
} else if (functionName) {
|
|
189
|
+
// Try to find button with onclick that calls this function
|
|
190
|
+
const buttons = root.querySelectorAll(`button[onclick*="${functionName}"]`);
|
|
191
|
+
if (buttons.length > 0) {
|
|
192
|
+
const btn = buttons[0];
|
|
193
|
+
selectorHint = btn.id ? `#${btn.id}` : `button[onclick*="${functionName}"]`;
|
|
194
|
+
}
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
if (selectorHint) {
|
|
198
|
+
// Determine method from fetch options if present
|
|
199
|
+
let method = 'GET';
|
|
200
|
+
const methodMatch = scriptContent.match(/method\s*:\s*['"]([^'"]+)['"]/i);
|
|
201
|
+
if (methodMatch) {
|
|
202
|
+
method = methodMatch[1].toUpperCase();
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
expectations.push({
|
|
206
|
+
fromPath: fromPath,
|
|
207
|
+
type: 'network_action',
|
|
208
|
+
expectedTarget: endpoint,
|
|
209
|
+
urlPath: endpoint,
|
|
210
|
+
method: method,
|
|
211
|
+
proof: 'PROVEN_EXPECTATION',
|
|
212
|
+
sourceRef: `${file}:${scriptContent.indexOf(match[0])}`,
|
|
213
|
+
selectorHint: selectorHint,
|
|
214
|
+
evidence: {
|
|
215
|
+
source: file,
|
|
216
|
+
selectorHint: selectorHint
|
|
217
|
+
}
|
|
218
|
+
});
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
|
|
223
|
+
// Also check onclick attributes directly
|
|
224
|
+
const buttons = root.querySelectorAll('button[onclick]');
|
|
225
|
+
for (const button of buttons) {
|
|
226
|
+
const onclick = button.getAttribute('onclick') || '';
|
|
227
|
+
const fetchMatch = onclick.match(/fetch\s*\(\s*['"]([^'"]+)['"]/);
|
|
228
|
+
if (fetchMatch) {
|
|
229
|
+
const endpoint = fetchMatch[1];
|
|
230
|
+
if (endpoint.startsWith('/api/')) {
|
|
231
|
+
const buttonId = button.getAttribute('id');
|
|
232
|
+
const selectorHint = buttonId ? `#${buttonId}` : `button[onclick*="${endpoint}"]`;
|
|
233
|
+
|
|
234
|
+
let method = 'GET';
|
|
235
|
+
const methodMatch = onclick.match(/method\s*:\s*['"]([^'"]+)['"]/i);
|
|
236
|
+
if (methodMatch) {
|
|
237
|
+
method = methodMatch[1].toUpperCase();
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
expectations.push({
|
|
241
|
+
fromPath: fromPath,
|
|
242
|
+
type: 'network_action',
|
|
243
|
+
expectedTarget: endpoint,
|
|
244
|
+
urlPath: endpoint,
|
|
245
|
+
method: method,
|
|
246
|
+
proof: 'PROVEN_EXPECTATION',
|
|
247
|
+
sourceRef: `${file}:onclick`,
|
|
248
|
+
selectorHint: selectorHint,
|
|
249
|
+
evidence: {
|
|
250
|
+
source: file,
|
|
251
|
+
selectorHint: selectorHint
|
|
252
|
+
}
|
|
253
|
+
});
|
|
254
|
+
}
|
|
255
|
+
}
|
|
256
|
+
}
|
|
257
|
+
|
|
258
|
+
return expectations;
|
|
259
|
+
}
|
|
260
|
+
|
|
165
261
|
export async function extractStaticExpectations(projectDir, routes) {
|
|
166
262
|
const expectations = [];
|
|
167
263
|
const routeMap = new Map(routes.map(r => [r.path, r]));
|
|
@@ -199,7 +295,6 @@ export async function extractStaticExpectations(projectDir, routes) {
|
|
|
199
295
|
fromPath: fromPath,
|
|
200
296
|
type: 'navigation',
|
|
201
297
|
targetPath: targetPath,
|
|
202
|
-
proof: ExpectationProof.PROVEN_EXPECTATION,
|
|
203
298
|
evidence: {
|
|
204
299
|
source: file,
|
|
205
300
|
selectorHint: selectorHint
|
|
@@ -212,6 +307,19 @@ export async function extractStaticExpectations(projectDir, routes) {
|
|
|
212
307
|
|
|
213
308
|
const formExpectations = extractFormSubmissionExpectations(root, fromPath, file, routeMap, projectDir);
|
|
214
309
|
expectations.push(...formExpectations);
|
|
310
|
+
|
|
311
|
+
const networkExpectations = extractNetworkExpectations(root, fromPath, file, projectDir);
|
|
312
|
+
expectations.push(...networkExpectations);
|
|
313
|
+
|
|
314
|
+
// NAVIGATION INTELLIGENCE v2: Extract navigation expectations from inline scripts
|
|
315
|
+
const { extractNavigationExpectations } = await import('./static-extractor-navigation.js');
|
|
316
|
+
const navigationExpectations = extractNavigationExpectations(root, fromPath, file, projectDir);
|
|
317
|
+
expectations.push(...navigationExpectations);
|
|
318
|
+
|
|
319
|
+
// VALIDATION INTELLIGENCE v1: Extract validation_block expectations from inline scripts
|
|
320
|
+
const { extractValidationExpectations } = await import('./static-extractor-validation.js');
|
|
321
|
+
const validationExpectations = extractValidationExpectations(root, fromPath, file, projectDir);
|
|
322
|
+
expectations.push(...validationExpectations);
|
|
215
323
|
} catch (error) {
|
|
216
324
|
continue;
|
|
217
325
|
}
|
|
@@ -3,7 +3,7 @@ import { hasReactRouterDom } from './project-detector.js';
|
|
|
3
3
|
|
|
4
4
|
const MAX_HTML_FILES = 200;
|
|
5
5
|
|
|
6
|
-
export async function assessLearnTruth(projectDir, projectType, routes, staticExpectations
|
|
6
|
+
export async function assessLearnTruth(projectDir, projectType, routes, staticExpectations) {
|
|
7
7
|
const truth = {
|
|
8
8
|
routesDiscovered: routes.length,
|
|
9
9
|
routesSource: 'none',
|
|
@@ -18,14 +18,24 @@ export async function assessLearnTruth(projectDir, projectType, routes, staticEx
|
|
|
18
18
|
if (projectType === 'nextjs_app_router' || projectType === 'nextjs_pages_router') {
|
|
19
19
|
truth.routesSource = 'nextjs_fs';
|
|
20
20
|
truth.routesConfidence = 'HIGH';
|
|
21
|
+
} else if (projectType === 'vue_router') {
|
|
22
|
+
truth.routesSource = 'vue_router_ast';
|
|
23
|
+
truth.routesConfidence = 'HIGH';
|
|
21
24
|
|
|
22
|
-
|
|
23
|
-
|
|
24
|
-
truth.
|
|
25
|
-
|
|
25
|
+
if (staticExpectations && staticExpectations.length > 0) {
|
|
26
|
+
truth.expectationsDiscovered = staticExpectations.length;
|
|
27
|
+
truth.expectationsStrong = staticExpectations.filter(e =>
|
|
28
|
+
e.type === 'spa_navigation'
|
|
29
|
+
).length;
|
|
26
30
|
truth.expectationsWeak = 0;
|
|
27
|
-
truth.expectationsSource = 'ast_contracts';
|
|
28
31
|
}
|
|
32
|
+
} else if (projectType === 'vue_spa') {
|
|
33
|
+
truth.routesSource = 'vue_no_router';
|
|
34
|
+
truth.routesConfidence = 'LOW';
|
|
35
|
+
truth.limitations.push({
|
|
36
|
+
code: 'VUE_ROUTER_NOT_INSTALLED',
|
|
37
|
+
message: 'Vue detected but vue-router not installed. Routes cannot be extracted from router configuration.'
|
|
38
|
+
});
|
|
29
39
|
} else if (projectType === 'static') {
|
|
30
40
|
truth.routesSource = 'static_html';
|
|
31
41
|
|
|
@@ -67,21 +77,14 @@ export async function assessLearnTruth(projectDir, projectType, routes, staticEx
|
|
|
67
77
|
});
|
|
68
78
|
}
|
|
69
79
|
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
78
|
-
truth.expectationsStrong = 0;
|
|
79
|
-
truth.expectationsWeak = 0;
|
|
80
|
-
truth.warnings.push({
|
|
81
|
-
code: 'NO_AST_CONTRACTS_FOUND',
|
|
82
|
-
message: 'No JSX Link/NavLink elements with static href/to found. No PROVEN expectations available.'
|
|
83
|
-
});
|
|
84
|
-
}
|
|
80
|
+
truth.warnings.push({
|
|
81
|
+
code: 'REACT_ROUTE_EXTRACTION_FRAGILE',
|
|
82
|
+
message: 'Route extraction uses regex parsing of source files. Dynamic routes, nested routers, and code-split route definitions may be missed.'
|
|
83
|
+
});
|
|
84
|
+
|
|
85
|
+
truth.expectationsDiscovered = routes.length;
|
|
86
|
+
truth.expectationsStrong = routes.length;
|
|
87
|
+
truth.expectationsWeak = 0;
|
|
85
88
|
} else if (projectType === 'unknown') {
|
|
86
89
|
truth.routesSource = 'none';
|
|
87
90
|
truth.routesConfidence = 'LOW';
|