@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
package/src/verax/index.js
CHANGED
|
@@ -3,12 +3,14 @@ import { observe } from './observe/index.js';
|
|
|
3
3
|
import { detect } from './detect/index.js';
|
|
4
4
|
import { writeScanSummary } from './scan-summary-writer.js';
|
|
5
5
|
import { validateRoutes } from './learn/route-validator.js';
|
|
6
|
-
import {
|
|
6
|
+
import { createScanBudgetWithProfile } from './shared/budget-profiles.js';
|
|
7
|
+
import { computeObservationSummary, writeEvidenceIndex } from './detect/verdict-engine.js';
|
|
8
|
+
import SilenceTracker from './core/silence-model.js';
|
|
9
|
+
import { generateRunId, getRunArtifactDir, getArtifactPath } from './core/run-id.js';
|
|
10
|
+
import { createRunManifest, updateRunManifestHashes } from './core/run-manifest.js';
|
|
11
|
+
import { computeArtifactHashes } from './core/run-id.js';
|
|
7
12
|
|
|
8
|
-
export async function scan(projectDir, url, manifestPath = null,
|
|
9
|
-
// Generate runId for this scan if not provided
|
|
10
|
-
const scanRunId = runId || generateRunId();
|
|
11
|
-
const artifactPaths = initArtifactPaths(projectDir, scanRunId);
|
|
13
|
+
export async function scan(projectDir, url, manifestPath = null, scanBudgetOverride = null, safetyFlags = {}) {
|
|
12
14
|
// If manifestPath is provided, read it first before learn() overwrites it
|
|
13
15
|
let loadedManifest = null;
|
|
14
16
|
if (manifestPath) {
|
|
@@ -32,6 +34,7 @@ export async function scan(projectDir, url, manifestPath = null, runId = null) {
|
|
|
32
34
|
publicRoutes: loadedManifest.publicRoutes || learnedManifest.publicRoutes,
|
|
33
35
|
routes: loadedManifest.routes || learnedManifest.routes,
|
|
34
36
|
internalRoutes: loadedManifest.internalRoutes || learnedManifest.internalRoutes,
|
|
37
|
+
staticExpectations: loadedManifest.staticExpectations || learnedManifest.staticExpectations,
|
|
35
38
|
manifestPath: manifestPath
|
|
36
39
|
};
|
|
37
40
|
} else {
|
|
@@ -50,8 +53,18 @@ export async function scan(projectDir, url, manifestPath = null, runId = null) {
|
|
|
50
53
|
manifestPath: manifest.manifestPath
|
|
51
54
|
};
|
|
52
55
|
|
|
53
|
-
|
|
56
|
+
let validation = await validateRoutes(manifestForValidation, url);
|
|
54
57
|
|
|
58
|
+
if (!validation) {
|
|
59
|
+
// validateRoutes might return null if routes cannot be validated
|
|
60
|
+
validation = {
|
|
61
|
+
routesValidated: 0,
|
|
62
|
+
routesReachable: 0,
|
|
63
|
+
routesUnreachable: 0,
|
|
64
|
+
details: [],
|
|
65
|
+
warnings: []
|
|
66
|
+
};
|
|
67
|
+
}
|
|
55
68
|
if (validation.warnings && validation.warnings.length > 0) {
|
|
56
69
|
if (!manifest.learnTruth.warnings) {
|
|
57
70
|
manifest.learnTruth.warnings = [];
|
|
@@ -59,22 +72,56 @@ export async function scan(projectDir, url, manifestPath = null, runId = null) {
|
|
|
59
72
|
manifest.learnTruth.warnings.push(...validation.warnings);
|
|
60
73
|
}
|
|
61
74
|
|
|
75
|
+
// Use budget profile if no override provided
|
|
76
|
+
const scanBudget = scanBudgetOverride || createScanBudgetWithProfile();
|
|
77
|
+
|
|
78
|
+
// PHASE 5: Generate deterministic runId and create run manifest
|
|
79
|
+
const { getBaseOrigin } = await import('./observe/domain-boundary.js');
|
|
80
|
+
const baseOrigin = getBaseOrigin(url);
|
|
81
|
+
const runId = generateRunId({
|
|
82
|
+
url,
|
|
83
|
+
safetyFlags,
|
|
84
|
+
baseOrigin,
|
|
85
|
+
scanBudget,
|
|
86
|
+
manifestPath
|
|
87
|
+
});
|
|
88
|
+
|
|
89
|
+
// Create run manifest at start of execution
|
|
90
|
+
const runManifest = createRunManifest(projectDir, runId, {
|
|
91
|
+
url,
|
|
92
|
+
safetyFlags,
|
|
93
|
+
baseOrigin,
|
|
94
|
+
scanBudget,
|
|
95
|
+
manifestPath,
|
|
96
|
+
argv: process.argv
|
|
97
|
+
});
|
|
98
|
+
|
|
62
99
|
const usedManifestPath = manifestPath || manifest.manifestPath;
|
|
63
|
-
const observation = await observe(url, usedManifestPath,
|
|
100
|
+
const observation = await observe(url, usedManifestPath, scanBudget, safetyFlags, projectDir, runId);
|
|
101
|
+
|
|
102
|
+
// Write a copy of the manifest into canonical run directory for replay integrity
|
|
103
|
+
try {
|
|
104
|
+
const { writeFileSync } = await import('fs');
|
|
105
|
+
const manifestCopyPath = getArtifactPath(projectDir, runId, 'manifest.json');
|
|
106
|
+
writeFileSync(manifestCopyPath, JSON.stringify(manifest, null, 2));
|
|
107
|
+
} catch {
|
|
108
|
+
// Ignore write errors
|
|
109
|
+
}
|
|
64
110
|
|
|
65
|
-
//
|
|
66
|
-
|
|
67
|
-
if (
|
|
68
|
-
|
|
111
|
+
// Create silence tracker from observation silences
|
|
112
|
+
const silenceTracker = new SilenceTracker();
|
|
113
|
+
if (observation.silences && observation.silences.entries) {
|
|
114
|
+
silenceTracker.recordBatch(observation.silences.entries);
|
|
69
115
|
}
|
|
70
116
|
|
|
71
|
-
const findings = await detect(usedManifestPath,
|
|
117
|
+
const findings = await detect(usedManifestPath, observation.tracesPath, validation, observation.expectationCoverageGaps || [], silenceTracker);
|
|
72
118
|
|
|
73
119
|
const learnTruthWithValidation = {
|
|
74
120
|
...manifest.learnTruth,
|
|
75
121
|
validation: validation
|
|
76
122
|
};
|
|
77
123
|
|
|
124
|
+
const runDir = getRunArtifactDir(projectDir, runId);
|
|
78
125
|
const scanSummary = writeScanSummary(
|
|
79
126
|
projectDir,
|
|
80
127
|
url,
|
|
@@ -85,13 +132,54 @@ export async function scan(projectDir, url, manifestPath = null, runId = null) {
|
|
|
85
132
|
manifest.manifestPath,
|
|
86
133
|
observation.tracesPath,
|
|
87
134
|
findings.findingsPath,
|
|
88
|
-
|
|
135
|
+
runDir,
|
|
136
|
+
findings.findings // PHASE 7: Pass findings array for decision snapshot
|
|
137
|
+
);
|
|
138
|
+
|
|
139
|
+
// Compute observation summary from scan results (not a verdict)
|
|
140
|
+
// Pass observation object (which includes traces) to observation engine
|
|
141
|
+
const observeTruthWithTraces = {
|
|
142
|
+
...observation.observeTruth,
|
|
143
|
+
traces: observation.traces || []
|
|
144
|
+
};
|
|
145
|
+
const observationSummary = computeObservationSummary(
|
|
146
|
+
findings.findings || [],
|
|
147
|
+
observeTruthWithTraces,
|
|
148
|
+
manifest.learnTruth,
|
|
149
|
+
findings.coverageGaps || [],
|
|
150
|
+
observation.observeTruth?.budgetExceeded,
|
|
151
|
+
findings.detectTruth, // Pass detectTruth for silence data
|
|
152
|
+
projectDir, // Pass projectDir for evidence validation
|
|
153
|
+
silenceTracker // Pass silenceTracker for evidence integrity tracking
|
|
154
|
+
);
|
|
155
|
+
|
|
156
|
+
// Write evidence index
|
|
157
|
+
const evidenceIndexPath = await writeEvidenceIndex(
|
|
158
|
+
projectDir,
|
|
159
|
+
observationSummary.evidenceIndex || [],
|
|
160
|
+
observation.tracesPath,
|
|
161
|
+
findings.findingsPath,
|
|
162
|
+
runDir
|
|
89
163
|
);
|
|
164
|
+
observationSummary.evidenceIndexPath = evidenceIndexPath;
|
|
90
165
|
|
|
91
|
-
|
|
166
|
+
// PHASE 5: Compute artifact hashes and update run manifest
|
|
167
|
+
const artifactHashes = computeArtifactHashes(projectDir, runId);
|
|
168
|
+
updateRunManifestHashes(projectDir, runId, artifactHashes);
|
|
169
|
+
|
|
170
|
+
return {
|
|
171
|
+
manifest,
|
|
172
|
+
observation,
|
|
173
|
+
findings,
|
|
174
|
+
scanSummary,
|
|
175
|
+
validation,
|
|
176
|
+
coverageGaps: findings.coverageGaps || [],
|
|
177
|
+
observationSummary,
|
|
178
|
+
runId,
|
|
179
|
+
runManifest
|
|
180
|
+
};
|
|
92
181
|
}
|
|
93
182
|
|
|
94
183
|
export { learn } from './learn/index.js';
|
|
95
184
|
export { observe } from './observe/index.js';
|
|
96
185
|
export { detect } from './detect/index.js';
|
|
97
|
-
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* CODE INTELLIGENCE v1 — Effect Detector
|
|
3
|
+
*
|
|
4
|
+
* Detects DIRECT effects in handler function bodies:
|
|
5
|
+
* - Navigation: navigate(), router.push(), router.replace()
|
|
6
|
+
* - Network: fetch(), axios.get/post/put/delete(), XMLHttpRequest
|
|
7
|
+
* - Validation: preventDefault() + return false, throw, setError
|
|
8
|
+
*
|
|
9
|
+
* String literals ONLY. No template literals in v1.
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
import ts from 'typescript';
|
|
13
|
+
import { getFunctionBody, getStringLiteral, findNodes, getNodeLocation } from './ts-program.js';
|
|
14
|
+
import { normalizeTemplateLiteral } from '../shared/dynamic-route-utils.js';
|
|
15
|
+
|
|
16
|
+
/**
|
|
17
|
+
* Detect effects in handler function.
|
|
18
|
+
*
|
|
19
|
+
* @param {ts.Node} handlerNode - Function node
|
|
20
|
+
* @param {ts.SourceFile} sourceFile - Source file
|
|
21
|
+
* @param {string} projectRoot - Project root
|
|
22
|
+
* @param {string} eventType - Event type (onSubmit, onClick, etc.) - VALIDATION INTELLIGENCE v1
|
|
23
|
+
* @returns {Array} - Array of effect objects
|
|
24
|
+
*/
|
|
25
|
+
export function detectEffects(handlerNode, sourceFile, projectRoot, eventType = null) {
|
|
26
|
+
const effects = [];
|
|
27
|
+
const statements = getFunctionBody(handlerNode);
|
|
28
|
+
|
|
29
|
+
if (!statements) {
|
|
30
|
+
// Arrow function with expression body
|
|
31
|
+
if (ts.isArrowFunction(handlerNode) && ts.isExpression(handlerNode.body)) {
|
|
32
|
+
const effect = analyzeExpression(handlerNode.body, sourceFile, projectRoot, eventType);
|
|
33
|
+
if (effect) effects.push(effect);
|
|
34
|
+
}
|
|
35
|
+
return effects;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
// Walk all statements and expressions
|
|
39
|
+
for (const statement of statements) {
|
|
40
|
+
findNodes(statement, node => {
|
|
41
|
+
const effect = analyzeNode(node, sourceFile, projectRoot, eventType);
|
|
42
|
+
if (effect) effects.push(effect);
|
|
43
|
+
return false; // Continue walking
|
|
44
|
+
});
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
return effects;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Analyze node for effects.
|
|
52
|
+
*
|
|
53
|
+
* @param {ts.Node} node - AST node
|
|
54
|
+
* @param {ts.SourceFile} sourceFile - Source file
|
|
55
|
+
* @param {string} projectRoot - Project root
|
|
56
|
+
* @param {string} eventType - Event type (onSubmit, onClick, etc.) - VALIDATION INTELLIGENCE v1
|
|
57
|
+
* @returns {Object|null} - Effect object or null
|
|
58
|
+
*/
|
|
59
|
+
function analyzeNode(node, sourceFile, projectRoot, eventType = null) {
|
|
60
|
+
// Call expressions
|
|
61
|
+
if (ts.isCallExpression(node)) {
|
|
62
|
+
return analyzeCallExpression(node, sourceFile, projectRoot, eventType);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
// VALIDATION INTELLIGENCE v1: Check for return false in submit context
|
|
66
|
+
if (ts.isReturnStatement(node) && eventType === 'onSubmit') {
|
|
67
|
+
const expression = node.expression;
|
|
68
|
+
if (expression && expression.kind === ts.SyntaxKind.FalseKeyword) {
|
|
69
|
+
const location = getNodeLocation(sourceFile, node, projectRoot);
|
|
70
|
+
return {
|
|
71
|
+
type: 'validation_block',
|
|
72
|
+
method: 'return_false',
|
|
73
|
+
target: null,
|
|
74
|
+
sourceRef: location.sourceRef,
|
|
75
|
+
file: location.file,
|
|
76
|
+
line: location.line
|
|
77
|
+
};
|
|
78
|
+
}
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return null;
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
/**
|
|
85
|
+
* Analyze expression for effects.
|
|
86
|
+
*
|
|
87
|
+
* @param {ts.Expression} expr - Expression node
|
|
88
|
+
* @param {ts.SourceFile} sourceFile - Source file
|
|
89
|
+
* @param {string} projectRoot - Project root
|
|
90
|
+
* @param {string} eventType - Event type (onSubmit, onClick, etc.) - VALIDATION INTELLIGENCE v1
|
|
91
|
+
* @returns {Object|null} - Effect object or null
|
|
92
|
+
*/
|
|
93
|
+
function analyzeExpression(expr, sourceFile, projectRoot, eventType = null) {
|
|
94
|
+
if (ts.isCallExpression(expr)) {
|
|
95
|
+
return analyzeCallExpression(expr, sourceFile, projectRoot, eventType);
|
|
96
|
+
}
|
|
97
|
+
return null;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Analyze call expression for effects.
|
|
102
|
+
*
|
|
103
|
+
* @param {ts.CallExpression} call - Call expression
|
|
104
|
+
* @param {ts.SourceFile} sourceFile - Source file
|
|
105
|
+
* @param {string} projectRoot - Project root
|
|
106
|
+
* @param {string} eventType - Event type (onSubmit, onClick, etc.) - VALIDATION INTELLIGENCE v1
|
|
107
|
+
* @returns {Object|null} - Effect object or null
|
|
108
|
+
*/
|
|
109
|
+
function analyzeCallExpression(call, sourceFile, projectRoot, eventType = null) {
|
|
110
|
+
const expr = call.expression;
|
|
111
|
+
|
|
112
|
+
// Navigation: navigate("/path")
|
|
113
|
+
if (ts.isIdentifier(expr) && expr.text === 'navigate') {
|
|
114
|
+
const target = getFirstStringArg(call);
|
|
115
|
+
if (target) {
|
|
116
|
+
const location = getNodeLocation(sourceFile, call, projectRoot);
|
|
117
|
+
return {
|
|
118
|
+
type: 'navigation',
|
|
119
|
+
method: 'navigate',
|
|
120
|
+
target,
|
|
121
|
+
sourceRef: location.sourceRef,
|
|
122
|
+
file: location.file,
|
|
123
|
+
line: location.line
|
|
124
|
+
};
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
// Router: router.push("/path")
|
|
129
|
+
if (ts.isPropertyAccessExpression(expr)) {
|
|
130
|
+
const obj = expr.expression;
|
|
131
|
+
const prop = expr.name;
|
|
132
|
+
|
|
133
|
+
if (ts.isIdentifier(obj) && ts.isIdentifier(prop)) {
|
|
134
|
+
const objName = obj.text;
|
|
135
|
+
const propName = prop.text;
|
|
136
|
+
|
|
137
|
+
// router.push/replace/navigate
|
|
138
|
+
if ((objName === 'router' || objName === 'history') &&
|
|
139
|
+
['push', 'replace', 'navigate'].includes(propName)) {
|
|
140
|
+
const target = getFirstStringArg(call);
|
|
141
|
+
if (target) {
|
|
142
|
+
const location = getNodeLocation(sourceFile, call, projectRoot);
|
|
143
|
+
return {
|
|
144
|
+
type: 'navigation',
|
|
145
|
+
method: `${objName}.${propName}`,
|
|
146
|
+
target,
|
|
147
|
+
sourceRef: location.sourceRef,
|
|
148
|
+
file: location.file,
|
|
149
|
+
line: location.line
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// axios.get/post/put/delete("/api/...")
|
|
155
|
+
if (objName === 'axios' && ['get', 'post', 'put', 'delete', 'patch'].includes(propName)) {
|
|
156
|
+
const target = getFirstStringArg(call);
|
|
157
|
+
if (target) {
|
|
158
|
+
const location = getNodeLocation(sourceFile, call, projectRoot);
|
|
159
|
+
return {
|
|
160
|
+
type: 'network',
|
|
161
|
+
method: propName.toUpperCase(),
|
|
162
|
+
target,
|
|
163
|
+
sourceRef: location.sourceRef,
|
|
164
|
+
file: location.file,
|
|
165
|
+
line: location.line
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
}
|
|
169
|
+
}
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// fetch("/api/...")
|
|
173
|
+
if (ts.isIdentifier(expr) && expr.text === 'fetch') {
|
|
174
|
+
const target = getFirstStringArg(call);
|
|
175
|
+
if (target) {
|
|
176
|
+
// Check for method in options
|
|
177
|
+
let method = 'GET';
|
|
178
|
+
if (call.arguments.length > 1) {
|
|
179
|
+
const options = call.arguments[1];
|
|
180
|
+
if (ts.isObjectLiteralExpression(options)) {
|
|
181
|
+
for (const prop of options.properties) {
|
|
182
|
+
if (ts.isPropertyAssignment(prop)) {
|
|
183
|
+
const name = prop.name;
|
|
184
|
+
if (ts.isIdentifier(name) && name.text === 'method') {
|
|
185
|
+
const value = getStringLiteral(prop.initializer);
|
|
186
|
+
if (value) method = value.toUpperCase();
|
|
187
|
+
}
|
|
188
|
+
}
|
|
189
|
+
}
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const location = getNodeLocation(sourceFile, call, projectRoot);
|
|
194
|
+
return {
|
|
195
|
+
type: 'network',
|
|
196
|
+
method,
|
|
197
|
+
target,
|
|
198
|
+
sourceRef: location.sourceRef,
|
|
199
|
+
file: location.file,
|
|
200
|
+
line: location.line
|
|
201
|
+
};
|
|
202
|
+
}
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
// preventDefault() - VALIDATION INTELLIGENCE v1: validation_block in onSubmit context
|
|
206
|
+
if (ts.isPropertyAccessExpression(expr)) {
|
|
207
|
+
const prop = expr.name;
|
|
208
|
+
if (ts.isIdentifier(prop) && prop.text === 'preventDefault') {
|
|
209
|
+
const location = getNodeLocation(sourceFile, call, projectRoot);
|
|
210
|
+
// If in onSubmit context, emit validation_block; otherwise legacy validation type
|
|
211
|
+
if (eventType === 'onSubmit') {
|
|
212
|
+
return {
|
|
213
|
+
type: 'validation_block',
|
|
214
|
+
method: 'preventDefault',
|
|
215
|
+
target: null,
|
|
216
|
+
sourceRef: location.sourceRef,
|
|
217
|
+
file: location.file,
|
|
218
|
+
line: location.line
|
|
219
|
+
};
|
|
220
|
+
} else {
|
|
221
|
+
return {
|
|
222
|
+
type: 'validation',
|
|
223
|
+
method: 'preventDefault',
|
|
224
|
+
target: null,
|
|
225
|
+
sourceRef: location.sourceRef,
|
|
226
|
+
file: location.file,
|
|
227
|
+
line: location.line
|
|
228
|
+
};
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
// xhr.open('POST', '/api/foo')
|
|
233
|
+
if (ts.isIdentifier(prop) && prop.text === 'open') {
|
|
234
|
+
const args = call.arguments;
|
|
235
|
+
if (args.length >= 2) {
|
|
236
|
+
const methodLiteral = getStringLiteral(args[0]);
|
|
237
|
+
const urlLiteral = getStringLiteral(args[1]);
|
|
238
|
+
if (methodLiteral && urlLiteral) {
|
|
239
|
+
const location = getNodeLocation(sourceFile, call, projectRoot);
|
|
240
|
+
return {
|
|
241
|
+
type: 'network',
|
|
242
|
+
method: methodLiteral.toUpperCase(),
|
|
243
|
+
target: urlLiteral,
|
|
244
|
+
sourceRef: location.sourceRef,
|
|
245
|
+
file: location.file,
|
|
246
|
+
line: location.line
|
|
247
|
+
};
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
}
|
|
251
|
+
}
|
|
252
|
+
|
|
253
|
+
// STATE INTELLIGENCE: Redux dispatch(action())
|
|
254
|
+
if (ts.isIdentifier(expr) && expr.text === 'dispatch') {
|
|
255
|
+
const firstArg = call.arguments[0];
|
|
256
|
+
if (firstArg && ts.isCallExpression(firstArg)) {
|
|
257
|
+
// dispatch(increment()) - action creator call
|
|
258
|
+
let actionName = null;
|
|
259
|
+
|
|
260
|
+
// Simple action creator: increment()
|
|
261
|
+
if (ts.isIdentifier(firstArg.expression)) {
|
|
262
|
+
actionName = firstArg.expression.text;
|
|
263
|
+
}
|
|
264
|
+
// Slice action: counterSlice.actions.increment()
|
|
265
|
+
else if (ts.isPropertyAccessExpression(firstArg.expression)) {
|
|
266
|
+
const prop = firstArg.expression.name;
|
|
267
|
+
if (ts.isIdentifier(prop)) {
|
|
268
|
+
actionName = prop.text;
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
if (actionName) {
|
|
273
|
+
const location = getNodeLocation(sourceFile, call, projectRoot);
|
|
274
|
+
return {
|
|
275
|
+
type: 'state',
|
|
276
|
+
method: 'dispatch',
|
|
277
|
+
target: actionName,
|
|
278
|
+
storeType: 'redux',
|
|
279
|
+
sourceRef: location.sourceRef,
|
|
280
|
+
file: location.file,
|
|
281
|
+
line: location.line
|
|
282
|
+
};
|
|
283
|
+
}
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
// STATE INTELLIGENCE: Zustand set((state) => ({ key: value }))
|
|
288
|
+
if (ts.isIdentifier(expr) && expr.text === 'set') {
|
|
289
|
+
const firstArg = call.arguments[0];
|
|
290
|
+
if (firstArg && ts.isArrowFunction(firstArg)) {
|
|
291
|
+
// Extract keys from object literal in return
|
|
292
|
+
const body = firstArg.body;
|
|
293
|
+
if (ts.isObjectLiteralExpression(body)) {
|
|
294
|
+
const keys = [];
|
|
295
|
+
for (const prop of body.properties) {
|
|
296
|
+
if (ts.isPropertyAssignment(prop) && ts.isIdentifier(prop.name)) {
|
|
297
|
+
keys.push(prop.name.text);
|
|
298
|
+
}
|
|
299
|
+
}
|
|
300
|
+
|
|
301
|
+
if (keys.length > 0) {
|
|
302
|
+
const location = getNodeLocation(sourceFile, call, projectRoot);
|
|
303
|
+
return {
|
|
304
|
+
type: 'state',
|
|
305
|
+
method: 'set',
|
|
306
|
+
target: keys.join(','), // Multiple keys possible
|
|
307
|
+
storeType: 'zustand',
|
|
308
|
+
sourceRef: location.sourceRef,
|
|
309
|
+
file: location.file,
|
|
310
|
+
line: location.line
|
|
311
|
+
};
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
}
|
|
315
|
+
}
|
|
316
|
+
|
|
317
|
+
return null;
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
/**
|
|
321
|
+
* Get first string argument from call expression.
|
|
322
|
+
*
|
|
323
|
+
* @param {ts.CallExpression} call - Call expression
|
|
324
|
+
* @returns {string|null} - String value or null
|
|
325
|
+
*/
|
|
326
|
+
function getFirstStringArg(call) {
|
|
327
|
+
if (call.arguments.length === 0) return null;
|
|
328
|
+
const firstArg = call.arguments[0];
|
|
329
|
+
|
|
330
|
+
// Try string literal first
|
|
331
|
+
const stringLiteral = getStringLiteral(firstArg);
|
|
332
|
+
if (stringLiteral) return stringLiteral;
|
|
333
|
+
|
|
334
|
+
// Try template literal (static only - no interpolations)
|
|
335
|
+
if (ts.isTemplateExpression(firstArg)) {
|
|
336
|
+
// Check if it's a static template (no expressions)
|
|
337
|
+
if (firstArg.templateSpans && firstArg.templateSpans.length === 0) {
|
|
338
|
+
// Pure template literal without ${} - treat as static
|
|
339
|
+
const text = firstArg.head?.text || '';
|
|
340
|
+
return text;
|
|
341
|
+
}
|
|
342
|
+
|
|
343
|
+
// Template with expressions - extract pattern for normalization
|
|
344
|
+
let templateText = firstArg.head?.text || '';
|
|
345
|
+
for (const span of firstArg.templateSpans) {
|
|
346
|
+
// Check if expression is a simple identifier we can replace
|
|
347
|
+
const expr = span.expression;
|
|
348
|
+
if (ts.isIdentifier(expr)) {
|
|
349
|
+
templateText += '${' + expr.text + '}';
|
|
350
|
+
} else {
|
|
351
|
+
// Complex expression - cannot normalize safely
|
|
352
|
+
return null;
|
|
353
|
+
}
|
|
354
|
+
templateText += span.literal?.text || '';
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// Normalize template literal to example path
|
|
358
|
+
const normalized = normalizeTemplateLiteral(templateText);
|
|
359
|
+
return normalized ? normalized.examplePath : null;
|
|
360
|
+
}
|
|
361
|
+
|
|
362
|
+
// Try NoSubstitutionTemplateLiteral (template literal without expressions)
|
|
363
|
+
if (ts.isNoSubstitutionTemplateLiteral(firstArg)) {
|
|
364
|
+
return firstArg.text;
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
return null;
|
|
368
|
+
}
|