@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
|
@@ -0,0 +1,249 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CODE INTELLIGENCE v1 — JSX Handler Mapper
|
|
3
|
+
*
|
|
4
|
+
* Maps JSX event handlers (onClick, onSubmit) to function declarations.
|
|
5
|
+
* Resolves handler identifiers to actual function bodies.
|
|
6
|
+
* Captures sourceRef for both element and handler.
|
|
7
|
+
*/
|
|
8
|
+
|
|
9
|
+
import ts from 'typescript';
|
|
10
|
+
import { parseFile, findNodes, resolveIdentifier, isFunctionNode, getNodeLocation } from './ts-program.js';
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Extract handler mappings from JSX elements.
|
|
14
|
+
*
|
|
15
|
+
* @param {string} projectRoot - Project root
|
|
16
|
+
* @param {Object} program - TypeScript program
|
|
17
|
+
* @returns {Array} - Handler mappings { element, handlerName, handlerNode, elementSourceRef, handlerSourceRef }
|
|
18
|
+
*/
|
|
19
|
+
export function extractHandlerMappings(projectRoot, program) {
|
|
20
|
+
const mappings = [];
|
|
21
|
+
|
|
22
|
+
if (!program || !program.program || !program.typeChecker) return mappings;
|
|
23
|
+
|
|
24
|
+
for (const filePath of program.sourceFiles) {
|
|
25
|
+
const ast = parseFile(filePath, true);
|
|
26
|
+
if (!ast) continue;
|
|
27
|
+
|
|
28
|
+
// Find JSX elements with event handlers
|
|
29
|
+
const jsxElements = findNodes(ast, node => {
|
|
30
|
+
return ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node);
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
for (const element of jsxElements) {
|
|
34
|
+
const tagName = getTagName(element);
|
|
35
|
+
const attributes = element.attributes;
|
|
36
|
+
if (!attributes || !attributes.properties) continue;
|
|
37
|
+
|
|
38
|
+
for (const attr of attributes.properties) {
|
|
39
|
+
if (!ts.isJsxAttribute(attr)) continue;
|
|
40
|
+
|
|
41
|
+
const attrName = attr.name;
|
|
42
|
+
if (!ts.isIdentifier(attrName)) continue;
|
|
43
|
+
|
|
44
|
+
const eventType = attrName.text;
|
|
45
|
+
if (!['onClick', 'onSubmit', 'onChange', 'onBlur'].includes(eventType)) continue;
|
|
46
|
+
|
|
47
|
+
const initializer = attr.initializer;
|
|
48
|
+
if (!initializer) continue;
|
|
49
|
+
|
|
50
|
+
let handlerNode = null;
|
|
51
|
+
let handlerName = null;
|
|
52
|
+
|
|
53
|
+
// JsxExpression: onClick={handleClick}
|
|
54
|
+
if (ts.isJsxExpression(initializer)) {
|
|
55
|
+
const expr = initializer.expression;
|
|
56
|
+
|
|
57
|
+
if (ts.isIdentifier(expr)) {
|
|
58
|
+
// Reference to function: onClick={handleClick}
|
|
59
|
+
handlerName = expr.text;
|
|
60
|
+
const declaration = resolveIdentifier(program.typeChecker, expr);
|
|
61
|
+
if (declaration && isFunctionNode(declaration)) {
|
|
62
|
+
handlerNode = declaration;
|
|
63
|
+
} else if (declaration && ts.isVariableDeclaration(declaration)) {
|
|
64
|
+
// const handleClick = () => {...}
|
|
65
|
+
const init = declaration.initializer;
|
|
66
|
+
if (init && isFunctionNode(init)) {
|
|
67
|
+
handlerNode = init;
|
|
68
|
+
}
|
|
69
|
+
} else {
|
|
70
|
+
// Fallback for JS/JSX where typeChecker cannot resolve identifiers.
|
|
71
|
+
// Search the file for a matching function/variable declaration manually.
|
|
72
|
+
const localMatch = findNodes(ast, n => {
|
|
73
|
+
if (ts.isFunctionDeclaration(n) && n.name?.text === handlerName) return true;
|
|
74
|
+
if (ts.isVariableDeclaration(n) && ts.isIdentifier(n.name) && n.name.text === handlerName) {
|
|
75
|
+
return isFunctionNode(n.initializer || n);
|
|
76
|
+
}
|
|
77
|
+
return false;
|
|
78
|
+
})[0];
|
|
79
|
+
|
|
80
|
+
if (localMatch) {
|
|
81
|
+
if (isFunctionNode(localMatch)) {
|
|
82
|
+
handlerNode = localMatch;
|
|
83
|
+
} else if (ts.isVariableDeclaration(localMatch)) {
|
|
84
|
+
const init = localMatch.initializer;
|
|
85
|
+
if (init && isFunctionNode(init)) {
|
|
86
|
+
handlerNode = init;
|
|
87
|
+
}
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
} else if (isFunctionNode(expr)) {
|
|
92
|
+
// Inline arrow: onClick={() => {...}}
|
|
93
|
+
handlerNode = expr;
|
|
94
|
+
handlerName = 'inline';
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
if (handlerNode) {
|
|
99
|
+
const elementLoc = getNodeLocation(ast, element, projectRoot);
|
|
100
|
+
const handlerSourceFile = program.program.getSourceFile(handlerNode.getSourceFile().fileName);
|
|
101
|
+
const handlerLoc = handlerSourceFile
|
|
102
|
+
? getNodeLocation(handlerSourceFile, handlerNode, projectRoot)
|
|
103
|
+
: null;
|
|
104
|
+
|
|
105
|
+
// Capture simple attribute map for matching hints (e.g., href/to)
|
|
106
|
+
const attrMap = {};
|
|
107
|
+
for (const ap of attributes.properties) {
|
|
108
|
+
if (!ts.isJsxAttribute(ap)) continue;
|
|
109
|
+
if (!ts.isIdentifier(ap.name)) continue;
|
|
110
|
+
const aname = ap.name.text;
|
|
111
|
+
const init = ap.initializer;
|
|
112
|
+
if (!init) continue;
|
|
113
|
+
if (ts.isStringLiteral(init)) {
|
|
114
|
+
attrMap[aname] = init.text;
|
|
115
|
+
} else if (ts.isJsxExpression(init) && init.expression && ts.isStringLiteral(init.expression)) {
|
|
116
|
+
attrMap[aname] = init.expression.text;
|
|
117
|
+
}
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
mappings.push({
|
|
121
|
+
element: {
|
|
122
|
+
tag: tagName,
|
|
123
|
+
event: eventType,
|
|
124
|
+
attrs: attrMap,
|
|
125
|
+
sourceRef: elementLoc.sourceRef,
|
|
126
|
+
file: elementLoc.file,
|
|
127
|
+
line: elementLoc.line
|
|
128
|
+
},
|
|
129
|
+
handler: {
|
|
130
|
+
name: handlerName,
|
|
131
|
+
node: handlerNode,
|
|
132
|
+
sourceRef: handlerLoc?.sourceRef || elementLoc.sourceRef,
|
|
133
|
+
file: handlerLoc?.file || elementLoc.file,
|
|
134
|
+
line: handlerLoc?.line || elementLoc.line
|
|
135
|
+
}
|
|
136
|
+
});
|
|
137
|
+
}
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
}
|
|
141
|
+
|
|
142
|
+
return mappings;
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
/**
|
|
146
|
+
* Extract navigation-capable JSX elements (Link/NavLink/a) with static href/to.
|
|
147
|
+
* Returns minimal element records with attrs and sourceRef, independent of handlers.
|
|
148
|
+
*/
|
|
149
|
+
export function extractNavElements(projectRoot, program) {
|
|
150
|
+
const elements = [];
|
|
151
|
+
if (!program || !program.program || !program.typeChecker) return elements;
|
|
152
|
+
for (const filePath of program.sourceFiles) {
|
|
153
|
+
const ast = parseFile(filePath, true);
|
|
154
|
+
if (!ast) continue;
|
|
155
|
+
const jsxElements = findNodes(ast, node => ts.isJsxOpeningElement(node) || ts.isJsxSelfClosingElement(node));
|
|
156
|
+
for (const element of jsxElements) {
|
|
157
|
+
const tagName = getTagName(element);
|
|
158
|
+
if (!['Link', 'NavLink', 'a'].includes(tagName)) continue;
|
|
159
|
+
const attributes = element.attributes;
|
|
160
|
+
if (!attributes || !attributes.properties) continue;
|
|
161
|
+
let href = null, to = null;
|
|
162
|
+
for (const attr of attributes.properties) {
|
|
163
|
+
if (!ts.isJsxAttribute(attr)) continue;
|
|
164
|
+
if (!ts.isIdentifier(attr.name)) continue;
|
|
165
|
+
const an = attr.name.text;
|
|
166
|
+
const init = attr.initializer;
|
|
167
|
+
if (!init) continue;
|
|
168
|
+
if (an === 'href' || an === 'to') {
|
|
169
|
+
if (ts.isStringLiteral(init)) {
|
|
170
|
+
if (an === 'href') href = init.text; else to = init.text;
|
|
171
|
+
} else if (ts.isJsxExpression(init) && init.expression && ts.isStringLiteral(init.expression)) {
|
|
172
|
+
if (an === 'href') href = init.expression.text; else to = init.expression.text;
|
|
173
|
+
} else if (ts.isJsxExpression(init) && init.expression && ts.isNoSubstitutionTemplateLiteral(init.expression)) {
|
|
174
|
+
if (an === 'href') href = init.expression.text; else to = init.expression.text;
|
|
175
|
+
}
|
|
176
|
+
}
|
|
177
|
+
}
|
|
178
|
+
const target = to || href;
|
|
179
|
+
if (typeof target === 'string' && target.startsWith('/') && !target.startsWith('//')) {
|
|
180
|
+
const loc = getNodeLocation(ast, element, projectRoot);
|
|
181
|
+
elements.push({
|
|
182
|
+
tag: tagName,
|
|
183
|
+
attrs: { href, to },
|
|
184
|
+
sourceRef: loc.sourceRef,
|
|
185
|
+
file: loc.file,
|
|
186
|
+
line: loc.line
|
|
187
|
+
});
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
return elements;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Get tag name from JSX element.
|
|
196
|
+
*
|
|
197
|
+
* @param {ts.Node} element - JSX element node
|
|
198
|
+
* @returns {string} - Tag name
|
|
199
|
+
*/
|
|
200
|
+
function getTagName(element) {
|
|
201
|
+
const tagName = element.tagName;
|
|
202
|
+
if (ts.isIdentifier(tagName)) {
|
|
203
|
+
return tagName.text;
|
|
204
|
+
}
|
|
205
|
+
return 'unknown';
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Extract selector hint from JSX element for stable identification.
|
|
210
|
+
*
|
|
211
|
+
* @param {ts.Node} element - JSX element node
|
|
212
|
+
* @returns {string|null} - Selector hint (id, data-testid, or null)
|
|
213
|
+
*/
|
|
214
|
+
export function extractSelectorHint(element) {
|
|
215
|
+
const attributes = element.attributes;
|
|
216
|
+
if (!attributes || !attributes.properties) return null;
|
|
217
|
+
|
|
218
|
+
// Priority: id > data-testid > data-cy > role
|
|
219
|
+
for (const attr of attributes.properties) {
|
|
220
|
+
if (!ts.isJsxAttribute(attr)) continue;
|
|
221
|
+
|
|
222
|
+
const name = attr.name;
|
|
223
|
+
if (!ts.isIdentifier(name)) continue;
|
|
224
|
+
|
|
225
|
+
const attrName = name.text;
|
|
226
|
+
const initializer = attr.initializer;
|
|
227
|
+
|
|
228
|
+
if (attrName === 'id' && initializer && ts.isStringLiteral(initializer)) {
|
|
229
|
+
return `#${initializer.text}`;
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
if (attrName === 'data-testid' && initializer) {
|
|
233
|
+
if (ts.isStringLiteral(initializer)) {
|
|
234
|
+
return `[data-testid="${initializer.text}"]`;
|
|
235
|
+
} else if (ts.isJsxExpression(initializer)) {
|
|
236
|
+
const expr = initializer.expression;
|
|
237
|
+
if (expr && ts.isStringLiteral(expr)) {
|
|
238
|
+
return `[data-testid="${expr.text}"]`;
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
if (attrName === 'data-cy' && initializer && ts.isStringLiteral(initializer)) {
|
|
244
|
+
return `[data-cy="${initializer.text}"]`;
|
|
245
|
+
}
|
|
246
|
+
}
|
|
247
|
+
|
|
248
|
+
return null;
|
|
249
|
+
}
|
|
@@ -0,0 +1,281 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CODE INTELLIGENCE v1 — Main Orchestrator
|
|
3
|
+
*
|
|
4
|
+
* Combines all intelligence modules to emit PROVEN expectations:
|
|
5
|
+
* - Routes from AST
|
|
6
|
+
* - Handlers from JSX
|
|
7
|
+
* - Effects from function bodies
|
|
8
|
+
*
|
|
9
|
+
* Every expectation has sourceRef. NO GUESSING.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import { createTSProgram } from './ts-program.js';
|
|
13
|
+
import { extractRoutes } from './route-extractor.js';
|
|
14
|
+
import { extractHandlerMappings, extractSelectorHint, extractNavElements } from './handler-mapper.js';
|
|
15
|
+
import { detectEffects } from './effect-detector.js';
|
|
16
|
+
import { extractVueNavigationPromises } from './vue-navigation-extractor.js';
|
|
17
|
+
import { normalizeDynamicRoute, normalizeTemplateLiteral } from '../shared/dynamic-route-utils.js';
|
|
18
|
+
|
|
19
|
+
/**
|
|
20
|
+
* Run code intelligence analysis.
|
|
21
|
+
*
|
|
22
|
+
* @param {string} projectRoot - Project root
|
|
23
|
+
* @returns {Promise<Object>} - { routes, handlerMappings, expectations, stats }
|
|
24
|
+
*/
|
|
25
|
+
export async function runCodeIntelligence(projectRoot) {
|
|
26
|
+
const result = {
|
|
27
|
+
routes: [],
|
|
28
|
+
handlerMappings: [],
|
|
29
|
+
expectations: [],
|
|
30
|
+
stats: {
|
|
31
|
+
routesFound: 0,
|
|
32
|
+
handlersFound: 0,
|
|
33
|
+
effectsFound: 0,
|
|
34
|
+
expectationsGenerated: 0,
|
|
35
|
+
expectationsProven: 0
|
|
36
|
+
},
|
|
37
|
+
error: null
|
|
38
|
+
};
|
|
39
|
+
|
|
40
|
+
try {
|
|
41
|
+
// Step 1: Create TypeScript Program
|
|
42
|
+
const program = createTSProgram(projectRoot, { includeJs: true });
|
|
43
|
+
|
|
44
|
+
if (program.error) {
|
|
45
|
+
result.error = program.error;
|
|
46
|
+
return result;
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
// Step 2: Extract routes (AST-based, no regex)
|
|
50
|
+
const routes = extractRoutes(projectRoot, program);
|
|
51
|
+
result.routes = routes;
|
|
52
|
+
result.stats.routesFound = routes.length;
|
|
53
|
+
|
|
54
|
+
// Step 3: Extract handler mappings (JSX → Function)
|
|
55
|
+
const handlerMappings = extractHandlerMappings(projectRoot, program);
|
|
56
|
+
result.handlerMappings = handlerMappings;
|
|
57
|
+
result.stats.handlersFound = handlerMappings.length;
|
|
58
|
+
|
|
59
|
+
// Step 4: For each handler, detect effects
|
|
60
|
+
for (const mapping of handlerMappings) {
|
|
61
|
+
const handlerSourceFile = mapping.handler.node.getSourceFile();
|
|
62
|
+
// VALIDATION INTELLIGENCE v1: Pass eventType to detectEffects
|
|
63
|
+
const eventType = mapping.element?.event || null;
|
|
64
|
+
const effects = detectEffects(
|
|
65
|
+
mapping.handler.node,
|
|
66
|
+
handlerSourceFile,
|
|
67
|
+
projectRoot,
|
|
68
|
+
eventType
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
mapping.effects = effects;
|
|
72
|
+
result.stats.effectsFound += effects.length;
|
|
73
|
+
|
|
74
|
+
// Step 5a: Generate expectations from (element + handler + effect)
|
|
75
|
+
for (const effect of effects) {
|
|
76
|
+
const expectation = generateExpectation(mapping, effect);
|
|
77
|
+
if (expectation) {
|
|
78
|
+
result.expectations.push(expectation);
|
|
79
|
+
result.stats.expectationsGenerated++;
|
|
80
|
+
if (expectation.proof === 'PROVEN_EXPECTATION') {
|
|
81
|
+
result.stats.expectationsProven++;
|
|
82
|
+
}
|
|
83
|
+
}
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
// Step 5b: Attribute-driven navigation expectations (href/to literal)
|
|
87
|
+
if ((effects?.length || 0) === 0) {
|
|
88
|
+
const attrs = mapping.element?.attrs || {};
|
|
89
|
+
const target = attrs.to || attrs.href || null;
|
|
90
|
+
if (target && typeof target === 'string' && target.startsWith('/')) {
|
|
91
|
+
const expectation = {
|
|
92
|
+
type: 'spa_navigation',
|
|
93
|
+
targetPath: target,
|
|
94
|
+
matchAttribute: attrs.to ? 'to' : (attrs.href ? 'href' : null),
|
|
95
|
+
proof: 'PROVEN_EXPECTATION',
|
|
96
|
+
sourceRef: mapping.element.sourceRef,
|
|
97
|
+
selectorHint: extractSelectorHint(mapping.element) || `${mapping.element.tag}`,
|
|
98
|
+
metadata: {
|
|
99
|
+
elementFile: mapping.element.file,
|
|
100
|
+
elementLine: mapping.element.line,
|
|
101
|
+
handlerName: mapping.handler.name,
|
|
102
|
+
handlerFile: mapping.handler.file,
|
|
103
|
+
handlerLine: mapping.handler.line,
|
|
104
|
+
eventType: mapping.element.event
|
|
105
|
+
}
|
|
106
|
+
};
|
|
107
|
+
result.expectations.push(expectation);
|
|
108
|
+
result.stats.expectationsGenerated++;
|
|
109
|
+
result.stats.expectationsProven++;
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
// Step 5c: Generate expectations from plain Link/NavLink/a elements with static href/to
|
|
115
|
+
const navElements = extractNavElements(projectRoot, program);
|
|
116
|
+
for (const el of navElements) {
|
|
117
|
+
const target = el.attrs.to || el.attrs.href || null;
|
|
118
|
+
if (!target) continue;
|
|
119
|
+
const expectation = {
|
|
120
|
+
type: 'spa_navigation',
|
|
121
|
+
targetPath: target,
|
|
122
|
+
matchAttribute: el.attrs.to ? 'to' : (el.attrs.href ? 'href' : null),
|
|
123
|
+
proof: 'PROVEN_EXPECTATION',
|
|
124
|
+
sourceRef: el.sourceRef,
|
|
125
|
+
selectorHint: extractSelectorHint(/** @type {any} */ ({ attributes: { properties: [] } })) || `${el.tag}`,
|
|
126
|
+
metadata: {
|
|
127
|
+
elementFile: el.file,
|
|
128
|
+
elementLine: el.line,
|
|
129
|
+
handlerName: null,
|
|
130
|
+
handlerFile: null,
|
|
131
|
+
handlerLine: null,
|
|
132
|
+
eventType: 'click'
|
|
133
|
+
}
|
|
134
|
+
};
|
|
135
|
+
result.expectations.push(expectation);
|
|
136
|
+
result.stats.expectationsGenerated++;
|
|
137
|
+
result.stats.expectationsProven++;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
// Step 5d: Extract Vue navigation promises (router-link, RouterLink, router.push/replace)
|
|
141
|
+
const vueNavPromises = await extractVueNavigationPromises(projectRoot, program);
|
|
142
|
+
for (const promise of vueNavPromises) {
|
|
143
|
+
result.expectations.push(promise);
|
|
144
|
+
result.stats.expectationsGenerated++;
|
|
145
|
+
if (promise.proof === 'PROVEN_EXPECTATION') {
|
|
146
|
+
result.stats.expectationsProven++;
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
} catch (err) {
|
|
151
|
+
result.error = err.message || 'Unknown error';
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
return result;
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Generate expectation from handler mapping and effect.
|
|
159
|
+
*
|
|
160
|
+
* @param {Object} mapping - Handler mapping
|
|
161
|
+
* @param {Object} effect - Detected effect
|
|
162
|
+
* @returns {Object|null} - Expectation object
|
|
163
|
+
*/
|
|
164
|
+
function generateExpectation(mapping, effect) {
|
|
165
|
+
if (!effect || !effect.type) return null;
|
|
166
|
+
|
|
167
|
+
const selectorHint = extractSelectorHint(mapping.element);
|
|
168
|
+
|
|
169
|
+
const base = {
|
|
170
|
+
selectorHint: selectorHint || `${mapping.element.tag}`,
|
|
171
|
+
proof: 'PROVEN_EXPECTATION',
|
|
172
|
+
sourceRef: effect.sourceRef,
|
|
173
|
+
metadata: {
|
|
174
|
+
elementFile: mapping.element.file,
|
|
175
|
+
elementLine: mapping.element.line,
|
|
176
|
+
handlerName: mapping.handler.name,
|
|
177
|
+
handlerFile: mapping.handler.file,
|
|
178
|
+
handlerLine: mapping.handler.line,
|
|
179
|
+
effectFile: effect.file,
|
|
180
|
+
effectLine: effect.line,
|
|
181
|
+
eventType: mapping.element.event
|
|
182
|
+
}
|
|
183
|
+
};
|
|
184
|
+
|
|
185
|
+
// Navigation → spa_navigation expectation with matchAttribute
|
|
186
|
+
if (effect.type === 'navigation' && effect.target) {
|
|
187
|
+
const matchAttribute = mapping.element?.attrs?.to
|
|
188
|
+
? 'to'
|
|
189
|
+
: (mapping.element?.attrs?.href ? 'href' : null);
|
|
190
|
+
// Extract navigation method (push/replace/navigate) from effect
|
|
191
|
+
const navMethod = effect.method || 'push'; // Default to push if not specified
|
|
192
|
+
|
|
193
|
+
// Normalize dynamic routes to example paths
|
|
194
|
+
const normalized = normalizeDynamicRoute(effect.target) || normalizeTemplateLiteral(effect.target);
|
|
195
|
+
|
|
196
|
+
if (normalized) {
|
|
197
|
+
return {
|
|
198
|
+
...base,
|
|
199
|
+
type: 'spa_navigation',
|
|
200
|
+
targetPath: normalized.examplePath,
|
|
201
|
+
originalPattern: normalized.originalPattern,
|
|
202
|
+
isDynamic: true,
|
|
203
|
+
exampleExecution: true,
|
|
204
|
+
matchAttribute,
|
|
205
|
+
expectedTarget: normalized.examplePath,
|
|
206
|
+
navigationMethod: navMethod
|
|
207
|
+
};
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return {
|
|
211
|
+
...base,
|
|
212
|
+
type: 'spa_navigation',
|
|
213
|
+
targetPath: effect.target,
|
|
214
|
+
matchAttribute,
|
|
215
|
+
expectedTarget: effect.target,
|
|
216
|
+
navigationMethod: navMethod
|
|
217
|
+
};
|
|
218
|
+
}
|
|
219
|
+
|
|
220
|
+
// Network → network_action expectation
|
|
221
|
+
if (effect.type === 'network') {
|
|
222
|
+
return {
|
|
223
|
+
...base,
|
|
224
|
+
type: 'network_action',
|
|
225
|
+
method: effect.method || 'GET',
|
|
226
|
+
expectedTarget: effect.target || null,
|
|
227
|
+
urlPath: effect.target || null // Alias for compatibility
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
// VALIDATION INTELLIGENCE v1: validation_block → validation_block expectation
|
|
232
|
+
if (effect.type === 'validation_block') {
|
|
233
|
+
return {
|
|
234
|
+
...base,
|
|
235
|
+
type: 'validation_block',
|
|
236
|
+
proof: 'PROVEN_EXPECTATION',
|
|
237
|
+
handlerRef: base.metadata.handlerFile ? `${base.metadata.handlerFile}:${base.metadata.handlerLine}` : null
|
|
238
|
+
};
|
|
239
|
+
}
|
|
240
|
+
|
|
241
|
+
// Validation → form submission expectation (legacy)
|
|
242
|
+
if (effect.type === 'validation') {
|
|
243
|
+
return {
|
|
244
|
+
...base,
|
|
245
|
+
type: 'form_submission',
|
|
246
|
+
expectedTarget: effect.target || null
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
|
|
250
|
+
// State → state_action expectation
|
|
251
|
+
if (effect.type === 'state' && effect.target) {
|
|
252
|
+
return {
|
|
253
|
+
...base,
|
|
254
|
+
type: 'state_action',
|
|
255
|
+
expectedTarget: effect.target,
|
|
256
|
+
storeType: effect.storeType || 'unknown',
|
|
257
|
+
method: effect.method || null
|
|
258
|
+
};
|
|
259
|
+
}
|
|
260
|
+
|
|
261
|
+
return null;
|
|
262
|
+
}
|
|
263
|
+
|
|
264
|
+
/**
|
|
265
|
+
* Determine expectation type from effect.
|
|
266
|
+
*
|
|
267
|
+
* @param {Object} effect - Effect object
|
|
268
|
+
* @returns {string} - Expectation type
|
|
269
|
+
*/
|
|
270
|
+
function _determineExpectationType(effect) {
|
|
271
|
+
switch (effect.type) {
|
|
272
|
+
case 'navigation':
|
|
273
|
+
return 'navigation';
|
|
274
|
+
case 'network':
|
|
275
|
+
return 'network_action';
|
|
276
|
+
case 'validation':
|
|
277
|
+
return 'form_submission'; // preventDefault typically in forms
|
|
278
|
+
default:
|
|
279
|
+
return 'unknown';
|
|
280
|
+
}
|
|
281
|
+
}
|