@veraxhq/verax 0.1.0 → 0.2.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.
Files changed (126) hide show
  1. package/README.md +123 -88
  2. package/bin/verax.js +11 -452
  3. package/package.json +14 -36
  4. package/src/cli/commands/default.js +523 -0
  5. package/src/cli/commands/doctor.js +165 -0
  6. package/src/cli/commands/inspect.js +109 -0
  7. package/src/cli/commands/run.js +402 -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 +296 -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 +34 -0
  14. package/src/cli/util/expectation-extractor.js +378 -0
  15. package/src/cli/util/findings-writer.js +31 -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 +366 -0
  19. package/src/cli/util/observe-writer.js +25 -0
  20. package/src/cli/util/paths.js +29 -0
  21. package/src/cli/util/project-discovery.js +277 -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/summary-writer.js +32 -0
  26. package/src/verax/cli/ci-summary.js +35 -0
  27. package/src/verax/cli/context-explanation.js +89 -0
  28. package/src/verax/cli/doctor.js +277 -0
  29. package/src/verax/cli/error-normalizer.js +154 -0
  30. package/src/verax/cli/explain-output.js +105 -0
  31. package/src/verax/cli/finding-explainer.js +130 -0
  32. package/src/verax/cli/init.js +237 -0
  33. package/src/verax/cli/run-overview.js +163 -0
  34. package/src/verax/cli/url-safety.js +101 -0
  35. package/src/verax/cli/wizard.js +98 -0
  36. package/src/verax/cli/zero-findings-explainer.js +57 -0
  37. package/src/verax/cli/zero-interaction-explainer.js +127 -0
  38. package/src/verax/core/action-classifier.js +86 -0
  39. package/src/verax/core/budget-engine.js +218 -0
  40. package/src/verax/core/canonical-outcomes.js +157 -0
  41. package/src/verax/core/decision-snapshot.js +335 -0
  42. package/src/verax/core/determinism-model.js +403 -0
  43. package/src/verax/core/incremental-store.js +237 -0
  44. package/src/verax/core/invariants.js +356 -0
  45. package/src/verax/core/promise-model.js +230 -0
  46. package/src/verax/core/replay-validator.js +350 -0
  47. package/src/verax/core/replay.js +222 -0
  48. package/src/verax/core/run-id.js +175 -0
  49. package/src/verax/core/run-manifest.js +99 -0
  50. package/src/verax/core/silence-impact.js +369 -0
  51. package/src/verax/core/silence-model.js +521 -0
  52. package/src/verax/detect/comparison.js +2 -34
  53. package/src/verax/detect/confidence-engine.js +764 -329
  54. package/src/verax/detect/detection-engine.js +293 -0
  55. package/src/verax/detect/evidence-index.js +177 -0
  56. package/src/verax/detect/expectation-model.js +194 -172
  57. package/src/verax/detect/explanation-helpers.js +187 -0
  58. package/src/verax/detect/finding-detector.js +450 -0
  59. package/src/verax/detect/findings-writer.js +44 -8
  60. package/src/verax/detect/flow-detector.js +366 -0
  61. package/src/verax/detect/index.js +172 -286
  62. package/src/verax/detect/interactive-findings.js +613 -0
  63. package/src/verax/detect/signal-mapper.js +308 -0
  64. package/src/verax/detect/verdict-engine.js +563 -0
  65. package/src/verax/evidence-index-writer.js +61 -0
  66. package/src/verax/index.js +90 -14
  67. package/src/verax/intel/effect-detector.js +368 -0
  68. package/src/verax/intel/handler-mapper.js +249 -0
  69. package/src/verax/intel/index.js +281 -0
  70. package/src/verax/intel/route-extractor.js +280 -0
  71. package/src/verax/intel/ts-program.js +256 -0
  72. package/src/verax/intel/vue-navigation-extractor.js +579 -0
  73. package/src/verax/intel/vue-router-extractor.js +323 -0
  74. package/src/verax/learn/action-contract-extractor.js +335 -101
  75. package/src/verax/learn/ast-contract-extractor.js +95 -5
  76. package/src/verax/learn/flow-extractor.js +172 -0
  77. package/src/verax/learn/manifest-writer.js +97 -47
  78. package/src/verax/learn/project-detector.js +40 -0
  79. package/src/verax/learn/route-extractor.js +27 -96
  80. package/src/verax/learn/state-extractor.js +212 -0
  81. package/src/verax/learn/static-extractor-navigation.js +114 -0
  82. package/src/verax/learn/static-extractor-validation.js +88 -0
  83. package/src/verax/learn/static-extractor.js +112 -4
  84. package/src/verax/learn/truth-assessor.js +24 -21
  85. package/src/verax/observe/aria-sensor.js +211 -0
  86. package/src/verax/observe/browser.js +10 -5
  87. package/src/verax/observe/console-sensor.js +1 -17
  88. package/src/verax/observe/domain-boundary.js +10 -1
  89. package/src/verax/observe/expectation-executor.js +512 -0
  90. package/src/verax/observe/flow-matcher.js +143 -0
  91. package/src/verax/observe/focus-sensor.js +196 -0
  92. package/src/verax/observe/human-driver.js +643 -275
  93. package/src/verax/observe/index.js +908 -27
  94. package/src/verax/observe/index.js.backup +1 -0
  95. package/src/verax/observe/interaction-discovery.js +365 -14
  96. package/src/verax/observe/interaction-runner.js +563 -198
  97. package/src/verax/observe/loading-sensor.js +139 -0
  98. package/src/verax/observe/navigation-sensor.js +255 -0
  99. package/src/verax/observe/network-sensor.js +55 -7
  100. package/src/verax/observe/observed-expectation-deriver.js +186 -0
  101. package/src/verax/observe/observed-expectation.js +305 -0
  102. package/src/verax/observe/page-frontier.js +234 -0
  103. package/src/verax/observe/settle.js +37 -17
  104. package/src/verax/observe/state-sensor.js +389 -0
  105. package/src/verax/observe/timing-sensor.js +228 -0
  106. package/src/verax/observe/traces-writer.js +61 -20
  107. package/src/verax/observe/ui-signal-sensor.js +136 -17
  108. package/src/verax/scan-summary-writer.js +77 -15
  109. package/src/verax/shared/artifact-manager.js +110 -8
  110. package/src/verax/shared/budget-profiles.js +136 -0
  111. package/src/verax/shared/ci-detection.js +39 -0
  112. package/src/verax/shared/config-loader.js +170 -0
  113. package/src/verax/shared/dynamic-route-utils.js +218 -0
  114. package/src/verax/shared/expectation-coverage.js +44 -0
  115. package/src/verax/shared/expectation-prover.js +81 -0
  116. package/src/verax/shared/expectation-tracker.js +201 -0
  117. package/src/verax/shared/expectations-writer.js +60 -0
  118. package/src/verax/shared/first-run.js +44 -0
  119. package/src/verax/shared/progress-reporter.js +171 -0
  120. package/src/verax/shared/retry-policy.js +14 -1
  121. package/src/verax/shared/root-artifacts.js +49 -0
  122. package/src/verax/shared/scan-budget.js +86 -0
  123. package/src/verax/shared/url-normalizer.js +162 -0
  124. package/src/verax/shared/zip-artifacts.js +65 -0
  125. package/src/verax/validate/context-validator.js +244 -0
  126. package/src/verax/validate/context-validator.js.bak +0 -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 {
@@ -59,22 +62,54 @@ export async function scan(projectDir, url, manifestPath = null, runId = null) {
59
62
  manifest.learnTruth.warnings.push(...validation.warnings);
60
63
  }
61
64
 
65
+ // Use budget profile if no override provided
66
+ const scanBudget = scanBudgetOverride || createScanBudgetWithProfile();
67
+
68
+ // PHASE 5: Generate deterministic runId and create run manifest
69
+ const { getBaseOrigin } = await import('./observe/domain-boundary.js');
70
+ const baseOrigin = getBaseOrigin(url);
71
+ const runId = generateRunId({
72
+ url,
73
+ safetyFlags,
74
+ baseOrigin,
75
+ scanBudget,
76
+ manifestPath
77
+ });
78
+
79
+ // Create run manifest at start of execution
80
+ const runManifest = createRunManifest(projectDir, runId, {
81
+ url,
82
+ safetyFlags,
83
+ baseOrigin,
84
+ scanBudget,
85
+ manifestPath,
86
+ argv: process.argv
87
+ });
88
+
62
89
  const usedManifestPath = manifestPath || manifest.manifestPath;
63
- const observation = await observe(url, usedManifestPath, artifactPaths);
90
+ const observation = await observe(url, usedManifestPath, scanBudget, safetyFlags, projectDir, runId);
91
+
92
+ // Write a copy of the manifest into canonical run directory for replay integrity
93
+ try {
94
+ const { writeFileSync } = await import('fs');
95
+ const manifestCopyPath = getArtifactPath(projectDir, runId, 'manifest.json');
96
+ writeFileSync(manifestCopyPath, JSON.stringify(manifest, null, 2));
97
+ } catch {}
64
98
 
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;
99
+ // Create silence tracker from observation silences
100
+ const silenceTracker = new SilenceTracker();
101
+ if (observation.silences && observation.silences.entries) {
102
+ silenceTracker.recordBatch(observation.silences.entries);
69
103
  }
70
104
 
71
- const findings = await detect(usedManifestPath, tracesPathForDetect, validation, artifactPaths);
105
+ const findings = await detect(usedManifestPath, observation.tracesPath, validation, observation.expectationCoverageGaps || [], silenceTracker);
72
106
 
73
107
  const learnTruthWithValidation = {
74
108
  ...manifest.learnTruth,
75
109
  validation: validation
76
110
  };
77
111
 
112
+ const runDir = getRunArtifactDir(projectDir, runId);
78
113
  const scanSummary = writeScanSummary(
79
114
  projectDir,
80
115
  url,
@@ -85,13 +120,54 @@ export async function scan(projectDir, url, manifestPath = null, runId = null) {
85
120
  manifest.manifestPath,
86
121
  observation.tracesPath,
87
122
  findings.findingsPath,
88
- artifactPaths
123
+ runDir,
124
+ findings.findings // PHASE 7: Pass findings array for decision snapshot
89
125
  );
90
126
 
91
- return { manifest, observation, findings, scanSummary, validation };
127
+ // Compute observation summary from scan results (not a verdict)
128
+ // Pass observation object (which includes traces) to observation engine
129
+ const observeTruthWithTraces = {
130
+ ...observation.observeTruth,
131
+ traces: observation.traces || []
132
+ };
133
+ const observationSummary = computeObservationSummary(
134
+ findings.findings || [],
135
+ observeTruthWithTraces,
136
+ manifest.learnTruth,
137
+ findings.coverageGaps || [],
138
+ observation.observeTruth?.budgetExceeded,
139
+ findings.detectTruth, // Pass detectTruth for silence data
140
+ projectDir, // Pass projectDir for evidence validation
141
+ silenceTracker // Pass silenceTracker for evidence integrity tracking
142
+ );
143
+
144
+ // Write evidence index
145
+ const evidenceIndexPath = await writeEvidenceIndex(
146
+ projectDir,
147
+ observationSummary.evidenceIndex || [],
148
+ observation.tracesPath,
149
+ findings.findingsPath,
150
+ runDir
151
+ );
152
+ observationSummary.evidenceIndexPath = evidenceIndexPath;
153
+
154
+ // PHASE 5: Compute artifact hashes and update run manifest
155
+ const artifactHashes = computeArtifactHashes(projectDir, runId);
156
+ updateRunManifestHashes(projectDir, runId, artifactHashes);
157
+
158
+ return {
159
+ manifest,
160
+ observation,
161
+ findings,
162
+ scanSummary,
163
+ validation,
164
+ coverageGaps: findings.coverageGaps || [],
165
+ observationSummary,
166
+ runId,
167
+ runManifest
168
+ };
92
169
  }
93
170
 
94
171
  export { learn } from './learn/index.js';
95
172
  export { observe } from './observe/index.js';
96
173
  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 && ts.isFalseKeyword(expression)) {
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
+ }