@veraxhq/verax 0.2.1 → 0.3.0

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