@veraxhq/verax 0.2.1 → 0.3.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +14 -18
- package/bin/verax.js +7 -0
- package/package.json +3 -3
- package/src/cli/commands/baseline.js +104 -0
- package/src/cli/commands/default.js +79 -25
- package/src/cli/commands/ga.js +243 -0
- package/src/cli/commands/gates.js +95 -0
- package/src/cli/commands/inspect.js +131 -2
- package/src/cli/commands/release-check.js +213 -0
- package/src/cli/commands/run.js +246 -35
- package/src/cli/commands/security-check.js +211 -0
- package/src/cli/commands/truth.js +114 -0
- package/src/cli/entry.js +304 -67
- package/src/cli/util/angular-component-extractor.js +179 -0
- package/src/cli/util/angular-navigation-detector.js +141 -0
- package/src/cli/util/angular-network-detector.js +161 -0
- package/src/cli/util/angular-state-detector.js +162 -0
- package/src/cli/util/ast-interactive-detector.js +546 -0
- package/src/cli/util/ast-network-detector.js +603 -0
- package/src/cli/util/ast-usestate-detector.js +602 -0
- package/src/cli/util/bootstrap-guard.js +86 -0
- package/src/cli/util/determinism-runner.js +123 -0
- package/src/cli/util/determinism-writer.js +129 -0
- package/src/cli/util/env-url.js +4 -0
- package/src/cli/util/expectation-extractor.js +369 -73
- package/src/cli/util/findings-writer.js +126 -16
- package/src/cli/util/learn-writer.js +3 -1
- package/src/cli/util/observe-writer.js +3 -1
- package/src/cli/util/paths.js +3 -12
- package/src/cli/util/project-discovery.js +3 -0
- package/src/cli/util/project-writer.js +3 -1
- package/src/cli/util/run-resolver.js +64 -0
- package/src/cli/util/source-requirement.js +55 -0
- package/src/cli/util/summary-writer.js +1 -0
- package/src/cli/util/svelte-navigation-detector.js +163 -0
- package/src/cli/util/svelte-network-detector.js +80 -0
- package/src/cli/util/svelte-sfc-extractor.js +147 -0
- package/src/cli/util/svelte-state-detector.js +243 -0
- package/src/cli/util/vue-navigation-detector.js +177 -0
- package/src/cli/util/vue-sfc-extractor.js +162 -0
- package/src/cli/util/vue-state-detector.js +215 -0
- package/src/verax/cli/finding-explainer.js +56 -3
- package/src/verax/core/artifacts/registry.js +154 -0
- package/src/verax/core/artifacts/verifier.js +980 -0
- package/src/verax/core/baseline/baseline.enforcer.js +137 -0
- package/src/verax/core/baseline/baseline.snapshot.js +231 -0
- package/src/verax/core/capabilities/gates.js +499 -0
- package/src/verax/core/capabilities/registry.js +475 -0
- package/src/verax/core/confidence/confidence-compute.js +137 -0
- package/src/verax/core/confidence/confidence-invariants.js +234 -0
- package/src/verax/core/confidence/confidence-report-writer.js +112 -0
- package/src/verax/core/confidence/confidence-weights.js +44 -0
- package/src/verax/core/confidence/confidence.defaults.js +65 -0
- package/src/verax/core/confidence/confidence.loader.js +79 -0
- package/src/verax/core/confidence/confidence.schema.js +94 -0
- package/src/verax/core/confidence-engine-refactor.js +484 -0
- package/src/verax/core/confidence-engine.js +486 -0
- package/src/verax/core/confidence-engine.js.backup +471 -0
- package/src/verax/core/contracts/index.js +29 -0
- package/src/verax/core/contracts/types.js +185 -0
- package/src/verax/core/contracts/validators.js +381 -0
- package/src/verax/core/decision-snapshot.js +30 -3
- package/src/verax/core/decisions/decision.trace.js +276 -0
- package/src/verax/core/determinism/contract-writer.js +89 -0
- package/src/verax/core/determinism/contract.js +139 -0
- package/src/verax/core/determinism/diff.js +364 -0
- package/src/verax/core/determinism/engine.js +221 -0
- package/src/verax/core/determinism/finding-identity.js +148 -0
- package/src/verax/core/determinism/normalize.js +438 -0
- package/src/verax/core/determinism/report-writer.js +92 -0
- package/src/verax/core/determinism/run-fingerprint.js +118 -0
- package/src/verax/core/dynamic-route-intelligence.js +528 -0
- package/src/verax/core/evidence/evidence-capture-service.js +307 -0
- package/src/verax/core/evidence/evidence-intent-ledger.js +165 -0
- package/src/verax/core/evidence-builder.js +487 -0
- package/src/verax/core/execution-mode-context.js +77 -0
- package/src/verax/core/execution-mode-detector.js +190 -0
- package/src/verax/core/failures/exit-codes.js +86 -0
- package/src/verax/core/failures/failure-summary.js +76 -0
- package/src/verax/core/failures/failure.factory.js +225 -0
- package/src/verax/core/failures/failure.ledger.js +132 -0
- package/src/verax/core/failures/failure.types.js +196 -0
- package/src/verax/core/failures/index.js +10 -0
- package/src/verax/core/ga/ga-report-writer.js +43 -0
- package/src/verax/core/ga/ga.artifact.js +49 -0
- package/src/verax/core/ga/ga.contract.js +434 -0
- package/src/verax/core/ga/ga.enforcer.js +86 -0
- package/src/verax/core/guardrails/guardrails-report-writer.js +109 -0
- package/src/verax/core/guardrails/policy.defaults.js +210 -0
- package/src/verax/core/guardrails/policy.loader.js +83 -0
- package/src/verax/core/guardrails/policy.schema.js +110 -0
- package/src/verax/core/guardrails/truth-reconciliation.js +136 -0
- package/src/verax/core/guardrails-engine.js +505 -0
- package/src/verax/core/observe/run-timeline.js +316 -0
- package/src/verax/core/perf/perf.contract.js +186 -0
- package/src/verax/core/perf/perf.display.js +65 -0
- package/src/verax/core/perf/perf.enforcer.js +91 -0
- package/src/verax/core/perf/perf.monitor.js +209 -0
- package/src/verax/core/perf/perf.report.js +198 -0
- package/src/verax/core/pipeline-tracker.js +238 -0
- package/src/verax/core/product-definition.js +127 -0
- package/src/verax/core/release/provenance.builder.js +271 -0
- package/src/verax/core/release/release-report-writer.js +40 -0
- package/src/verax/core/release/release.enforcer.js +159 -0
- package/src/verax/core/release/reproducibility.check.js +221 -0
- package/src/verax/core/release/sbom.builder.js +283 -0
- package/src/verax/core/report/cross-index.js +192 -0
- package/src/verax/core/report/human-summary.js +222 -0
- package/src/verax/core/route-intelligence.js +419 -0
- package/src/verax/core/security/secrets.scan.js +326 -0
- package/src/verax/core/security/security-report.js +50 -0
- package/src/verax/core/security/security.enforcer.js +124 -0
- package/src/verax/core/security/supplychain.defaults.json +38 -0
- package/src/verax/core/security/supplychain.policy.js +326 -0
- package/src/verax/core/security/vuln.scan.js +265 -0
- package/src/verax/core/truth/truth.certificate.js +250 -0
- package/src/verax/core/ui-feedback-intelligence.js +515 -0
- package/src/verax/detect/confidence-engine.js +628 -40
- package/src/verax/detect/confidence-helper.js +33 -0
- package/src/verax/detect/detection-engine.js +18 -1
- package/src/verax/detect/dynamic-route-findings.js +335 -0
- package/src/verax/detect/expectation-chain-detector.js +417 -0
- package/src/verax/detect/expectation-model.js +3 -1
- package/src/verax/detect/findings-writer.js +141 -5
- package/src/verax/detect/index.js +229 -5
- package/src/verax/detect/journey-stall-detector.js +558 -0
- package/src/verax/detect/route-findings.js +218 -0
- package/src/verax/detect/ui-feedback-findings.js +207 -0
- package/src/verax/detect/verdict-engine.js +57 -3
- package/src/verax/detect/view-switch-correlator.js +242 -0
- package/src/verax/index.js +413 -45
- package/src/verax/learn/action-contract-extractor.js +682 -64
- package/src/verax/learn/route-validator.js +4 -1
- package/src/verax/observe/index.js +88 -843
- package/src/verax/observe/interaction-runner.js +25 -8
- package/src/verax/observe/observe-context.js +205 -0
- package/src/verax/observe/observe-helpers.js +191 -0
- package/src/verax/observe/observe-runner.js +226 -0
- package/src/verax/observe/observers/budget-observer.js +185 -0
- package/src/verax/observe/observers/console-observer.js +102 -0
- package/src/verax/observe/observers/coverage-observer.js +107 -0
- package/src/verax/observe/observers/interaction-observer.js +471 -0
- package/src/verax/observe/observers/navigation-observer.js +132 -0
- package/src/verax/observe/observers/network-observer.js +87 -0
- package/src/verax/observe/observers/safety-observer.js +82 -0
- package/src/verax/observe/observers/ui-feedback-observer.js +99 -0
- package/src/verax/observe/ui-feedback-detector.js +742 -0
- package/src/verax/observe/ui-signal-sensor.js +148 -2
- package/src/verax/scan-summary-writer.js +42 -8
- package/src/verax/shared/artifact-manager.js +8 -5
- package/src/verax/shared/css-spinner-rules.js +204 -0
- package/src/verax/shared/view-switch-rules.js +208 -0
|
@@ -0,0 +1,546 @@
|
|
|
1
|
+
import { parse } from '@babel/parser';
|
|
2
|
+
import _traverse from '@babel/traverse';
|
|
3
|
+
|
|
4
|
+
// Handle default export from @babel/traverse (CommonJS/ESM compatibility)
|
|
5
|
+
const traverse = _traverse.default || _traverse;
|
|
6
|
+
|
|
7
|
+
/**
|
|
8
|
+
* PHASE 11 — Professional Interactive Element Detection (No href)
|
|
9
|
+
*
|
|
10
|
+
* AST-based detection of interactive elements without href:
|
|
11
|
+
* - Elements with onClick/onSubmit handlers
|
|
12
|
+
* - Elements with role="button" or role="link"
|
|
13
|
+
* - Router links (React Router <Link>, <NavLink>, Next.js <Link>)
|
|
14
|
+
* - Programmatic navigation calls (navigate(), history.push, router.push)
|
|
15
|
+
*
|
|
16
|
+
* Features:
|
|
17
|
+
* - AST source code extraction for evidence
|
|
18
|
+
* - Context tracking (component > handler)
|
|
19
|
+
* - Navigation promise extraction
|
|
20
|
+
* - False positive prevention
|
|
21
|
+
*/
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Detect interactive elements without href and their navigation promises
|
|
25
|
+
* @param {string} content - File content
|
|
26
|
+
* @param {string} filePath - Absolute file path
|
|
27
|
+
* @param {string} relPath - Relative path from source root
|
|
28
|
+
* @returns {Array} Array of interactive element detections with navigation promises
|
|
29
|
+
*/
|
|
30
|
+
export function detectInteractiveElementsAST(content, filePath, relPath) {
|
|
31
|
+
const detections = [];
|
|
32
|
+
const lines = content.split('\n');
|
|
33
|
+
|
|
34
|
+
try {
|
|
35
|
+
const ast = parse(content, {
|
|
36
|
+
sourceType: 'module',
|
|
37
|
+
plugins: [
|
|
38
|
+
'jsx',
|
|
39
|
+
'typescript',
|
|
40
|
+
'classProperties',
|
|
41
|
+
'optionalChaining',
|
|
42
|
+
'nullishCoalescingOperator',
|
|
43
|
+
'dynamicImport',
|
|
44
|
+
['decorators', { decoratorsBeforeExport: true }],
|
|
45
|
+
'topLevelAwait',
|
|
46
|
+
'objectRestSpread',
|
|
47
|
+
'asyncGenerators',
|
|
48
|
+
],
|
|
49
|
+
errorRecovery: true,
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
// Track router imports
|
|
53
|
+
const routerBindings = new Set();
|
|
54
|
+
const navigationBindings = new Set();
|
|
55
|
+
const historyBindings = new Set();
|
|
56
|
+
|
|
57
|
+
traverse(ast, {
|
|
58
|
+
// Track router/navigation imports
|
|
59
|
+
ImportDeclaration(path) {
|
|
60
|
+
const source = path.node.source.value;
|
|
61
|
+
|
|
62
|
+
// React Router
|
|
63
|
+
if (source === 'react-router-dom' || source === 'react-router') {
|
|
64
|
+
path.node.specifiers.forEach((spec) => {
|
|
65
|
+
if (spec.type === 'ImportSpecifier') {
|
|
66
|
+
if (spec.imported.name === 'useNavigate' || spec.imported.name === 'useHistory') {
|
|
67
|
+
navigationBindings.add(spec.local.name);
|
|
68
|
+
}
|
|
69
|
+
if (spec.imported.name === 'Link' || spec.imported.name === 'NavLink') {
|
|
70
|
+
routerBindings.add(spec.local.name);
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
});
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
// Next.js
|
|
77
|
+
if (source === 'next/link' || source === 'next/navigation') {
|
|
78
|
+
path.node.specifiers.forEach((spec) => {
|
|
79
|
+
if (spec.type === 'ImportSpecifier') {
|
|
80
|
+
if (spec.imported.name === 'Link') {
|
|
81
|
+
routerBindings.add(spec.local.name);
|
|
82
|
+
}
|
|
83
|
+
if (spec.imported.name === 'useRouter') {
|
|
84
|
+
navigationBindings.add(spec.local.name);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
});
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
// History API
|
|
91
|
+
if (source === 'history') {
|
|
92
|
+
path.node.specifiers.forEach((spec) => {
|
|
93
|
+
if (spec.type === 'ImportSpecifier' || spec.type === 'ImportDefaultSpecifier') {
|
|
94
|
+
historyBindings.add(spec.local.name);
|
|
95
|
+
}
|
|
96
|
+
});
|
|
97
|
+
}
|
|
98
|
+
},
|
|
99
|
+
|
|
100
|
+
// PHASE 11: Detect JSX elements with interactive handlers
|
|
101
|
+
JSXOpeningElement(path) {
|
|
102
|
+
const { node } = path;
|
|
103
|
+
const loc = node.loc;
|
|
104
|
+
|
|
105
|
+
// Extract tag name
|
|
106
|
+
let tagName = null;
|
|
107
|
+
if (node.name.type === 'JSXIdentifier') {
|
|
108
|
+
tagName = node.name.name;
|
|
109
|
+
} else if (node.name.type === 'JSXMemberExpression') {
|
|
110
|
+
tagName = `${node.name.object.name}.${node.name.property.name}`;
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
if (!tagName) return;
|
|
114
|
+
|
|
115
|
+
// Check for onClick/onSubmit handlers
|
|
116
|
+
let onClickHandler = null;
|
|
117
|
+
let onSubmitHandler = null;
|
|
118
|
+
let roleAttr = null;
|
|
119
|
+
let linkTo = null;
|
|
120
|
+
let linkHref = null;
|
|
121
|
+
|
|
122
|
+
for (const attr of node.attributes) {
|
|
123
|
+
if (attr.name?.name === 'onClick') {
|
|
124
|
+
onClickHandler = attr.value;
|
|
125
|
+
}
|
|
126
|
+
if (attr.name?.name === 'onSubmit') {
|
|
127
|
+
onSubmitHandler = attr.value;
|
|
128
|
+
}
|
|
129
|
+
if (attr.name?.name === 'role') {
|
|
130
|
+
roleAttr = attr.value?.value || (attr.value?.expression?.value);
|
|
131
|
+
}
|
|
132
|
+
// React Router Link - check if tagName is Link or NavLink
|
|
133
|
+
if (attr.name?.name === 'to' && (tagName === 'Link' || tagName === 'NavLink' || routerBindings.has(tagName))) {
|
|
134
|
+
linkTo = attr.value;
|
|
135
|
+
// Also add to routerBindings if not already there
|
|
136
|
+
if (!routerBindings.has(tagName)) {
|
|
137
|
+
routerBindings.add(tagName);
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
// Next.js Link
|
|
141
|
+
if (attr.name?.name === 'href' && (tagName === 'Link' || routerBindings.has(tagName))) {
|
|
142
|
+
linkHref = attr.value;
|
|
143
|
+
// Also add to routerBindings if not already there
|
|
144
|
+
if (!routerBindings.has(tagName)) {
|
|
145
|
+
routerBindings.add(tagName);
|
|
146
|
+
}
|
|
147
|
+
}
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// PHASE 11: Detect interactive elements
|
|
151
|
+
const isInteractive =
|
|
152
|
+
onClickHandler ||
|
|
153
|
+
onSubmitHandler ||
|
|
154
|
+
roleAttr === 'button' ||
|
|
155
|
+
roleAttr === 'link' ||
|
|
156
|
+
routerBindings.has(tagName) ||
|
|
157
|
+
tagName === 'Link' ||
|
|
158
|
+
tagName === 'NavLink' ||
|
|
159
|
+
tagName === 'button' ||
|
|
160
|
+
(tagName === 'a' && !linkHref && !linkTo);
|
|
161
|
+
|
|
162
|
+
if (isInteractive) {
|
|
163
|
+
// Extract navigation promise from handler
|
|
164
|
+
const handlerValue = onClickHandler || onSubmitHandler;
|
|
165
|
+
const navigationPromise = handlerValue ? extractNavigationPromise(
|
|
166
|
+
handlerValue,
|
|
167
|
+
path,
|
|
168
|
+
routerBindings,
|
|
169
|
+
navigationBindings,
|
|
170
|
+
historyBindings,
|
|
171
|
+
lines
|
|
172
|
+
) : null;
|
|
173
|
+
|
|
174
|
+
// Extract context
|
|
175
|
+
const context = inferContext(path);
|
|
176
|
+
const isUIBound = isUIBoundHandler(path);
|
|
177
|
+
|
|
178
|
+
// Extract AST source for handler
|
|
179
|
+
const handlerSource = onClickHandler || onSubmitHandler;
|
|
180
|
+
let handlerLoc = loc;
|
|
181
|
+
if (handlerSource) {
|
|
182
|
+
if (handlerSource.loc) {
|
|
183
|
+
handlerLoc = handlerSource.loc;
|
|
184
|
+
} else if (handlerSource.expression?.loc) {
|
|
185
|
+
handlerLoc = handlerSource.expression.loc;
|
|
186
|
+
} else if (handlerSource.type === 'JSXExpressionContainer' && handlerSource.expression?.loc) {
|
|
187
|
+
handlerLoc = handlerSource.expression.loc;
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
const astSource = handlerSource ? extractASTSource(handlerSource, lines, handlerLoc) : null;
|
|
191
|
+
|
|
192
|
+
// Extract selector hint
|
|
193
|
+
const selectorHint = extractSelectorHint(node, path);
|
|
194
|
+
|
|
195
|
+
detections.push({
|
|
196
|
+
tagName,
|
|
197
|
+
role: roleAttr,
|
|
198
|
+
hasOnClick: !!onClickHandler,
|
|
199
|
+
hasOnSubmit: !!onSubmitHandler,
|
|
200
|
+
isRouterLink: routerBindings.has(tagName) || tagName === 'Link' || tagName === 'NavLink',
|
|
201
|
+
linkTo: linkTo ? extractStringValue(linkTo) : null,
|
|
202
|
+
linkHref: linkHref ? extractStringValue(linkHref) : null,
|
|
203
|
+
navigationPromise,
|
|
204
|
+
context,
|
|
205
|
+
isUIBound,
|
|
206
|
+
astSource,
|
|
207
|
+
selectorHint,
|
|
208
|
+
location: {
|
|
209
|
+
line: loc?.start.line,
|
|
210
|
+
column: loc?.start.column,
|
|
211
|
+
},
|
|
212
|
+
});
|
|
213
|
+
}
|
|
214
|
+
},
|
|
215
|
+
});
|
|
216
|
+
|
|
217
|
+
} catch (error) {
|
|
218
|
+
// Parse errors are silently handled
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
return detections;
|
|
222
|
+
}
|
|
223
|
+
|
|
224
|
+
/**
|
|
225
|
+
* Extract navigation promise from handler (router.push, navigate, etc.)
|
|
226
|
+
*/
|
|
227
|
+
function extractNavigationPromise(handlerValue, path, routerBindings, navigationBindings, historyBindings, lines = []) {
|
|
228
|
+
if (!handlerValue) return null;
|
|
229
|
+
|
|
230
|
+
// If handler is a reference to a function, we'd need to follow it
|
|
231
|
+
// For now, we'll check inline handlers and common patterns
|
|
232
|
+
|
|
233
|
+
// Check if handler is an inline arrow function or function expression
|
|
234
|
+
if (handlerValue.type === 'JSXExpressionContainer') {
|
|
235
|
+
const expr = handlerValue.expression;
|
|
236
|
+
|
|
237
|
+
// Arrow function: onClick={() => router.push('/path')} or onClick={() => navigate('/path')}
|
|
238
|
+
if (expr.type === 'ArrowFunctionExpression' || expr.type === 'FunctionExpression') {
|
|
239
|
+
const body = expr.body;
|
|
240
|
+
|
|
241
|
+
// Check for navigation calls in body
|
|
242
|
+
// For arrow functions, body can be an expression or BlockStatement
|
|
243
|
+
return findNavigationCallInBody(body, routerBindings, navigationBindings, historyBindings, lines);
|
|
244
|
+
}
|
|
245
|
+
|
|
246
|
+
// Function reference: onClick={handleClick}
|
|
247
|
+
if (expr.type === 'Identifier') {
|
|
248
|
+
// Would need to follow reference - for now return null
|
|
249
|
+
return null;
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
return null;
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
/**
|
|
257
|
+
* Find navigation calls in function body
|
|
258
|
+
*/
|
|
259
|
+
function findNavigationCallInBody(body, routerBindings, navigationBindings, historyBindings, lines = []) {
|
|
260
|
+
const navigationCalls = [];
|
|
261
|
+
|
|
262
|
+
function walk(node) {
|
|
263
|
+
if (!node) return;
|
|
264
|
+
|
|
265
|
+
// CallExpression: router.push('/path'), navigate('/path'), etc.
|
|
266
|
+
if (node.type === 'CallExpression') {
|
|
267
|
+
const callee = node.callee;
|
|
268
|
+
|
|
269
|
+
// router.push('/path'), router.replace('/path')
|
|
270
|
+
if (callee.type === 'MemberExpression' &&
|
|
271
|
+
callee.object.type === 'Identifier' &&
|
|
272
|
+
routerBindings.has(callee.object.name)) {
|
|
273
|
+
const method = callee.property.name;
|
|
274
|
+
if (['push', 'replace', 'go'].includes(method)) {
|
|
275
|
+
const target = extractStringValue(node.arguments[0]);
|
|
276
|
+
if (target) {
|
|
277
|
+
navigationCalls.push({
|
|
278
|
+
type: 'router',
|
|
279
|
+
method,
|
|
280
|
+
target,
|
|
281
|
+
astSource: node.loc ? extractASTSource(node, lines, node.loc) : null,
|
|
282
|
+
});
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// navigate('/path')
|
|
288
|
+
if (callee.type === 'Identifier' && navigationBindings.has(callee.name)) {
|
|
289
|
+
const target = extractStringValue(node.arguments[0]);
|
|
290
|
+
if (target) {
|
|
291
|
+
navigationCalls.push({
|
|
292
|
+
type: 'navigate',
|
|
293
|
+
method: 'navigate',
|
|
294
|
+
target,
|
|
295
|
+
astSource: node.loc ? extractASTSource(node, lines, node.loc) : null,
|
|
296
|
+
});
|
|
297
|
+
}
|
|
298
|
+
}
|
|
299
|
+
|
|
300
|
+
// history.push('/path')
|
|
301
|
+
if (callee.type === 'MemberExpression' &&
|
|
302
|
+
callee.object.type === 'Identifier' &&
|
|
303
|
+
historyBindings.has(callee.object.name)) {
|
|
304
|
+
const method = callee.property.name;
|
|
305
|
+
if (['push', 'replace', 'go'].includes(method)) {
|
|
306
|
+
const target = extractStringValue(node.arguments[0]);
|
|
307
|
+
if (target) {
|
|
308
|
+
navigationCalls.push({
|
|
309
|
+
type: 'history',
|
|
310
|
+
method,
|
|
311
|
+
target,
|
|
312
|
+
astSource: node.loc ? extractASTSource(node, lines, node.loc) : null,
|
|
313
|
+
});
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
}
|
|
317
|
+
|
|
318
|
+
// window.location = '/path' or window.location.href = '/path'
|
|
319
|
+
if (callee.type === 'MemberExpression' &&
|
|
320
|
+
callee.object.type === 'MemberExpression' &&
|
|
321
|
+
callee.object.object.type === 'Identifier' &&
|
|
322
|
+
callee.object.object.name === 'window' &&
|
|
323
|
+
callee.object.property.name === 'location') {
|
|
324
|
+
const prop = callee.property.name;
|
|
325
|
+
if (prop === 'href' || prop === 'pathname') {
|
|
326
|
+
const target = extractStringValue(node.arguments[0]);
|
|
327
|
+
if (target) {
|
|
328
|
+
navigationCalls.push({
|
|
329
|
+
type: 'window_location',
|
|
330
|
+
method: prop,
|
|
331
|
+
target,
|
|
332
|
+
astSource: node.loc ? extractASTSource(node, lines, node.loc) : null,
|
|
333
|
+
});
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
// AssignmentExpression: window.location = '/path'
|
|
340
|
+
if (node.type === 'AssignmentExpression') {
|
|
341
|
+
if (node.left.type === 'MemberExpression' &&
|
|
342
|
+
node.left.object.type === 'MemberExpression' &&
|
|
343
|
+
node.left.object.object.type === 'Identifier' &&
|
|
344
|
+
node.left.object.object.name === 'window' &&
|
|
345
|
+
node.left.object.property.name === 'location') {
|
|
346
|
+
const prop = node.left.property.name;
|
|
347
|
+
if (prop === 'href' || prop === 'pathname') {
|
|
348
|
+
const target = extractStringValue(node.right);
|
|
349
|
+
if (target) {
|
|
350
|
+
navigationCalls.push({
|
|
351
|
+
type: 'window_location',
|
|
352
|
+
method: 'assign',
|
|
353
|
+
target,
|
|
354
|
+
astSource: node.loc ? extractASTSource(node, lines, node.loc) : null,
|
|
355
|
+
});
|
|
356
|
+
}
|
|
357
|
+
}
|
|
358
|
+
}
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
// Recursively walk children
|
|
362
|
+
if (node.body && Array.isArray(node.body)) {
|
|
363
|
+
node.body.forEach(walk);
|
|
364
|
+
} else if (node.body) {
|
|
365
|
+
walk(node.body);
|
|
366
|
+
}
|
|
367
|
+
if (node.expression) {
|
|
368
|
+
walk(node.expression);
|
|
369
|
+
}
|
|
370
|
+
if (node.consequent) {
|
|
371
|
+
walk(node.consequent);
|
|
372
|
+
}
|
|
373
|
+
if (node.alternate) {
|
|
374
|
+
walk(node.alternate);
|
|
375
|
+
}
|
|
376
|
+
}
|
|
377
|
+
|
|
378
|
+
// Handle both BlockStatement and Expression bodies
|
|
379
|
+
if (body.type === 'BlockStatement' && body.body) {
|
|
380
|
+
body.body.forEach(walk);
|
|
381
|
+
} else {
|
|
382
|
+
// Arrow function with expression body: () => navigate('/path')
|
|
383
|
+
walk(body);
|
|
384
|
+
}
|
|
385
|
+
|
|
386
|
+
return navigationCalls.length > 0 ? navigationCalls[0] : null;
|
|
387
|
+
}
|
|
388
|
+
|
|
389
|
+
/**
|
|
390
|
+
* Extract string value from AST node
|
|
391
|
+
*/
|
|
392
|
+
function extractStringValue(node) {
|
|
393
|
+
if (!node) return null;
|
|
394
|
+
|
|
395
|
+
if (node.type === 'StringLiteral') {
|
|
396
|
+
return node.value;
|
|
397
|
+
}
|
|
398
|
+
|
|
399
|
+
if (node.type === 'TemplateLiteral' && node.expressions.length === 0) {
|
|
400
|
+
return node.quasis[0].value.cooked;
|
|
401
|
+
}
|
|
402
|
+
|
|
403
|
+
// Template literal with expressions: `/path/${id}` -> '<dynamic>'
|
|
404
|
+
if (node.type === 'TemplateLiteral' && node.expressions.length > 0) {
|
|
405
|
+
return '<dynamic>';
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
return null;
|
|
409
|
+
}
|
|
410
|
+
|
|
411
|
+
/**
|
|
412
|
+
* Extract selector hint from JSX element
|
|
413
|
+
*/
|
|
414
|
+
function extractSelectorHint(node, path) {
|
|
415
|
+
let selector = node.name.name || '';
|
|
416
|
+
|
|
417
|
+
// Check for id attribute
|
|
418
|
+
for (const attr of node.attributes) {
|
|
419
|
+
if (attr.name?.name === 'id' && attr.value?.value) {
|
|
420
|
+
return `#${attr.value.value}`;
|
|
421
|
+
}
|
|
422
|
+
if (attr.name?.name === 'data-testid' && attr.value?.value) {
|
|
423
|
+
return `[data-testid="${attr.value.value}"]`;
|
|
424
|
+
}
|
|
425
|
+
}
|
|
426
|
+
|
|
427
|
+
return selector;
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
/**
|
|
431
|
+
* Infer execution context (handler, hook, component, etc.)
|
|
432
|
+
* Reuses Phase 9/10 style context tracking
|
|
433
|
+
*/
|
|
434
|
+
function inferContext(path) {
|
|
435
|
+
const contexts = [];
|
|
436
|
+
|
|
437
|
+
let current = path.parentPath;
|
|
438
|
+
while (current) {
|
|
439
|
+
const node = current.node;
|
|
440
|
+
|
|
441
|
+
// Event handler prop: onClick={() => ...}
|
|
442
|
+
if (current.isJSXAttribute()) {
|
|
443
|
+
const attrName = node.name.name;
|
|
444
|
+
if (attrName && attrName.startsWith('on')) {
|
|
445
|
+
contexts.push(`handler:${attrName}`);
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
// Hook: useEffect(() => ...), useCallback(() => ...)
|
|
450
|
+
if (current.isCallExpression() &&
|
|
451
|
+
current.node.callee.type === 'Identifier') {
|
|
452
|
+
const calleeName = current.node.callee.name;
|
|
453
|
+
if (calleeName.startsWith('use')) {
|
|
454
|
+
contexts.push(`hook:${calleeName}`);
|
|
455
|
+
}
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
// Function/Arrow in component
|
|
459
|
+
if (current.isFunctionDeclaration() ||
|
|
460
|
+
current.isFunctionExpression() ||
|
|
461
|
+
current.isArrowFunctionExpression()) {
|
|
462
|
+
const funcName = getFunctionName(current);
|
|
463
|
+
if (funcName) {
|
|
464
|
+
if (funcName.startsWith('handle') || funcName.startsWith('on')) {
|
|
465
|
+
contexts.push(`handler:${funcName}`);
|
|
466
|
+
} else {
|
|
467
|
+
contexts.push(`function:${funcName}`);
|
|
468
|
+
}
|
|
469
|
+
}
|
|
470
|
+
}
|
|
471
|
+
|
|
472
|
+
current = current.parentPath;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
return contexts.length > 0 ? contexts.reverse().join(' > ') : 'top-level';
|
|
476
|
+
}
|
|
477
|
+
|
|
478
|
+
/**
|
|
479
|
+
* Get function name from path
|
|
480
|
+
*/
|
|
481
|
+
function getFunctionName(path) {
|
|
482
|
+
const node = path.node;
|
|
483
|
+
|
|
484
|
+
if (node.id?.name) {
|
|
485
|
+
return node.id.name;
|
|
486
|
+
}
|
|
487
|
+
|
|
488
|
+
const parent = path.parent;
|
|
489
|
+
if (parent.type === 'VariableDeclarator' && parent.id.name) {
|
|
490
|
+
return parent.id.name;
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
if (parent.type === 'ObjectProperty' && parent.key.name) {
|
|
494
|
+
return parent.key.name;
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
return null;
|
|
498
|
+
}
|
|
499
|
+
|
|
500
|
+
/**
|
|
501
|
+
* Determine if handler is UI-bound (connected to user interaction)
|
|
502
|
+
*/
|
|
503
|
+
function isUIBoundHandler(path) {
|
|
504
|
+
const context = inferContext(path);
|
|
505
|
+
|
|
506
|
+
if (context.includes('handler:on')) {
|
|
507
|
+
return true;
|
|
508
|
+
}
|
|
509
|
+
|
|
510
|
+
if (context.includes('handler:handle')) {
|
|
511
|
+
return true;
|
|
512
|
+
}
|
|
513
|
+
|
|
514
|
+
return false;
|
|
515
|
+
}
|
|
516
|
+
|
|
517
|
+
/**
|
|
518
|
+
* Extract AST source code snippet for evidence
|
|
519
|
+
*/
|
|
520
|
+
function extractASTSource(node, lines, loc) {
|
|
521
|
+
if (!loc || !loc.start || !loc.end) {
|
|
522
|
+
return '';
|
|
523
|
+
}
|
|
524
|
+
|
|
525
|
+
const startLine = loc.start.line - 1;
|
|
526
|
+
const endLine = loc.end.line - 1;
|
|
527
|
+
|
|
528
|
+
if (startLine < 0 || endLine >= lines.length) {
|
|
529
|
+
return '';
|
|
530
|
+
}
|
|
531
|
+
|
|
532
|
+
if (startLine === endLine) {
|
|
533
|
+
const line = lines[startLine];
|
|
534
|
+
const startCol = loc.start.column;
|
|
535
|
+
const endCol = loc.end.column;
|
|
536
|
+
return line.substring(startCol, endCol).trim();
|
|
537
|
+
} else {
|
|
538
|
+
const snippet = lines.slice(startLine, endLine + 1);
|
|
539
|
+
if (snippet.length > 0) {
|
|
540
|
+
snippet[0] = snippet[0].substring(loc.start.column);
|
|
541
|
+
snippet[snippet.length - 1] = snippet[snippet.length - 1].substring(0, loc.end.column);
|
|
542
|
+
}
|
|
543
|
+
return snippet.join('\n').trim();
|
|
544
|
+
}
|
|
545
|
+
}
|
|
546
|
+
|