@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.
Files changed (135) hide show
  1. package/README.md +123 -88
  2. package/bin/verax.js +11 -452
  3. package/package.json +24 -36
  4. package/src/cli/commands/default.js +681 -0
  5. package/src/cli/commands/doctor.js +197 -0
  6. package/src/cli/commands/inspect.js +109 -0
  7. package/src/cli/commands/run.js +586 -0
  8. package/src/cli/entry.js +196 -0
  9. package/src/cli/util/atomic-write.js +37 -0
  10. package/src/cli/util/detection-engine.js +297 -0
  11. package/src/cli/util/env-url.js +33 -0
  12. package/src/cli/util/errors.js +44 -0
  13. package/src/cli/util/events.js +110 -0
  14. package/src/cli/util/expectation-extractor.js +388 -0
  15. package/src/cli/util/findings-writer.js +32 -0
  16. package/src/cli/util/idgen.js +87 -0
  17. package/src/cli/util/learn-writer.js +39 -0
  18. package/src/cli/util/observation-engine.js +412 -0
  19. package/src/cli/util/observe-writer.js +25 -0
  20. package/src/cli/util/paths.js +30 -0
  21. package/src/cli/util/project-discovery.js +297 -0
  22. package/src/cli/util/project-writer.js +26 -0
  23. package/src/cli/util/redact.js +128 -0
  24. package/src/cli/util/run-id.js +30 -0
  25. package/src/cli/util/runtime-budget.js +147 -0
  26. package/src/cli/util/summary-writer.js +43 -0
  27. package/src/types/global.d.ts +28 -0
  28. package/src/types/ts-ast.d.ts +24 -0
  29. package/src/verax/cli/ci-summary.js +35 -0
  30. package/src/verax/cli/context-explanation.js +89 -0
  31. package/src/verax/cli/doctor.js +277 -0
  32. package/src/verax/cli/error-normalizer.js +154 -0
  33. package/src/verax/cli/explain-output.js +105 -0
  34. package/src/verax/cli/finding-explainer.js +130 -0
  35. package/src/verax/cli/init.js +237 -0
  36. package/src/verax/cli/run-overview.js +163 -0
  37. package/src/verax/cli/url-safety.js +111 -0
  38. package/src/verax/cli/wizard.js +109 -0
  39. package/src/verax/cli/zero-findings-explainer.js +57 -0
  40. package/src/verax/cli/zero-interaction-explainer.js +127 -0
  41. package/src/verax/core/action-classifier.js +86 -0
  42. package/src/verax/core/budget-engine.js +218 -0
  43. package/src/verax/core/canonical-outcomes.js +157 -0
  44. package/src/verax/core/decision-snapshot.js +335 -0
  45. package/src/verax/core/determinism-model.js +432 -0
  46. package/src/verax/core/incremental-store.js +245 -0
  47. package/src/verax/core/invariants.js +356 -0
  48. package/src/verax/core/promise-model.js +230 -0
  49. package/src/verax/core/replay-validator.js +350 -0
  50. package/src/verax/core/replay.js +222 -0
  51. package/src/verax/core/run-id.js +175 -0
  52. package/src/verax/core/run-manifest.js +99 -0
  53. package/src/verax/core/silence-impact.js +369 -0
  54. package/src/verax/core/silence-model.js +523 -0
  55. package/src/verax/detect/comparison.js +7 -34
  56. package/src/verax/detect/confidence-engine.js +764 -329
  57. package/src/verax/detect/detection-engine.js +293 -0
  58. package/src/verax/detect/evidence-index.js +127 -0
  59. package/src/verax/detect/expectation-model.js +241 -168
  60. package/src/verax/detect/explanation-helpers.js +187 -0
  61. package/src/verax/detect/finding-detector.js +450 -0
  62. package/src/verax/detect/findings-writer.js +41 -12
  63. package/src/verax/detect/flow-detector.js +366 -0
  64. package/src/verax/detect/index.js +200 -288
  65. package/src/verax/detect/interactive-findings.js +612 -0
  66. package/src/verax/detect/signal-mapper.js +308 -0
  67. package/src/verax/detect/skip-classifier.js +4 -4
  68. package/src/verax/detect/verdict-engine.js +561 -0
  69. package/src/verax/evidence-index-writer.js +61 -0
  70. package/src/verax/flow/flow-engine.js +3 -2
  71. package/src/verax/flow/flow-spec.js +1 -2
  72. package/src/verax/index.js +103 -15
  73. package/src/verax/intel/effect-detector.js +368 -0
  74. package/src/verax/intel/handler-mapper.js +249 -0
  75. package/src/verax/intel/index.js +281 -0
  76. package/src/verax/intel/route-extractor.js +280 -0
  77. package/src/verax/intel/ts-program.js +256 -0
  78. package/src/verax/intel/vue-navigation-extractor.js +642 -0
  79. package/src/verax/intel/vue-router-extractor.js +325 -0
  80. package/src/verax/learn/action-contract-extractor.js +338 -104
  81. package/src/verax/learn/ast-contract-extractor.js +148 -6
  82. package/src/verax/learn/flow-extractor.js +172 -0
  83. package/src/verax/learn/index.js +36 -2
  84. package/src/verax/learn/manifest-writer.js +122 -58
  85. package/src/verax/learn/project-detector.js +40 -0
  86. package/src/verax/learn/route-extractor.js +28 -97
  87. package/src/verax/learn/route-validator.js +8 -7
  88. package/src/verax/learn/state-extractor.js +212 -0
  89. package/src/verax/learn/static-extractor-navigation.js +114 -0
  90. package/src/verax/learn/static-extractor-validation.js +88 -0
  91. package/src/verax/learn/static-extractor.js +119 -10
  92. package/src/verax/learn/truth-assessor.js +24 -21
  93. package/src/verax/learn/ts-contract-resolver.js +14 -12
  94. package/src/verax/observe/aria-sensor.js +211 -0
  95. package/src/verax/observe/browser.js +30 -6
  96. package/src/verax/observe/console-sensor.js +2 -18
  97. package/src/verax/observe/domain-boundary.js +10 -1
  98. package/src/verax/observe/expectation-executor.js +513 -0
  99. package/src/verax/observe/flow-matcher.js +143 -0
  100. package/src/verax/observe/focus-sensor.js +196 -0
  101. package/src/verax/observe/human-driver.js +660 -273
  102. package/src/verax/observe/index.js +910 -26
  103. package/src/verax/observe/interaction-discovery.js +378 -15
  104. package/src/verax/observe/interaction-runner.js +562 -197
  105. package/src/verax/observe/loading-sensor.js +145 -0
  106. package/src/verax/observe/navigation-sensor.js +255 -0
  107. package/src/verax/observe/network-sensor.js +55 -7
  108. package/src/verax/observe/observed-expectation-deriver.js +186 -0
  109. package/src/verax/observe/observed-expectation.js +305 -0
  110. package/src/verax/observe/page-frontier.js +234 -0
  111. package/src/verax/observe/settle.js +38 -17
  112. package/src/verax/observe/state-sensor.js +393 -0
  113. package/src/verax/observe/state-ui-sensor.js +7 -1
  114. package/src/verax/observe/timing-sensor.js +228 -0
  115. package/src/verax/observe/traces-writer.js +73 -21
  116. package/src/verax/observe/ui-signal-sensor.js +143 -17
  117. package/src/verax/scan-summary-writer.js +80 -15
  118. package/src/verax/shared/artifact-manager.js +111 -9
  119. package/src/verax/shared/budget-profiles.js +136 -0
  120. package/src/verax/shared/caching.js +1 -1
  121. package/src/verax/shared/ci-detection.js +39 -0
  122. package/src/verax/shared/config-loader.js +169 -0
  123. package/src/verax/shared/dynamic-route-utils.js +224 -0
  124. package/src/verax/shared/expectation-coverage.js +44 -0
  125. package/src/verax/shared/expectation-prover.js +81 -0
  126. package/src/verax/shared/expectation-tracker.js +201 -0
  127. package/src/verax/shared/expectations-writer.js +60 -0
  128. package/src/verax/shared/first-run.js +44 -0
  129. package/src/verax/shared/progress-reporter.js +171 -0
  130. package/src/verax/shared/retry-policy.js +9 -1
  131. package/src/verax/shared/root-artifacts.js +49 -0
  132. package/src/verax/shared/scan-budget.js +86 -0
  133. package/src/verax/shared/url-normalizer.js +162 -0
  134. package/src/verax/shared/zip-artifacts.js +66 -0
  135. package/src/verax/validate/context-validator.js +244 -0
@@ -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 { initArtifactPaths, generateRunId } from './shared/artifact-manager.js';
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, runId = 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
- const validation = await validateRoutes(manifestForValidation, url);
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, artifactPaths);
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
- // For detect, we need tracesPath - use observation.tracesPath if available, otherwise use artifact paths
66
- let tracesPathForDetect = observation.tracesPath;
67
- if (artifactPaths && !tracesPathForDetect) {
68
- tracesPathForDetect = artifactPaths.traces;
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, tracesPathForDetect, validation, artifactPaths);
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
- artifactPaths
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
- return { manifest, observation, findings, scanSummary, validation };
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
+ }