@veraxhq/verax 0.3.0 → 0.4.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 +28 -20
- package/bin/verax.js +11 -18
- package/package.json +28 -7
- package/src/cli/commands/baseline.js +1 -2
- package/src/cli/commands/default.js +72 -81
- package/src/cli/commands/doctor.js +29 -0
- package/src/cli/commands/ga.js +3 -0
- package/src/cli/commands/gates.js +1 -1
- package/src/cli/commands/inspect.js +6 -133
- package/src/cli/commands/release-check.js +2 -0
- package/src/cli/commands/run.js +74 -246
- package/src/cli/commands/security-check.js +2 -1
- package/src/cli/commands/truth.js +0 -1
- package/src/cli/entry.js +82 -309
- package/src/cli/util/angular-component-extractor.js +2 -2
- package/src/cli/util/angular-navigation-detector.js +2 -2
- package/src/cli/util/ast-interactive-detector.js +4 -6
- package/src/cli/util/ast-network-detector.js +3 -3
- package/src/cli/util/ast-promise-extractor.js +581 -0
- package/src/cli/util/ast-usestate-detector.js +3 -3
- package/src/cli/util/atomic-write.js +12 -1
- package/src/cli/util/console-reporter.js +72 -0
- package/src/cli/util/detection-engine.js +105 -41
- package/src/cli/util/determinism-runner.js +2 -1
- package/src/cli/util/determinism-writer.js +1 -1
- package/src/cli/util/digest-engine.js +359 -0
- package/src/cli/util/dom-diff.js +226 -0
- package/src/cli/util/env-url.js +0 -4
- package/src/cli/util/evidence-engine.js +287 -0
- package/src/cli/util/expectation-extractor.js +217 -367
- package/src/cli/util/findings-writer.js +19 -126
- package/src/cli/util/framework-detector.js +572 -0
- package/src/cli/util/idgen.js +1 -1
- package/src/cli/util/interaction-planner.js +529 -0
- package/src/cli/util/learn-writer.js +2 -2
- package/src/cli/util/ledger-writer.js +110 -0
- package/src/cli/util/monorepo-resolver.js +162 -0
- package/src/cli/util/observation-engine.js +127 -278
- package/src/cli/util/observe-writer.js +2 -2
- package/src/cli/util/paths.js +12 -3
- package/src/cli/util/project-discovery.js +284 -3
- package/src/cli/util/project-writer.js +2 -2
- package/src/cli/util/run-id.js +23 -27
- package/src/cli/util/run-result.js +778 -0
- package/src/cli/util/selector-resolver.js +235 -0
- package/src/cli/util/summary-writer.js +2 -1
- package/src/cli/util/svelte-navigation-detector.js +3 -3
- package/src/cli/util/svelte-sfc-extractor.js +0 -1
- package/src/cli/util/svelte-state-detector.js +1 -2
- package/src/cli/util/trust-activation-integration.js +496 -0
- package/src/cli/util/trust-activation-wrapper.js +85 -0
- package/src/cli/util/trust-integration-hooks.js +164 -0
- package/src/cli/util/types.js +153 -0
- package/src/cli/util/url-validation.js +40 -0
- package/src/cli/util/vue-navigation-detector.js +4 -3
- package/src/cli/util/vue-sfc-extractor.js +1 -2
- package/src/cli/util/vue-state-detector.js +1 -1
- package/src/types/fs-augment.d.ts +23 -0
- package/src/types/global.d.ts +137 -0
- package/src/types/internal-types.d.ts +35 -0
- package/src/verax/cli/finding-explainer.js +3 -56
- package/src/verax/cli/init.js +4 -18
- package/src/verax/core/action-classifier.js +4 -3
- package/src/verax/core/artifacts/registry.js +0 -15
- package/src/verax/core/artifacts/verifier.js +18 -8
- package/src/verax/core/baseline/baseline.snapshot.js +2 -0
- package/src/verax/core/capabilities/gates.js +7 -1
- package/src/verax/core/confidence/confidence-compute.js +14 -7
- package/src/verax/core/confidence/confidence.loader.js +1 -0
- package/src/verax/core/confidence-engine-refactor.js +8 -3
- package/src/verax/core/confidence-engine.js +162 -23
- package/src/verax/core/contracts/types.js +1 -0
- package/src/verax/core/contracts/validators.js +79 -4
- package/src/verax/core/decision-snapshot.js +3 -30
- package/src/verax/core/decisions/decision.trace.js +2 -0
- package/src/verax/core/determinism/contract-writer.js +2 -2
- package/src/verax/core/determinism/contract.js +1 -1
- package/src/verax/core/determinism/diff.js +42 -1
- package/src/verax/core/determinism/engine.js +7 -6
- package/src/verax/core/determinism/finding-identity.js +3 -2
- package/src/verax/core/determinism/normalize.js +32 -4
- package/src/verax/core/determinism/report-writer.js +1 -0
- package/src/verax/core/determinism/run-fingerprint.js +7 -2
- package/src/verax/core/dynamic-route-intelligence.js +8 -7
- package/src/verax/core/evidence/evidence-capture-service.js +1 -0
- package/src/verax/core/evidence/evidence-intent-ledger.js +2 -1
- package/src/verax/core/evidence-builder.js +2 -2
- package/src/verax/core/execution-mode-context.js +1 -1
- package/src/verax/core/execution-mode-detector.js +5 -3
- package/src/verax/core/failures/exit-codes.js +39 -37
- package/src/verax/core/failures/failure-summary.js +1 -1
- package/src/verax/core/failures/failure.factory.js +3 -3
- package/src/verax/core/failures/failure.ledger.js +3 -2
- package/src/verax/core/ga/ga.artifact.js +1 -1
- package/src/verax/core/ga/ga.contract.js +3 -2
- package/src/verax/core/ga/ga.enforcer.js +1 -0
- package/src/verax/core/guardrails/policy.loader.js +1 -0
- package/src/verax/core/guardrails/truth-reconciliation.js +1 -1
- package/src/verax/core/guardrails-engine.js +2 -2
- package/src/verax/core/incremental-store.js +1 -0
- package/src/verax/core/integrity/budget.js +138 -0
- package/src/verax/core/integrity/determinism.js +342 -0
- package/src/verax/core/integrity/integrity.js +208 -0
- package/src/verax/core/integrity/poisoning.js +108 -0
- package/src/verax/core/integrity/transaction.js +140 -0
- package/src/verax/core/observe/run-timeline.js +2 -0
- package/src/verax/core/perf/perf.report.js +2 -0
- package/src/verax/core/pipeline-tracker.js +5 -0
- package/src/verax/core/release/provenance.builder.js +73 -214
- package/src/verax/core/release/release.enforcer.js +14 -9
- package/src/verax/core/release/reproducibility.check.js +1 -0
- package/src/verax/core/release/sbom.builder.js +32 -23
- package/src/verax/core/replay-validator.js +2 -0
- package/src/verax/core/replay.js +4 -0
- package/src/verax/core/report/cross-index.js +6 -3
- package/src/verax/core/report/human-summary.js +141 -1
- package/src/verax/core/route-intelligence.js +4 -3
- package/src/verax/core/run-id.js +6 -3
- package/src/verax/core/run-manifest.js +4 -3
- package/src/verax/core/security/secrets.scan.js +10 -7
- package/src/verax/core/security/security.enforcer.js +4 -0
- package/src/verax/core/security/supplychain.policy.js +9 -1
- package/src/verax/core/security/vuln.scan.js +2 -2
- package/src/verax/core/truth/truth.certificate.js +3 -1
- package/src/verax/core/ui-feedback-intelligence.js +12 -46
- package/src/verax/detect/conditional-ui-silent-failure.js +84 -0
- package/src/verax/detect/confidence-engine.js +100 -660
- package/src/verax/detect/confidence-helper.js +1 -0
- package/src/verax/detect/detection-engine.js +1 -18
- package/src/verax/detect/dynamic-route-findings.js +17 -14
- package/src/verax/detect/expectation-chain-detector.js +1 -1
- package/src/verax/detect/expectation-model.js +3 -5
- package/src/verax/detect/failure-cause-inference.js +293 -0
- package/src/verax/detect/findings-writer.js +126 -166
- package/src/verax/detect/flow-detector.js +2 -2
- package/src/verax/detect/form-silent-failure.js +98 -0
- package/src/verax/detect/index.js +51 -234
- package/src/verax/detect/invariants-enforcer.js +147 -0
- package/src/verax/detect/journey-stall-detector.js +4 -4
- package/src/verax/detect/navigation-silent-failure.js +82 -0
- package/src/verax/detect/problem-aggregator.js +361 -0
- package/src/verax/detect/route-findings.js +7 -6
- package/src/verax/detect/summary-writer.js +477 -0
- package/src/verax/detect/test-failure-cause-inference.js +314 -0
- package/src/verax/detect/ui-feedback-findings.js +18 -18
- package/src/verax/detect/verdict-engine.js +3 -57
- package/src/verax/detect/view-switch-correlator.js +2 -2
- package/src/verax/flow/flow-engine.js +2 -1
- package/src/verax/flow/flow-spec.js +0 -6
- package/src/verax/index.js +48 -412
- package/src/verax/intel/ts-program.js +1 -0
- package/src/verax/intel/vue-navigation-extractor.js +3 -0
- package/src/verax/learn/action-contract-extractor.js +67 -682
- package/src/verax/learn/ast-contract-extractor.js +1 -1
- package/src/verax/learn/flow-extractor.js +1 -0
- package/src/verax/learn/project-detector.js +5 -0
- package/src/verax/learn/react-router-extractor.js +2 -0
- package/src/verax/learn/route-validator.js +1 -4
- package/src/verax/learn/source-instrumenter.js +1 -0
- package/src/verax/learn/state-extractor.js +2 -1
- package/src/verax/learn/static-extractor.js +1 -0
- package/src/verax/observe/coverage-gaps.js +132 -0
- package/src/verax/observe/expectation-handler.js +126 -0
- package/src/verax/observe/incremental-skip.js +46 -0
- package/src/verax/observe/index.js +735 -84
- package/src/verax/observe/interaction-executor.js +192 -0
- package/src/verax/observe/interaction-runner.js +782 -530
- package/src/verax/observe/network-firewall.js +86 -0
- package/src/verax/observe/observation-builder.js +169 -0
- package/src/verax/observe/observe-context.js +1 -1
- package/src/verax/observe/observe-helpers.js +2 -1
- package/src/verax/observe/observe-runner.js +28 -24
- package/src/verax/observe/observers/budget-observer.js +3 -3
- package/src/verax/observe/observers/console-observer.js +4 -4
- package/src/verax/observe/observers/coverage-observer.js +4 -4
- package/src/verax/observe/observers/interaction-observer.js +3 -3
- package/src/verax/observe/observers/navigation-observer.js +4 -4
- package/src/verax/observe/observers/network-observer.js +4 -4
- package/src/verax/observe/observers/safety-observer.js +1 -1
- package/src/verax/observe/observers/ui-feedback-observer.js +4 -4
- package/src/verax/observe/page-traversal.js +138 -0
- package/src/verax/observe/snapshot-ops.js +94 -0
- package/src/verax/observe/ui-signal-sensor.js +2 -148
- package/src/verax/scan-summary-writer.js +10 -42
- package/src/verax/shared/artifact-manager.js +30 -13
- package/src/verax/shared/caching.js +1 -0
- package/src/verax/shared/expectation-tracker.js +1 -0
- package/src/verax/shared/zip-artifacts.js +6 -0
- package/src/verax/core/confidence-engine.js.backup +0 -471
- package/src/verax/shared/config-loader.js +0 -169
- /package/src/verax/shared/{expectation-proof.js → expectation-validation.js} +0 -0
|
@@ -8,12 +8,10 @@
|
|
|
8
8
|
* NO HEURISTICS. Only static, deterministic analysis.
|
|
9
9
|
*/
|
|
10
10
|
|
|
11
|
-
import { normalizeTemplateLiteral } from '../shared/dynamic-route-utils.js';
|
|
12
|
-
import { parse } from '@babel/parser';
|
|
11
|
+
import { normalizeTemplateLiteral } from '../shared/dynamic-route-utils.js';import { parse } from '@babel/parser';
|
|
13
12
|
import traverse from '@babel/traverse';
|
|
14
|
-
import { readFileSync
|
|
15
|
-
import { relative, sep
|
|
16
|
-
import { isViewSwitchFunction, isDetectableLiteralArg, VIEW_SWITCH_REASON_CODES } from '../shared/view-switch-rules.js';
|
|
13
|
+
import { readFileSync } from 'fs';
|
|
14
|
+
import { relative, sep } from 'path';
|
|
17
15
|
|
|
18
16
|
/**
|
|
19
17
|
* Extract action contracts from a source file.
|
|
@@ -32,41 +30,18 @@ export function extractActionContracts(filePath, workspaceRoot) {
|
|
|
32
30
|
}
|
|
33
31
|
}
|
|
34
32
|
|
|
35
|
-
|
|
33
|
+
function extractActionContractsFromCode(filePath, workspaceRoot, code, lineOffset = 0) {
|
|
36
34
|
const contracts = [];
|
|
37
35
|
|
|
38
36
|
// Track function declarations and arrow function assignments with location
|
|
39
37
|
const functionBodies = new Map(); // name -> { body, loc }
|
|
40
|
-
|
|
41
|
-
// Track imports for cross-file resolution
|
|
42
|
-
const importMap = new Map(); // localName -> { modulePath, exportName }
|
|
43
|
-
const fileDir = dirname(filePath);
|
|
44
38
|
|
|
45
39
|
const ast = parse(code, {
|
|
46
40
|
sourceType: 'unambiguous',
|
|
47
41
|
plugins: ['jsx', 'typescript'],
|
|
48
42
|
});
|
|
49
|
-
|
|
50
|
-
// First pass: collect imports and function definitions
|
|
43
|
+
|
|
51
44
|
traverse.default(ast, {
|
|
52
|
-
ImportDeclaration(path) {
|
|
53
|
-
const source = path.node.source.value;
|
|
54
|
-
if (!source.startsWith('.') && !source.startsWith('/')) {
|
|
55
|
-
return; // Skip external packages
|
|
56
|
-
}
|
|
57
|
-
|
|
58
|
-
const resolvedPath = resolveModulePath(source, fileDir, workspaceRoot);
|
|
59
|
-
if (!resolvedPath) return;
|
|
60
|
-
|
|
61
|
-
path.node.specifiers.forEach(spec => {
|
|
62
|
-
if (spec.type === 'ImportSpecifier' || spec.type === 'ImportDefaultSpecifier') {
|
|
63
|
-
const localName = spec.local.name;
|
|
64
|
-
const exportName = spec.type === 'ImportDefaultSpecifier' ? 'default' : (spec.imported?.name || localName);
|
|
65
|
-
importMap.set(localName, { modulePath: resolvedPath, exportName });
|
|
66
|
-
}
|
|
67
|
-
});
|
|
68
|
-
},
|
|
69
|
-
|
|
70
45
|
FunctionDeclaration(path) {
|
|
71
46
|
if (path.node.id && path.node.id.name) {
|
|
72
47
|
functionBodies.set(path.node.id.name, { body: path.node.body, loc: path.node.loc });
|
|
@@ -81,11 +56,8 @@ export function extractActionContractsFromCode(filePath, workspaceRoot, code, li
|
|
|
81
56
|
) {
|
|
82
57
|
functionBodies.set(path.node.id.name, { body: path.node.init.body, loc: path.node.loc });
|
|
83
58
|
}
|
|
84
|
-
}
|
|
85
|
-
});
|
|
59
|
+
},
|
|
86
60
|
|
|
87
|
-
// Second pass: extract contracts with cross-file support
|
|
88
|
-
traverse.default(ast, {
|
|
89
61
|
// JSX handlers (React)
|
|
90
62
|
JSXAttribute(path) {
|
|
91
63
|
const attrName = path.node.name.name;
|
|
@@ -103,8 +75,7 @@ export function extractActionContractsFromCode(filePath, workspaceRoot, code, li
|
|
|
103
75
|
value.expression.type === 'ArrowFunctionExpression'
|
|
104
76
|
) {
|
|
105
77
|
const handlerBody = value.expression.body;
|
|
106
|
-
const networkCalls = findNetworkCallsInNode(handlerBody
|
|
107
|
-
const viewSwitchCalls = findViewSwitchCallsInNode(handlerBody, functionBodies, importMap, filePath, workspaceRoot, 0);
|
|
78
|
+
const networkCalls = findNetworkCallsInNode(handlerBody);
|
|
108
79
|
|
|
109
80
|
for (const call of networkCalls) {
|
|
110
81
|
const loc = path.node.loc;
|
|
@@ -121,116 +92,52 @@ export function extractActionContractsFromCode(filePath, workspaceRoot, code, li
|
|
|
121
92
|
selectorHint: null
|
|
122
93
|
});
|
|
123
94
|
} else if (call.kind !== 'VALIDATION_BLOCK') {
|
|
124
|
-
|
|
95
|
+
contracts.push({
|
|
125
96
|
kind: call.kind || 'NETWORK_ACTION',
|
|
126
97
|
method: call.method,
|
|
127
98
|
urlPath: call.url,
|
|
128
99
|
source: sourceRef,
|
|
129
100
|
elementType: path.parent.name.name,
|
|
130
101
|
handlerRef: sourceRef
|
|
131
|
-
};
|
|
132
|
-
if (call.isDynamic) {
|
|
133
|
-
contract.isDynamic = true;
|
|
134
|
-
contract.dynamicSegments = call.dynamicSegments;
|
|
135
|
-
if (call.astSnippet) contract.astSnippet = call.astSnippet;
|
|
136
|
-
}
|
|
137
|
-
contracts.push(contract);
|
|
102
|
+
});
|
|
138
103
|
}
|
|
139
104
|
}
|
|
140
|
-
|
|
141
|
-
// Extract view switch promises
|
|
142
|
-
for (const call of viewSwitchCalls) {
|
|
143
|
-
const loc = path.node.loc;
|
|
144
|
-
const sourceRef = formatSourceRef(filePath, workspaceRoot, loc, lineOffset);
|
|
145
|
-
|
|
146
|
-
contracts.push({
|
|
147
|
-
kind: 'VIEW_SWITCH_PROMISE',
|
|
148
|
-
target: call.target,
|
|
149
|
-
viewKind: call.viewKind,
|
|
150
|
-
pattern: call.pattern,
|
|
151
|
-
isUrlChanging: false,
|
|
152
|
-
source: sourceRef,
|
|
153
|
-
elementType: path.parent.name.name,
|
|
154
|
-
handlerRef: sourceRef,
|
|
155
|
-
astSnippet: call.astSnippet,
|
|
156
|
-
reasonCode: call.reasonCode,
|
|
157
|
-
confidenceHint: 'WEAK' // UI-bound but requires correlation
|
|
158
|
-
});
|
|
159
|
-
}
|
|
160
105
|
} else if (
|
|
161
106
|
value.type === 'JSXExpressionContainer' &&
|
|
162
107
|
value.expression.type === 'Identifier'
|
|
163
108
|
) {
|
|
164
109
|
const refName = value.expression.name;
|
|
165
110
|
const handlerRecord = functionBodies.get(refName);
|
|
166
|
-
|
|
167
|
-
// Try local function first
|
|
168
|
-
let networkCalls = [];
|
|
169
|
-
let viewSwitchCalls = [];
|
|
170
|
-
if (handlerRecord) {
|
|
171
|
-
networkCalls = findNetworkCallsInNode(handlerRecord.body, functionBodies, importMap, filePath, workspaceRoot, 0);
|
|
172
|
-
viewSwitchCalls = findViewSwitchCallsInNode(handlerRecord.body, functionBodies, importMap, filePath, workspaceRoot, 0);
|
|
173
|
-
} else {
|
|
174
|
-
// Try cross-file resolution
|
|
175
|
-
const importInfo = importMap.get(refName);
|
|
176
|
-
if (importInfo) {
|
|
177
|
-
const crossFileCalls = followCrossFileFunction(importInfo, workspaceRoot, 0);
|
|
178
|
-
networkCalls = crossFileCalls.filter(c => c.kind !== 'VIEW_SWITCH_PROMISE');
|
|
179
|
-
viewSwitchCalls = crossFileCalls.filter(c => c.kind === 'VIEW_SWITCH_PROMISE');
|
|
180
|
-
}
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
for (const call of networkCalls) {
|
|
184
|
-
const loc = path.node.loc;
|
|
185
|
-
const sourceRef = formatSourceRef(filePath, workspaceRoot, loc, lineOffset);
|
|
186
111
|
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
209
|
-
|
|
112
|
+
if (handlerRecord) {
|
|
113
|
+
const networkCalls = findNetworkCallsInNode(handlerRecord.body);
|
|
114
|
+
|
|
115
|
+
for (const call of networkCalls) {
|
|
116
|
+
const loc = path.node.loc;
|
|
117
|
+
const sourceRef = formatSourceRef(filePath, workspaceRoot, loc, lineOffset);
|
|
118
|
+
|
|
119
|
+
if (call.kind === 'VALIDATION_BLOCK' && isSubmitHandler) {
|
|
120
|
+
contracts.push({
|
|
121
|
+
kind: 'VALIDATION_BLOCK',
|
|
122
|
+
method: call.method,
|
|
123
|
+
urlPath: null,
|
|
124
|
+
source: sourceRef,
|
|
125
|
+
elementType: path.parent.name.name,
|
|
126
|
+
handlerRef: sourceRef,
|
|
127
|
+
selectorHint: null
|
|
128
|
+
});
|
|
129
|
+
} else if (call.kind !== 'VALIDATION_BLOCK') {
|
|
130
|
+
contracts.push({
|
|
131
|
+
kind: call.kind || 'NETWORK_ACTION',
|
|
132
|
+
method: call.method,
|
|
133
|
+
urlPath: call.url,
|
|
134
|
+
source: sourceRef,
|
|
135
|
+
elementType: path.parent.name.name,
|
|
136
|
+
handlerRef: sourceRef
|
|
137
|
+
});
|
|
210
138
|
}
|
|
211
|
-
contracts.push(contract);
|
|
212
139
|
}
|
|
213
140
|
}
|
|
214
|
-
|
|
215
|
-
// Extract view switch promises
|
|
216
|
-
for (const call of viewSwitchCalls) {
|
|
217
|
-
const loc = path.node.loc;
|
|
218
|
-
const sourceRef = formatSourceRef(filePath, workspaceRoot, loc, lineOffset);
|
|
219
|
-
|
|
220
|
-
contracts.push({
|
|
221
|
-
kind: 'VIEW_SWITCH_PROMISE',
|
|
222
|
-
target: call.target,
|
|
223
|
-
viewKind: call.viewKind,
|
|
224
|
-
pattern: call.pattern,
|
|
225
|
-
isUrlChanging: false,
|
|
226
|
-
source: sourceRef,
|
|
227
|
-
elementType: path.parent.name.name,
|
|
228
|
-
handlerRef: sourceRef,
|
|
229
|
-
astSnippet: call.astSnippet,
|
|
230
|
-
reasonCode: call.reasonCode,
|
|
231
|
-
confidenceHint: 'WEAK'
|
|
232
|
-
});
|
|
233
|
-
}
|
|
234
141
|
}
|
|
235
142
|
},
|
|
236
143
|
|
|
@@ -260,68 +167,12 @@ export function extractActionContractsFromCode(filePath, workspaceRoot, code, li
|
|
|
260
167
|
if (record) {
|
|
261
168
|
handlerBody = record.body;
|
|
262
169
|
handlerLoc = record.loc || path.node.loc;
|
|
263
|
-
} else {
|
|
264
|
-
// Try cross-file resolution
|
|
265
|
-
const importInfo = importMap.get(handlerArg.name);
|
|
266
|
-
if (importInfo) {
|
|
267
|
-
const crossFileCalls = followCrossFileFunction(importInfo, workspaceRoot, 0);
|
|
268
|
-
if (crossFileCalls.length > 0) {
|
|
269
|
-
handlerBody = { type: 'BlockStatement', body: [] }; // Dummy body for location
|
|
270
|
-
handlerLoc = path.node.loc;
|
|
271
|
-
const handlerRef = formatSourceRef(filePath, workspaceRoot, handlerLoc, lineOffset);
|
|
272
|
-
|
|
273
|
-
for (const call of crossFileCalls) {
|
|
274
|
-
if (call.kind === 'VALIDATION_BLOCK' && isSubmitEvent) {
|
|
275
|
-
contracts.push({
|
|
276
|
-
kind: 'VALIDATION_BLOCK',
|
|
277
|
-
method: call.method,
|
|
278
|
-
urlPath: null,
|
|
279
|
-
source: handlerRef,
|
|
280
|
-
handlerRef: call.handlerRef || `${handlerRef}#${eventType}`,
|
|
281
|
-
elementType: 'dom',
|
|
282
|
-
selectorHint: null
|
|
283
|
-
});
|
|
284
|
-
} else if (call.kind === 'VIEW_SWITCH_PROMISE') {
|
|
285
|
-
contracts.push({
|
|
286
|
-
kind: 'VIEW_SWITCH_PROMISE',
|
|
287
|
-
target: call.target,
|
|
288
|
-
viewKind: call.viewKind,
|
|
289
|
-
pattern: call.pattern,
|
|
290
|
-
isUrlChanging: false,
|
|
291
|
-
source: handlerRef,
|
|
292
|
-
handlerRef: call.handlerRef || `${handlerRef}#${eventType}`,
|
|
293
|
-
elementType: 'dom',
|
|
294
|
-
astSnippet: call.astSnippet,
|
|
295
|
-
reasonCode: call.reasonCode,
|
|
296
|
-
confidenceHint: 'WEAK'
|
|
297
|
-
});
|
|
298
|
-
} else if (call.kind !== 'VALIDATION_BLOCK') {
|
|
299
|
-
const contract = {
|
|
300
|
-
kind: call.kind || 'NETWORK_ACTION',
|
|
301
|
-
method: call.method,
|
|
302
|
-
urlPath: call.url,
|
|
303
|
-
source: handlerRef,
|
|
304
|
-
handlerRef: call.handlerRef || `${handlerRef}#${eventType}`,
|
|
305
|
-
elementType: 'dom'
|
|
306
|
-
};
|
|
307
|
-
if (call.isDynamic) {
|
|
308
|
-
contract.isDynamic = true;
|
|
309
|
-
contract.dynamicSegments = call.dynamicSegments;
|
|
310
|
-
if (call.astSnippet) contract.astSnippet = call.astSnippet;
|
|
311
|
-
}
|
|
312
|
-
contracts.push(contract);
|
|
313
|
-
}
|
|
314
|
-
}
|
|
315
|
-
return; // Already processed
|
|
316
|
-
}
|
|
317
|
-
}
|
|
318
170
|
}
|
|
319
171
|
}
|
|
320
172
|
|
|
321
173
|
if (!handlerBody) return;
|
|
322
174
|
|
|
323
|
-
const networkCalls = findNetworkCallsInNode(handlerBody
|
|
324
|
-
const viewSwitchCalls = findViewSwitchCallsInNode(handlerBody, functionBodies, importMap, filePath, workspaceRoot, 0);
|
|
175
|
+
const networkCalls = findNetworkCallsInNode(handlerBody);
|
|
325
176
|
const isSubmitEvent = eventType === 'submit';
|
|
326
177
|
|
|
327
178
|
for (const call of networkCalls) {
|
|
@@ -338,20 +189,14 @@ export function extractActionContractsFromCode(filePath, workspaceRoot, code, li
|
|
|
338
189
|
selectorHint: null
|
|
339
190
|
});
|
|
340
191
|
} else if (call.kind !== 'VALIDATION_BLOCK') {
|
|
341
|
-
|
|
192
|
+
contracts.push({
|
|
342
193
|
kind: call.kind || 'NETWORK_ACTION',
|
|
343
194
|
method: call.method,
|
|
344
195
|
urlPath: call.url,
|
|
345
196
|
source: handlerRef,
|
|
346
197
|
handlerRef: `${handlerRef}#${eventType}`,
|
|
347
198
|
elementType: 'dom'
|
|
348
|
-
};
|
|
349
|
-
if (call.isDynamic) {
|
|
350
|
-
contract.isDynamic = true;
|
|
351
|
-
contract.dynamicSegments = call.dynamicSegments;
|
|
352
|
-
if (call.astSnippet) contract.astSnippet = call.astSnippet;
|
|
353
|
-
}
|
|
354
|
-
contracts.push(contract);
|
|
199
|
+
});
|
|
355
200
|
}
|
|
356
201
|
}
|
|
357
202
|
}
|
|
@@ -364,37 +209,22 @@ export function extractActionContractsFromCode(filePath, workspaceRoot, code, li
|
|
|
364
209
|
/**
|
|
365
210
|
* Extract template literal pattern from node.
|
|
366
211
|
* Returns null if template has complex expressions.
|
|
367
|
-
*
|
|
368
|
-
* TRUTH BOUNDARY:
|
|
369
|
-
* - Detectable: Template literals with simple Identifier expressions
|
|
370
|
-
* - `/api/users/${id}` → `/api/users/:id`
|
|
371
|
-
* - `/api/${resource}/${id}` → `/api/:resource/:id`
|
|
372
|
-
* - Ambiguous/Not detectable: Complex expressions (function calls, member access, etc.)
|
|
373
|
-
* - `/api/${getPath()}` → null (rejected)
|
|
374
|
-
* - `/api/${obj.prop}` → null (rejected)
|
|
375
|
-
*
|
|
376
|
-
* @param {Object} node - AST TemplateLiteral node
|
|
377
|
-
* @returns {Object|null} - { templateStr, normalizedUrl, dynamicSegments, astSnippet } or null
|
|
378
212
|
*/
|
|
379
213
|
function extractTemplateLiteralPath(node) {
|
|
380
214
|
if (!node || node.type !== 'TemplateLiteral') {
|
|
381
215
|
return null;
|
|
382
216
|
}
|
|
383
217
|
|
|
384
|
-
// Build template string
|
|
218
|
+
// Build template string
|
|
385
219
|
let templateStr = node.quasis[0]?.value?.cooked || '';
|
|
386
|
-
const dynamicSegments = [];
|
|
387
220
|
|
|
388
221
|
for (let i = 0; i < node.expressions.length; i++) {
|
|
389
222
|
const expr = node.expressions[i];
|
|
390
223
|
|
|
391
|
-
// Only support simple identifiers
|
|
224
|
+
// Only support simple identifiers
|
|
392
225
|
if (expr.type === 'Identifier') {
|
|
393
|
-
|
|
394
|
-
templateStr += '${' + paramName + '}';
|
|
395
|
-
dynamicSegments.push(paramName);
|
|
226
|
+
templateStr += '${' + expr.name + '}';
|
|
396
227
|
} else {
|
|
397
|
-
// Complex expression - reject (truth boundary)
|
|
398
228
|
return null;
|
|
399
229
|
}
|
|
400
230
|
|
|
@@ -403,150 +233,7 @@ function extractTemplateLiteralPath(node) {
|
|
|
403
233
|
}
|
|
404
234
|
}
|
|
405
235
|
|
|
406
|
-
|
|
407
|
-
let normalizedUrl = templateStr;
|
|
408
|
-
for (const paramName of dynamicSegments) {
|
|
409
|
-
normalizedUrl = normalizedUrl.replace(`\${${paramName}}`, `:${paramName}`);
|
|
410
|
-
}
|
|
411
|
-
|
|
412
|
-
// Preserve AST snippet for evidence
|
|
413
|
-
const astSnippet = {
|
|
414
|
-
type: 'TemplateLiteral',
|
|
415
|
-
quasis: node.quasis.map(q => ({ value: q.value?.cooked || '' })),
|
|
416
|
-
expressions: node.expressions.map(e =>
|
|
417
|
-
e.type === 'Identifier' ? { type: 'Identifier', name: e.name } : null
|
|
418
|
-
).filter(Boolean)
|
|
419
|
-
};
|
|
420
|
-
|
|
421
|
-
return {
|
|
422
|
-
templateStr,
|
|
423
|
-
normalizedUrl,
|
|
424
|
-
dynamicSegments,
|
|
425
|
-
astSnippet
|
|
426
|
-
};
|
|
427
|
-
}
|
|
428
|
-
|
|
429
|
-
/**
|
|
430
|
-
* Resolve module path from import specifier
|
|
431
|
-
*/
|
|
432
|
-
function resolveModulePath(specifier, fromDir, workspaceRoot) {
|
|
433
|
-
if (!specifier.startsWith('.') && !specifier.startsWith('/')) {
|
|
434
|
-
return null; // External package
|
|
435
|
-
}
|
|
436
|
-
|
|
437
|
-
let resolved = resolve(fromDir, specifier);
|
|
438
|
-
|
|
439
|
-
// Try with extensions
|
|
440
|
-
const extensions = ['.js', '.jsx', '.ts', '.tsx', ''];
|
|
441
|
-
for (const ext of extensions) {
|
|
442
|
-
const withExt = ext ? resolved + ext : resolved;
|
|
443
|
-
if (existsSync(withExt)) {
|
|
444
|
-
return withExt;
|
|
445
|
-
}
|
|
446
|
-
// Try index files
|
|
447
|
-
const indexPath = resolve(withExt, 'index' + ext);
|
|
448
|
-
if (existsSync(indexPath)) {
|
|
449
|
-
return indexPath;
|
|
450
|
-
}
|
|
451
|
-
}
|
|
452
|
-
|
|
453
|
-
return null;
|
|
454
|
-
}
|
|
455
|
-
|
|
456
|
-
/**
|
|
457
|
-
* Follow function call across files
|
|
458
|
-
*/
|
|
459
|
-
function followCrossFileFunction(importInfo, workspaceRoot, depth) {
|
|
460
|
-
if (depth > 3) return []; // Prevent infinite recursion
|
|
461
|
-
|
|
462
|
-
const { modulePath, exportName } = importInfo;
|
|
463
|
-
if (!existsSync(modulePath)) return [];
|
|
464
|
-
|
|
465
|
-
try {
|
|
466
|
-
const code = readFileSync(modulePath, 'utf-8');
|
|
467
|
-
const fileDir = dirname(modulePath);
|
|
468
|
-
const ast = parse(code, {
|
|
469
|
-
sourceType: 'unambiguous',
|
|
470
|
-
plugins: ['jsx', 'typescript'],
|
|
471
|
-
});
|
|
472
|
-
|
|
473
|
-
const functionBodies = new Map();
|
|
474
|
-
const importMap = new Map();
|
|
475
|
-
|
|
476
|
-
// Collect function definitions and imports
|
|
477
|
-
traverse.default(ast, {
|
|
478
|
-
ImportDeclaration(path) {
|
|
479
|
-
const source = path.node.source.value;
|
|
480
|
-
if (!source.startsWith('.') && !source.startsWith('/')) return;
|
|
481
|
-
const resolved = resolveModulePath(source, fileDir, workspaceRoot);
|
|
482
|
-
if (!resolved) return;
|
|
483
|
-
path.node.specifiers.forEach(spec => {
|
|
484
|
-
if (spec.type === 'ImportSpecifier' || spec.type === 'ImportDefaultSpecifier') {
|
|
485
|
-
const localName = spec.local.name;
|
|
486
|
-
const expName = spec.type === 'ImportDefaultSpecifier' ? 'default' : (spec.imported?.name || localName);
|
|
487
|
-
importMap.set(localName, { modulePath: resolved, exportName: expName });
|
|
488
|
-
}
|
|
489
|
-
});
|
|
490
|
-
},
|
|
491
|
-
FunctionDeclaration(path) {
|
|
492
|
-
if (path.node.id && path.node.id.name) {
|
|
493
|
-
functionBodies.set(path.node.id.name, { body: path.node.body, loc: path.node.loc });
|
|
494
|
-
}
|
|
495
|
-
},
|
|
496
|
-
VariableDeclarator(path) {
|
|
497
|
-
if (path.node.id.type === 'Identifier' && path.node.init &&
|
|
498
|
-
(path.node.init.type === 'ArrowFunctionExpression' || path.node.init.type === 'FunctionExpression')) {
|
|
499
|
-
functionBodies.set(path.node.id.name, { body: path.node.init.body, loc: path.node.loc });
|
|
500
|
-
}
|
|
501
|
-
},
|
|
502
|
-
ExportNamedDeclaration(path) {
|
|
503
|
-
if (exportName === 'default') return;
|
|
504
|
-
const decl = path.node.declaration;
|
|
505
|
-
if (decl && decl.type === 'FunctionDeclaration' && decl.id && decl.id.name === exportName) {
|
|
506
|
-
functionBodies.set(exportName, { body: decl.body, loc: decl.loc });
|
|
507
|
-
}
|
|
508
|
-
},
|
|
509
|
-
ExportDefaultDeclaration(path) {
|
|
510
|
-
if (exportName !== 'default') return;
|
|
511
|
-
const decl = path.node.declaration;
|
|
512
|
-
if (decl && decl.type === 'FunctionDeclaration' && decl.id) {
|
|
513
|
-
functionBodies.set(decl.id.name, { body: decl.body, loc: decl.loc });
|
|
514
|
-
} else if (decl && (decl.type === 'ArrowFunctionExpression' || decl.type === 'FunctionExpression')) {
|
|
515
|
-
// Anonymous default export - scan directly
|
|
516
|
-
const calls = findNetworkCallsInNode(decl.body, functionBodies, importMap, modulePath, workspaceRoot, depth + 1);
|
|
517
|
-
return calls.map(call => ({
|
|
518
|
-
...call,
|
|
519
|
-
handlerRef: formatSourceRef(modulePath, workspaceRoot, decl.loc || { start: { line: 1, column: 0 } }, 0)
|
|
520
|
-
}));
|
|
521
|
-
}
|
|
522
|
-
}
|
|
523
|
-
});
|
|
524
|
-
|
|
525
|
-
// Find the exported function
|
|
526
|
-
const funcDef = functionBodies.get(exportName);
|
|
527
|
-
if (funcDef) {
|
|
528
|
-
const networkCalls = findNetworkCallsInNode(funcDef.body, functionBodies, importMap, modulePath, workspaceRoot, depth + 1);
|
|
529
|
-
const viewSwitchCalls = findViewSwitchCallsInNode(funcDef.body, functionBodies, importMap, modulePath, workspaceRoot, depth + 1);
|
|
530
|
-
const handlerRef = formatSourceRef(modulePath, workspaceRoot, funcDef.loc || { start: { line: 1, column: 0 } }, 0);
|
|
531
|
-
|
|
532
|
-
const allCalls = [
|
|
533
|
-
...networkCalls.map(call => ({
|
|
534
|
-
...call,
|
|
535
|
-
handlerRef
|
|
536
|
-
})),
|
|
537
|
-
...viewSwitchCalls.map(call => ({
|
|
538
|
-
...call,
|
|
539
|
-
handlerRef
|
|
540
|
-
}))
|
|
541
|
-
];
|
|
542
|
-
|
|
543
|
-
return allCalls;
|
|
544
|
-
}
|
|
545
|
-
} catch (err) {
|
|
546
|
-
// Ignore parse errors
|
|
547
|
-
}
|
|
548
|
-
|
|
549
|
-
return [];
|
|
236
|
+
return templateStr;
|
|
550
237
|
}
|
|
551
238
|
|
|
552
239
|
/**
|
|
@@ -554,14 +241,9 @@ function followCrossFileFunction(importInfo, workspaceRoot, depth) {
|
|
|
554
241
|
* Only returns calls with static URL/path literals or template patterns.
|
|
555
242
|
*
|
|
556
243
|
* @param {Object} node - AST node to scan
|
|
557
|
-
* @param {Map} functionBodies - Map of function name -> body for local functions
|
|
558
|
-
* @param {Map} importMap - Map of import name -> module info
|
|
559
|
-
* @param {string} filePath - Current file path
|
|
560
|
-
* @param {string} workspaceRoot - Workspace root
|
|
561
|
-
* @param {number} depth - Recursion depth
|
|
562
244
|
* @returns {Array<Object>} - Array of {kind, method, url} where kind is 'NETWORK_ACTION' or 'NAVIGATION_ACTION'
|
|
563
245
|
*/
|
|
564
|
-
function findNetworkCallsInNode(node
|
|
246
|
+
function findNetworkCallsInNode(node) {
|
|
565
247
|
const calls = [];
|
|
566
248
|
|
|
567
249
|
// Track if we found preventDefault or return false for validation block detection
|
|
@@ -575,57 +257,27 @@ function findNetworkCallsInNode(node, functionBodies = new Map(), importMap = ne
|
|
|
575
257
|
// Check if this is a CallExpression
|
|
576
258
|
if (n.type === 'CallExpression') {
|
|
577
259
|
const callee = n.callee;
|
|
578
|
-
|
|
579
|
-
// Follow function calls to their definitions (cross-file support)
|
|
580
|
-
if (callee.type === 'Identifier' && depth < 3) {
|
|
581
|
-
const funcName = callee.name;
|
|
582
|
-
const localFunc = functionBodies.get(funcName);
|
|
583
|
-
if (localFunc) {
|
|
584
|
-
// Recursively scan local function
|
|
585
|
-
const innerCalls = findNetworkCallsInNode(localFunc.body, functionBodies, importMap, filePath, workspaceRoot, depth + 1);
|
|
586
|
-
calls.push(...innerCalls);
|
|
587
|
-
} else {
|
|
588
|
-
// Try cross-file resolution
|
|
589
|
-
const importInfo = importMap.get(funcName);
|
|
590
|
-
if (importInfo) {
|
|
591
|
-
const crossFileCalls = followCrossFileFunction(importInfo, workspaceRoot, depth);
|
|
592
|
-
calls.push(...crossFileCalls);
|
|
593
|
-
}
|
|
594
|
-
}
|
|
595
|
-
}
|
|
596
260
|
|
|
597
261
|
// Case 1: fetch(url, options)
|
|
598
262
|
if (callee.type === 'Identifier' && callee.name === 'fetch') {
|
|
599
263
|
const urlArg = n.arguments[0];
|
|
600
264
|
const optionsArg = n.arguments[1];
|
|
601
265
|
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
)
|
|
608
|
-
|
|
609
|
-
|
|
266
|
+
// Only accept static string literals
|
|
267
|
+
if (urlArg && urlArg.type === 'StringLiteral') {
|
|
268
|
+
let method = 'GET'; // default
|
|
269
|
+
|
|
270
|
+
// Try to extract method from options
|
|
271
|
+
if (optionsArg && optionsArg.type === 'ObjectExpression') {
|
|
272
|
+
const methodProp = optionsArg.properties.find(
|
|
273
|
+
(p) => p.key && p.key.name === 'method'
|
|
274
|
+
);
|
|
275
|
+
if (methodProp && methodProp.value.type === 'StringLiteral') {
|
|
276
|
+
method = methodProp.value.value.toUpperCase();
|
|
277
|
+
}
|
|
610
278
|
}
|
|
611
|
-
}
|
|
612
279
|
|
|
613
|
-
if (urlArg && urlArg.type === 'StringLiteral') {
|
|
614
|
-
// Static URL
|
|
615
280
|
calls.push({ kind: 'NETWORK_ACTION', method, url: urlArg.value });
|
|
616
|
-
} else if (urlArg && urlArg.type === 'TemplateLiteral') {
|
|
617
|
-
// Dynamic URL: fetch(`/api/users/${id}`)
|
|
618
|
-
const templateInfo = extractTemplateLiteralPath(urlArg);
|
|
619
|
-
if (templateInfo && templateInfo.normalizedUrl.startsWith('/')) {
|
|
620
|
-
calls.push({
|
|
621
|
-
kind: 'NETWORK_ACTION',
|
|
622
|
-
method,
|
|
623
|
-
url: templateInfo.normalizedUrl,
|
|
624
|
-
isDynamic: true,
|
|
625
|
-
dynamicSegments: templateInfo.dynamicSegments.length,
|
|
626
|
-
astSnippet: templateInfo.astSnippet
|
|
627
|
-
});
|
|
628
|
-
}
|
|
629
281
|
}
|
|
630
282
|
}
|
|
631
283
|
|
|
@@ -640,57 +292,7 @@ function findNetworkCallsInNode(node, functionBodies = new Map(), importMap = ne
|
|
|
640
292
|
const urlArg = n.arguments[0];
|
|
641
293
|
|
|
642
294
|
if (urlArg && urlArg.type === 'StringLiteral') {
|
|
643
|
-
// Static URL
|
|
644
295
|
calls.push({ kind: 'NETWORK_ACTION', method: methodName, url: urlArg.value });
|
|
645
|
-
} else if (urlArg && urlArg.type === 'TemplateLiteral') {
|
|
646
|
-
// Dynamic URL: axios.post(`/api/users/${id}`)
|
|
647
|
-
const templateInfo = extractTemplateLiteralPath(urlArg);
|
|
648
|
-
if (templateInfo && templateInfo.normalizedUrl.startsWith('/')) {
|
|
649
|
-
calls.push({
|
|
650
|
-
kind: 'NETWORK_ACTION',
|
|
651
|
-
method: methodName,
|
|
652
|
-
url: templateInfo.normalizedUrl,
|
|
653
|
-
isDynamic: true,
|
|
654
|
-
dynamicSegments: templateInfo.dynamicSegments.length,
|
|
655
|
-
astSnippet: templateInfo.astSnippet
|
|
656
|
-
});
|
|
657
|
-
}
|
|
658
|
-
}
|
|
659
|
-
}
|
|
660
|
-
|
|
661
|
-
// Case 2b: apiClient.get(url), apiClient.post(url), http.get(url), etc. (wrapped API clients)
|
|
662
|
-
// Check this only if not axios (already handled above)
|
|
663
|
-
if (
|
|
664
|
-
callee.type === 'MemberExpression' &&
|
|
665
|
-
callee.object.type === 'Identifier' &&
|
|
666
|
-
callee.object.name !== 'axios' &&
|
|
667
|
-
callee.property.type === 'Identifier'
|
|
668
|
-
) {
|
|
669
|
-
const objectName = callee.object.name;
|
|
670
|
-
const methodName = callee.property.name.toUpperCase();
|
|
671
|
-
const urlArg = n.arguments[0];
|
|
672
|
-
|
|
673
|
-
// Common API client patterns (exclude axios which is handled above)
|
|
674
|
-
const apiClientNames = ['apiClient', 'http', 'request', 'api', 'client', 'httpClient'];
|
|
675
|
-
const httpMethods = ['GET', 'POST', 'PUT', 'PATCH', 'DELETE', 'HEAD', 'OPTIONS'];
|
|
676
|
-
if (apiClientNames.includes(objectName) && httpMethods.includes(methodName)) {
|
|
677
|
-
if (urlArg && urlArg.type === 'StringLiteral') {
|
|
678
|
-
// Static URL
|
|
679
|
-
calls.push({ kind: 'NETWORK_ACTION', method: methodName, url: urlArg.value });
|
|
680
|
-
} else if (urlArg && urlArg.type === 'TemplateLiteral') {
|
|
681
|
-
// Dynamic URL: apiClient.post(`/api/users/${id}`)
|
|
682
|
-
const templateInfo = extractTemplateLiteralPath(urlArg);
|
|
683
|
-
if (templateInfo && templateInfo.normalizedUrl.startsWith('/')) {
|
|
684
|
-
calls.push({
|
|
685
|
-
kind: 'NETWORK_ACTION',
|
|
686
|
-
method: methodName,
|
|
687
|
-
url: templateInfo.normalizedUrl,
|
|
688
|
-
isDynamic: true,
|
|
689
|
-
dynamicSegments: templateInfo.dynamicSegments.length,
|
|
690
|
-
astSnippet: templateInfo.astSnippet
|
|
691
|
-
});
|
|
692
|
-
}
|
|
693
|
-
}
|
|
694
296
|
}
|
|
695
297
|
}
|
|
696
298
|
|
|
@@ -704,30 +306,15 @@ function findNetworkCallsInNode(node, functionBodies = new Map(), importMap = ne
|
|
|
704
306
|
const methodArg = n.arguments[0];
|
|
705
307
|
const urlArg = n.arguments[1];
|
|
706
308
|
|
|
707
|
-
if (
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
});
|
|
717
|
-
} else if (urlArg && urlArg.type === 'TemplateLiteral') {
|
|
718
|
-
// Dynamic URL: xhr.open('GET', `/api/users/${id}`)
|
|
719
|
-
const templateInfo = extractTemplateLiteralPath(urlArg);
|
|
720
|
-
if (templateInfo && templateInfo.normalizedUrl.startsWith('/')) {
|
|
721
|
-
calls.push({
|
|
722
|
-
kind: 'NETWORK_ACTION',
|
|
723
|
-
method,
|
|
724
|
-
url: templateInfo.normalizedUrl,
|
|
725
|
-
isDynamic: true,
|
|
726
|
-
dynamicSegments: templateInfo.dynamicSegments.length,
|
|
727
|
-
astSnippet: templateInfo.astSnippet
|
|
728
|
-
});
|
|
729
|
-
}
|
|
730
|
-
}
|
|
309
|
+
if (
|
|
310
|
+
methodArg && methodArg.type === 'StringLiteral' &&
|
|
311
|
+
urlArg && urlArg.type === 'StringLiteral'
|
|
312
|
+
) {
|
|
313
|
+
calls.push({
|
|
314
|
+
kind: 'NETWORK_ACTION',
|
|
315
|
+
method: methodArg.value.toUpperCase(),
|
|
316
|
+
url: urlArg.value,
|
|
317
|
+
});
|
|
731
318
|
}
|
|
732
319
|
}
|
|
733
320
|
|
|
@@ -832,211 +419,6 @@ function findNetworkCallsInNode(node, functionBodies = new Map(), importMap = ne
|
|
|
832
419
|
return calls;
|
|
833
420
|
}
|
|
834
421
|
|
|
835
|
-
/**
|
|
836
|
-
* Find view switch promise calls (setView, dispatch(NAVIGATE), etc.) in an AST node.
|
|
837
|
-
* TRUTH BOUNDARY: Only literal string/number arguments are accepted.
|
|
838
|
-
*
|
|
839
|
-
* @param {Object} node - AST node to scan
|
|
840
|
-
* @param {Map} functionBodies - Map of function name -> body for local functions
|
|
841
|
-
* @param {Map} importMap - Map of import name -> module info
|
|
842
|
-
* @param {string} filePath - Current file path
|
|
843
|
-
* @param {string} workspaceRoot - Workspace root
|
|
844
|
-
* @param {number} depth - Recursion depth
|
|
845
|
-
* @returns {Array<Object>} - Array of {kind: 'VIEW_SWITCH_PROMISE', target, kind, isUrlChanging, astSnippet}
|
|
846
|
-
*/
|
|
847
|
-
function findViewSwitchCallsInNode(node, functionBodies = new Map(), importMap = new Map(), filePath = '', workspaceRoot = '', depth = 0) {
|
|
848
|
-
const calls = [];
|
|
849
|
-
|
|
850
|
-
function scan(n) {
|
|
851
|
-
if (!n || typeof n !== 'object') return;
|
|
852
|
-
|
|
853
|
-
if (n.type === 'CallExpression') {
|
|
854
|
-
const callee = n.callee;
|
|
855
|
-
|
|
856
|
-
// Follow function calls to their definitions (cross-file support)
|
|
857
|
-
if (callee.type === 'Identifier' && depth < 3) {
|
|
858
|
-
const funcName = callee.name;
|
|
859
|
-
const localFunc = functionBodies.get(funcName);
|
|
860
|
-
if (localFunc) {
|
|
861
|
-
const innerCalls = findViewSwitchCallsInNode(localFunc.body, functionBodies, importMap, filePath, workspaceRoot, depth + 1);
|
|
862
|
-
calls.push(...innerCalls);
|
|
863
|
-
} else {
|
|
864
|
-
const importInfo = importMap.get(funcName);
|
|
865
|
-
if (importInfo) {
|
|
866
|
-
const crossFileCalls = followCrossFileFunction(importInfo, workspaceRoot, depth);
|
|
867
|
-
// Filter for view switch calls only
|
|
868
|
-
const viewSwitchCalls = crossFileCalls.filter(c => c.kind === 'VIEW_SWITCH_PROMISE');
|
|
869
|
-
calls.push(...viewSwitchCalls);
|
|
870
|
-
}
|
|
871
|
-
}
|
|
872
|
-
}
|
|
873
|
-
|
|
874
|
-
// Case 1: React setState patterns: setView('settings'), setTab('billing')
|
|
875
|
-
if (callee.type === 'Identifier') {
|
|
876
|
-
const funcName = callee.name;
|
|
877
|
-
const viewSwitchInfo = isViewSwitchFunction(funcName);
|
|
878
|
-
|
|
879
|
-
if (viewSwitchInfo) {
|
|
880
|
-
const firstArg = n.arguments[0];
|
|
881
|
-
const literalArg = isDetectableLiteralArg(firstArg);
|
|
882
|
-
|
|
883
|
-
if (literalArg && literalArg.value) {
|
|
884
|
-
// Preserve AST snippet for evidence
|
|
885
|
-
const astSnippet = {
|
|
886
|
-
type: 'CallExpression',
|
|
887
|
-
callee: { type: 'Identifier', name: funcName },
|
|
888
|
-
arguments: [{
|
|
889
|
-
type: firstArg.type,
|
|
890
|
-
value: firstArg.type === 'StringLiteral' ? firstArg.value : firstArg.value
|
|
891
|
-
}]
|
|
892
|
-
};
|
|
893
|
-
|
|
894
|
-
calls.push({
|
|
895
|
-
kind: 'VIEW_SWITCH_PROMISE',
|
|
896
|
-
target: literalArg.value,
|
|
897
|
-
viewKind: viewSwitchInfo.kind,
|
|
898
|
-
pattern: viewSwitchInfo.pattern,
|
|
899
|
-
isUrlChanging: false,
|
|
900
|
-
astSnippet,
|
|
901
|
-
reasonCode: literalArg.reasonCode
|
|
902
|
-
});
|
|
903
|
-
}
|
|
904
|
-
}
|
|
905
|
-
}
|
|
906
|
-
|
|
907
|
-
// Case 2: Redux dispatch({type: 'NAVIGATE', to: 'profile'}) or dispatch(navigate('profile'))
|
|
908
|
-
if (callee.type === 'Identifier' && callee.name === 'dispatch') {
|
|
909
|
-
const firstArg = n.arguments[0];
|
|
910
|
-
|
|
911
|
-
// dispatch({type: 'NAVIGATE', to: 'profile'})
|
|
912
|
-
if (firstArg && firstArg.type === 'ObjectExpression') {
|
|
913
|
-
const typeProp = firstArg.properties.find(p =>
|
|
914
|
-
p.key && p.key.type === 'Identifier' && p.key.name === 'type'
|
|
915
|
-
);
|
|
916
|
-
const toProp = firstArg.properties.find(p =>
|
|
917
|
-
p.key && (p.key.name === 'to' || p.key.name === 'target' || p.key.name === 'view')
|
|
918
|
-
);
|
|
919
|
-
|
|
920
|
-
if (typeProp && typeProp.value && typeProp.value.type === 'StringLiteral') {
|
|
921
|
-
const actionType = typeProp.value.value;
|
|
922
|
-
const viewSwitchInfo = isViewSwitchFunction(actionType);
|
|
923
|
-
|
|
924
|
-
if (viewSwitchInfo && toProp && toProp.value) {
|
|
925
|
-
const literalArg = isDetectableLiteralArg(toProp.value);
|
|
926
|
-
|
|
927
|
-
if (literalArg && literalArg.value) {
|
|
928
|
-
const astSnippet = {
|
|
929
|
-
type: 'CallExpression',
|
|
930
|
-
callee: { type: 'Identifier', name: 'dispatch' },
|
|
931
|
-
arguments: [{
|
|
932
|
-
type: 'ObjectExpression',
|
|
933
|
-
properties: [
|
|
934
|
-
{ key: 'type', value: actionType },
|
|
935
|
-
{ key: toProp.key.name, value: literalArg.value }
|
|
936
|
-
]
|
|
937
|
-
}]
|
|
938
|
-
};
|
|
939
|
-
|
|
940
|
-
calls.push({
|
|
941
|
-
kind: 'VIEW_SWITCH_PROMISE',
|
|
942
|
-
target: literalArg.value,
|
|
943
|
-
viewKind: viewSwitchInfo.kind,
|
|
944
|
-
pattern: 'redux',
|
|
945
|
-
isUrlChanging: false,
|
|
946
|
-
astSnippet,
|
|
947
|
-
reasonCode: literalArg.reasonCode
|
|
948
|
-
});
|
|
949
|
-
}
|
|
950
|
-
}
|
|
951
|
-
}
|
|
952
|
-
}
|
|
953
|
-
|
|
954
|
-
// dispatch(navigate('profile')) - action creator call
|
|
955
|
-
if (firstArg && firstArg.type === 'CallExpression') {
|
|
956
|
-
const actionCallee = firstArg.callee;
|
|
957
|
-
if (actionCallee.type === 'Identifier') {
|
|
958
|
-
const actionName = actionCallee.name;
|
|
959
|
-
const viewSwitchInfo = isViewSwitchFunction(actionName);
|
|
960
|
-
|
|
961
|
-
if (viewSwitchInfo) {
|
|
962
|
-
const actionFirstArg = firstArg.arguments[0];
|
|
963
|
-
const literalArg = isDetectableLiteralArg(actionFirstArg);
|
|
964
|
-
|
|
965
|
-
if (literalArg && literalArg.value) {
|
|
966
|
-
const astSnippet = {
|
|
967
|
-
type: 'CallExpression',
|
|
968
|
-
callee: { type: 'Identifier', name: 'dispatch' },
|
|
969
|
-
arguments: [{
|
|
970
|
-
type: 'CallExpression',
|
|
971
|
-
callee: { type: 'Identifier', name: actionName },
|
|
972
|
-
arguments: [{ type: actionFirstArg.type, value: literalArg.value }]
|
|
973
|
-
}]
|
|
974
|
-
};
|
|
975
|
-
|
|
976
|
-
calls.push({
|
|
977
|
-
kind: 'VIEW_SWITCH_PROMISE',
|
|
978
|
-
target: literalArg.value,
|
|
979
|
-
viewKind: viewSwitchInfo.kind,
|
|
980
|
-
pattern: 'redux',
|
|
981
|
-
isUrlChanging: false,
|
|
982
|
-
astSnippet,
|
|
983
|
-
reasonCode: literalArg.reasonCode
|
|
984
|
-
});
|
|
985
|
-
}
|
|
986
|
-
}
|
|
987
|
-
}
|
|
988
|
-
}
|
|
989
|
-
}
|
|
990
|
-
|
|
991
|
-
// Case 3: Generic function calls: showModal('login'), openDrawer('cart')
|
|
992
|
-
if (callee.type === 'Identifier') {
|
|
993
|
-
const funcName = callee.name;
|
|
994
|
-
const viewSwitchInfo = isViewSwitchFunction(funcName);
|
|
995
|
-
|
|
996
|
-
if (viewSwitchInfo && viewSwitchInfo.pattern === 'generic') {
|
|
997
|
-
const firstArg = n.arguments[0];
|
|
998
|
-
const literalArg = isDetectableLiteralArg(firstArg);
|
|
999
|
-
|
|
1000
|
-
if (literalArg && literalArg.value) {
|
|
1001
|
-
const astSnippet = {
|
|
1002
|
-
type: 'CallExpression',
|
|
1003
|
-
callee: { type: 'Identifier', name: funcName },
|
|
1004
|
-
arguments: [{
|
|
1005
|
-
type: firstArg.type,
|
|
1006
|
-
value: literalArg.value
|
|
1007
|
-
}]
|
|
1008
|
-
};
|
|
1009
|
-
|
|
1010
|
-
calls.push({
|
|
1011
|
-
kind: 'VIEW_SWITCH_PROMISE',
|
|
1012
|
-
target: literalArg.value,
|
|
1013
|
-
viewKind: viewSwitchInfo.kind,
|
|
1014
|
-
pattern: 'generic',
|
|
1015
|
-
isUrlChanging: false,
|
|
1016
|
-
astSnippet,
|
|
1017
|
-
reasonCode: literalArg.reasonCode
|
|
1018
|
-
});
|
|
1019
|
-
}
|
|
1020
|
-
}
|
|
1021
|
-
}
|
|
1022
|
-
}
|
|
1023
|
-
|
|
1024
|
-
// Recursively scan child nodes
|
|
1025
|
-
for (const key in n) {
|
|
1026
|
-
if (key === 'parent' || key === 'leadingComments' || key === 'trailingComments') continue;
|
|
1027
|
-
const child = n[key];
|
|
1028
|
-
if (Array.isArray(child)) {
|
|
1029
|
-
child.forEach(item => scan(item));
|
|
1030
|
-
} else if (child && typeof child === 'object') {
|
|
1031
|
-
scan(child);
|
|
1032
|
-
}
|
|
1033
|
-
}
|
|
1034
|
-
}
|
|
1035
|
-
|
|
1036
|
-
scan(node);
|
|
1037
|
-
return calls;
|
|
1038
|
-
}
|
|
1039
|
-
|
|
1040
422
|
/**
|
|
1041
423
|
* Format source reference as "file:line:col"
|
|
1042
424
|
* Normalizes Windows paths to use forward slashes.
|
|
@@ -1108,10 +490,13 @@ export async function scanForContracts(rootPath, workspaceRoot) {
|
|
|
1108
490
|
const html = readFileSync(fullPath, 'utf-8');
|
|
1109
491
|
const scriptRegex = /<script[^>]*>([\s\S]*?)<\/script>/gi;
|
|
1110
492
|
let match;
|
|
493
|
+
// @ts-expect-error - readFileSync with encoding returns string
|
|
1111
494
|
while ((match = scriptRegex.exec(html)) !== null) {
|
|
1112
495
|
const tagOpen = html.slice(match.index, html.indexOf('>', match.index) + 1);
|
|
496
|
+
// @ts-expect-error - readFileSync with encoding returns string
|
|
1113
497
|
if (/\ssrc=/i.test(tagOpen)) continue; // skip external scripts
|
|
1114
498
|
const before = html.slice(0, match.index);
|
|
499
|
+
// @ts-expect-error - readFileSync with encoding returns string
|
|
1115
500
|
const lineOffset = (before.match(/\n/g) || []).length;
|
|
1116
501
|
const code = match[1];
|
|
1117
502
|
const blockContracts = extractActionContractsFromCode(fullPath, workspaceRoot, code, lineOffset + 1);
|