@vibecheckai/cli 3.2.5 → 3.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 (197) hide show
  1. package/bin/.generated +25 -25
  2. package/bin/dev/run-v2-torture.js +30 -30
  3. package/bin/registry.js +192 -5
  4. package/bin/runners/lib/__tests__/entitlements-v2.test.js +295 -295
  5. package/bin/runners/lib/agent-firewall/change-packet/builder.js +280 -6
  6. package/bin/runners/lib/agent-firewall/critic/index.js +151 -0
  7. package/bin/runners/lib/agent-firewall/critic/judge.js +432 -0
  8. package/bin/runners/lib/agent-firewall/critic/prompts.js +305 -0
  9. package/bin/runners/lib/agent-firewall/lawbook/distributor.js +465 -0
  10. package/bin/runners/lib/agent-firewall/lawbook/evaluator.js +604 -0
  11. package/bin/runners/lib/agent-firewall/lawbook/index.js +304 -0
  12. package/bin/runners/lib/agent-firewall/lawbook/registry.js +514 -0
  13. package/bin/runners/lib/agent-firewall/lawbook/schema.js +420 -0
  14. package/bin/runners/lib/agent-firewall/logger.js +141 -0
  15. package/bin/runners/lib/agent-firewall/policy/loader.js +312 -4
  16. package/bin/runners/lib/agent-firewall/policy/rules/ghost-env.js +113 -1
  17. package/bin/runners/lib/agent-firewall/policy/rules/ghost-route.js +133 -6
  18. package/bin/runners/lib/agent-firewall/proposal/extractor.js +394 -0
  19. package/bin/runners/lib/agent-firewall/proposal/index.js +212 -0
  20. package/bin/runners/lib/agent-firewall/proposal/schema.js +251 -0
  21. package/bin/runners/lib/agent-firewall/proposal/validator.js +386 -0
  22. package/bin/runners/lib/agent-firewall/reality/index.js +332 -0
  23. package/bin/runners/lib/agent-firewall/reality/state.js +625 -0
  24. package/bin/runners/lib/agent-firewall/reality/watcher.js +322 -0
  25. package/bin/runners/lib/agent-firewall/risk/index.js +173 -0
  26. package/bin/runners/lib/agent-firewall/risk/scorer.js +328 -0
  27. package/bin/runners/lib/agent-firewall/risk/thresholds.js +321 -0
  28. package/bin/runners/lib/agent-firewall/risk/vectors.js +421 -0
  29. package/bin/runners/lib/agent-firewall/simulator/diff-simulator.js +472 -0
  30. package/bin/runners/lib/agent-firewall/simulator/import-resolver.js +346 -0
  31. package/bin/runners/lib/agent-firewall/simulator/index.js +181 -0
  32. package/bin/runners/lib/agent-firewall/simulator/route-validator.js +380 -0
  33. package/bin/runners/lib/agent-firewall/time-machine/incident-correlator.js +661 -0
  34. package/bin/runners/lib/agent-firewall/time-machine/index.js +267 -0
  35. package/bin/runners/lib/agent-firewall/time-machine/replay-engine.js +436 -0
  36. package/bin/runners/lib/agent-firewall/time-machine/state-reconstructor.js +490 -0
  37. package/bin/runners/lib/agent-firewall/time-machine/timeline-builder.js +530 -0
  38. package/bin/runners/lib/analyzers.js +81 -18
  39. package/bin/runners/lib/api-client.js +269 -0
  40. package/bin/runners/lib/auth-truth.js +193 -193
  41. package/bin/runners/lib/authority-badge.js +425 -0
  42. package/bin/runners/lib/backup.js +62 -62
  43. package/bin/runners/lib/billing.js +107 -107
  44. package/bin/runners/lib/claims.js +118 -118
  45. package/bin/runners/lib/cli-output.js +7 -1
  46. package/bin/runners/lib/cli-ui.js +540 -540
  47. package/bin/runners/lib/contracts/auth-contract.js +202 -202
  48. package/bin/runners/lib/contracts/env-contract.js +181 -181
  49. package/bin/runners/lib/contracts/external-contract.js +206 -206
  50. package/bin/runners/lib/contracts/guard.js +168 -168
  51. package/bin/runners/lib/contracts/index.js +89 -89
  52. package/bin/runners/lib/contracts/plan-validator.js +311 -311
  53. package/bin/runners/lib/contracts/route-contract.js +199 -199
  54. package/bin/runners/lib/contracts.js +804 -804
  55. package/bin/runners/lib/detect.js +89 -89
  56. package/bin/runners/lib/doctor/autofix.js +254 -254
  57. package/bin/runners/lib/doctor/index.js +37 -37
  58. package/bin/runners/lib/doctor/modules/dependencies.js +325 -325
  59. package/bin/runners/lib/doctor/modules/index.js +46 -46
  60. package/bin/runners/lib/doctor/modules/network.js +250 -250
  61. package/bin/runners/lib/doctor/modules/project.js +312 -312
  62. package/bin/runners/lib/doctor/modules/runtime.js +224 -224
  63. package/bin/runners/lib/doctor/modules/security.js +348 -348
  64. package/bin/runners/lib/doctor/modules/system.js +213 -213
  65. package/bin/runners/lib/doctor/modules/vibecheck.js +394 -394
  66. package/bin/runners/lib/doctor/reporter.js +262 -262
  67. package/bin/runners/lib/doctor/service.js +262 -262
  68. package/bin/runners/lib/doctor/types.js +113 -113
  69. package/bin/runners/lib/doctor/ui.js +263 -263
  70. package/bin/runners/lib/doctor-v2.js +608 -608
  71. package/bin/runners/lib/drift.js +425 -425
  72. package/bin/runners/lib/enforcement.js +72 -72
  73. package/bin/runners/lib/enterprise-detect.js +603 -603
  74. package/bin/runners/lib/enterprise-init.js +942 -942
  75. package/bin/runners/lib/env-resolver.js +417 -417
  76. package/bin/runners/lib/env-template.js +66 -66
  77. package/bin/runners/lib/env.js +189 -189
  78. package/bin/runners/lib/error-handler.js +16 -9
  79. package/bin/runners/lib/exit-codes.js +275 -0
  80. package/bin/runners/lib/extractors/client-calls.js +990 -990
  81. package/bin/runners/lib/extractors/fastify-route-dump.js +573 -573
  82. package/bin/runners/lib/extractors/fastify-routes.js +426 -426
  83. package/bin/runners/lib/extractors/index.js +363 -363
  84. package/bin/runners/lib/extractors/next-routes.js +524 -524
  85. package/bin/runners/lib/extractors/proof-graph.js +431 -431
  86. package/bin/runners/lib/extractors/route-matcher.js +451 -451
  87. package/bin/runners/lib/extractors/truthpack-v2.js +377 -377
  88. package/bin/runners/lib/extractors/ui-bindings.js +547 -547
  89. package/bin/runners/lib/findings-schema.js +281 -281
  90. package/bin/runners/lib/firewall-prompt.js +50 -50
  91. package/bin/runners/lib/global-flags.js +37 -0
  92. package/bin/runners/lib/graph/graph-builder.js +265 -265
  93. package/bin/runners/lib/graph/html-renderer.js +413 -413
  94. package/bin/runners/lib/graph/index.js +32 -32
  95. package/bin/runners/lib/graph/runtime-collector.js +215 -215
  96. package/bin/runners/lib/graph/static-extractor.js +518 -518
  97. package/bin/runners/lib/help-formatter.js +413 -0
  98. package/bin/runners/lib/html-report.js +650 -650
  99. package/bin/runners/lib/llm.js +75 -75
  100. package/bin/runners/lib/logger.js +38 -0
  101. package/bin/runners/lib/meter.js +61 -61
  102. package/bin/runners/lib/missions/evidence.js +126 -126
  103. package/bin/runners/lib/patch.js +40 -40
  104. package/bin/runners/lib/permissions/auth-model.js +213 -213
  105. package/bin/runners/lib/permissions/idor-prover.js +205 -205
  106. package/bin/runners/lib/permissions/index.js +45 -45
  107. package/bin/runners/lib/permissions/matrix-builder.js +198 -198
  108. package/bin/runners/lib/pkgjson.js +28 -28
  109. package/bin/runners/lib/policy.js +295 -295
  110. package/bin/runners/lib/preflight.js +142 -142
  111. package/bin/runners/lib/reality/correlation-detectors.js +359 -359
  112. package/bin/runners/lib/reality/index.js +318 -318
  113. package/bin/runners/lib/reality/request-hashing.js +416 -416
  114. package/bin/runners/lib/reality/request-mapper.js +453 -453
  115. package/bin/runners/lib/reality/safety-rails.js +463 -463
  116. package/bin/runners/lib/reality/semantic-snapshot.js +408 -408
  117. package/bin/runners/lib/reality/toast-detector.js +393 -393
  118. package/bin/runners/lib/reality-findings.js +84 -84
  119. package/bin/runners/lib/receipts.js +179 -179
  120. package/bin/runners/lib/redact.js +29 -29
  121. package/bin/runners/lib/replay/capsule-manager.js +154 -154
  122. package/bin/runners/lib/replay/index.js +263 -263
  123. package/bin/runners/lib/replay/player.js +348 -348
  124. package/bin/runners/lib/replay/recorder.js +331 -331
  125. package/bin/runners/lib/report.js +135 -135
  126. package/bin/runners/lib/route-detection.js +1140 -1140
  127. package/bin/runners/lib/sandbox/index.js +59 -59
  128. package/bin/runners/lib/sandbox/proof-chain.js +399 -399
  129. package/bin/runners/lib/sandbox/sandbox-runner.js +205 -205
  130. package/bin/runners/lib/sandbox/worktree.js +174 -174
  131. package/bin/runners/lib/schema-validator.js +350 -350
  132. package/bin/runners/lib/schemas/contracts.schema.json +160 -160
  133. package/bin/runners/lib/schemas/finding.schema.json +100 -100
  134. package/bin/runners/lib/schemas/mission-pack.schema.json +206 -206
  135. package/bin/runners/lib/schemas/proof-graph.schema.json +176 -176
  136. package/bin/runners/lib/schemas/reality-report.schema.json +162 -162
  137. package/bin/runners/lib/schemas/share-pack.schema.json +180 -180
  138. package/bin/runners/lib/schemas/ship-report.schema.json +117 -117
  139. package/bin/runners/lib/schemas/truthpack-v2.schema.json +303 -303
  140. package/bin/runners/lib/schemas/validator.js +438 -438
  141. package/bin/runners/lib/score-history.js +282 -282
  142. package/bin/runners/lib/share-pack.js +239 -239
  143. package/bin/runners/lib/snippets.js +67 -67
  144. package/bin/runners/lib/unified-cli-output.js +604 -0
  145. package/bin/runners/lib/upsell.js +658 -510
  146. package/bin/runners/lib/usage.js +153 -153
  147. package/bin/runners/lib/validate-patch.js +156 -156
  148. package/bin/runners/lib/verdict-engine.js +628 -628
  149. package/bin/runners/reality/engine.js +917 -917
  150. package/bin/runners/reality/flows.js +122 -122
  151. package/bin/runners/reality/report.js +378 -378
  152. package/bin/runners/reality/session.js +193 -193
  153. package/bin/runners/runAgent.d.ts +5 -0
  154. package/bin/runners/runApprove.js +1200 -0
  155. package/bin/runners/runAuth.js +324 -95
  156. package/bin/runners/runCheckpoint.js +39 -21
  157. package/bin/runners/runClassify.js +859 -0
  158. package/bin/runners/runContext.js +136 -24
  159. package/bin/runners/runDoctor.js +108 -68
  160. package/bin/runners/runFirewall.d.ts +5 -0
  161. package/bin/runners/runFirewallHook.d.ts +5 -0
  162. package/bin/runners/runFix.js +6 -5
  163. package/bin/runners/runGuard.js +262 -168
  164. package/bin/runners/runInit.js +3 -2
  165. package/bin/runners/runMcp.js +130 -52
  166. package/bin/runners/runPolish.js +43 -20
  167. package/bin/runners/runProve.js +1 -2
  168. package/bin/runners/runReport.js +3 -2
  169. package/bin/runners/runScan.js +145 -44
  170. package/bin/runners/runShip.js +3 -4
  171. package/bin/runners/runTruth.d.ts +5 -0
  172. package/bin/runners/runValidate.js +19 -2
  173. package/bin/runners/runWatch.js +104 -53
  174. package/bin/vibecheck.js +106 -19
  175. package/mcp-server/HARDENING_SUMMARY.md +299 -0
  176. package/mcp-server/agent-firewall-interceptor.js +367 -31
  177. package/mcp-server/authority-tools.js +569 -0
  178. package/mcp-server/conductor/conflict-resolver.js +588 -0
  179. package/mcp-server/conductor/execution-planner.js +544 -0
  180. package/mcp-server/conductor/index.js +377 -0
  181. package/mcp-server/conductor/lock-manager.js +615 -0
  182. package/mcp-server/conductor/request-queue.js +550 -0
  183. package/mcp-server/conductor/session-manager.js +500 -0
  184. package/mcp-server/conductor/tools.js +510 -0
  185. package/mcp-server/index.js +1199 -208
  186. package/mcp-server/lib/api-client.cjs +305 -0
  187. package/mcp-server/lib/logger.cjs +30 -0
  188. package/mcp-server/logger.js +173 -0
  189. package/mcp-server/package.json +2 -2
  190. package/mcp-server/premium-tools.js +2 -2
  191. package/mcp-server/tier-auth.js +351 -136
  192. package/mcp-server/tools/index.js +72 -72
  193. package/mcp-server/truth-firewall-tools.js +145 -15
  194. package/mcp-server/vibecheck-tools.js +2 -2
  195. package/package.json +2 -3
  196. package/mcp-server/index.old.js +0 -4137
  197. package/mcp-server/package-lock.json +0 -165
@@ -1,547 +1,547 @@
1
- /**
2
- * UI Bindings Extractor v2
3
- *
4
- * Extracts static UI event bindings from TSX/JSX for proof graph.
5
- * Detects onClick, onSubmit, form actions, useTransition, useFormState patterns.
6
- */
7
-
8
- "use strict";
9
-
10
- const fs = require("fs");
11
- const path = require("path");
12
- const crypto = require("crypto");
13
- const fg = require("fast-glob");
14
-
15
- let parser, traverse, t;
16
- try {
17
- parser = require("@babel/parser");
18
- traverse = require("@babel/traverse").default;
19
- t = require("@babel/types");
20
- } catch {
21
- parser = null;
22
- traverse = null;
23
- t = null;
24
- }
25
-
26
- // =============================================================================
27
- // CONSTANTS
28
- // =============================================================================
29
-
30
- const UI_EVENT_ATTRIBUTES = [
31
- "onClick", "onSubmit", "onBlur", "onChange", "onFocus",
32
- "onDoubleClick", "onMouseDown", "onMouseUp", "onKeyDown", "onKeyUp",
33
- ];
34
-
35
- const FORM_ACTION_PATTERNS = [
36
- "action", // <form action={serverAction}>
37
- "formAction", // <button formAction={...}>
38
- ];
39
-
40
- const TRANSITION_HOOKS = [
41
- "useTransition",
42
- "useFormState",
43
- "useActionState",
44
- "useFormStatus",
45
- ];
46
-
47
- const IGNORE_PATTERNS = [
48
- "**/node_modules/**",
49
- "**/.next/**",
50
- "**/dist/**",
51
- "**/build/**",
52
- "**/coverage/**",
53
- "**/*.d.ts",
54
- "**/*.min.js",
55
- ];
56
-
57
- // =============================================================================
58
- // EXTRACTION
59
- // =============================================================================
60
-
61
- /**
62
- * Extract all UI bindings from a project
63
- */
64
- function extractUIBindings(projectRoot, options = {}) {
65
- if (!parser || !traverse) {
66
- return { bindings: [], errors: [{ message: "Babel parser not available" }], stats: {} };
67
- }
68
-
69
- const {
70
- include = ["**/*.tsx", "**/*.jsx", "**/*.js"],
71
- exclude = IGNORE_PATTERNS,
72
- } = options;
73
-
74
- const files = fg.sync(include, {
75
- cwd: projectRoot,
76
- absolute: true,
77
- ignore: exclude,
78
- });
79
-
80
- const bindings = [];
81
- const errors = [];
82
- const transitionScopes = new Map(); // Track useTransition scopes
83
-
84
- for (const fileAbs of files) {
85
- try {
86
- const fileRel = path.relative(projectRoot, fileAbs).replace(/\\/g, "/");
87
- const code = fs.readFileSync(fileAbs, "utf8");
88
-
89
- const fileBindings = extractBindingsFromFile(code, fileAbs, fileRel, transitionScopes);
90
- bindings.push(...fileBindings);
91
- } catch (e) {
92
- errors.push({ file: fileAbs, message: e.message });
93
- }
94
- }
95
-
96
- return {
97
- bindings,
98
- errors,
99
- stats: {
100
- filesScanned: files.length,
101
- bindingsFound: bindings.length,
102
- byEvent: countByEvent(bindings),
103
- },
104
- };
105
- }
106
-
107
- /**
108
- * Extract bindings from a single file
109
- */
110
- function extractBindingsFromFile(code, fileAbs, fileRel, transitionScopes) {
111
- const bindings = [];
112
-
113
- let ast;
114
- try {
115
- ast = parser.parse(code, {
116
- sourceType: "unambiguous",
117
- plugins: ["typescript", "jsx"],
118
- errorRecovery: true,
119
- });
120
- } catch {
121
- return bindings;
122
- }
123
-
124
- // First pass: find useTransition and similar hooks
125
- const hookScopes = new Map();
126
-
127
- traverse(ast, {
128
- VariableDeclarator(p) {
129
- const init = p.node.init;
130
- if (!t.isCallExpression(init)) return;
131
-
132
- const callee = init.callee;
133
- let hookName = null;
134
-
135
- if (t.isIdentifier(callee) && TRANSITION_HOOKS.includes(callee.name)) {
136
- hookName = callee.name;
137
- }
138
-
139
- if (hookName && t.isArrayPattern(p.node.id)) {
140
- // const [isPending, startTransition] = useTransition()
141
- for (const el of p.node.id.elements) {
142
- if (t.isIdentifier(el)) {
143
- hookScopes.set(el.name, {
144
- hook: hookName,
145
- file: fileRel,
146
- line: p.node.loc?.start?.line,
147
- });
148
- }
149
- }
150
- }
151
- },
152
- });
153
-
154
- // Second pass: extract JSX event bindings
155
- traverse(ast, {
156
- JSXAttribute(p) {
157
- const attrName = p.node.name?.name;
158
- if (!attrName) return;
159
-
160
- // Check for event handlers
161
- if (UI_EVENT_ATTRIBUTES.includes(attrName)) {
162
- const binding = extractEventBinding(p, attrName, fileRel, hookScopes, code);
163
- if (binding) bindings.push(binding);
164
- }
165
-
166
- // Check for form actions
167
- if (FORM_ACTION_PATTERNS.includes(attrName)) {
168
- const binding = extractFormActionBinding(p, attrName, fileRel, code);
169
- if (binding) bindings.push(binding);
170
- }
171
- },
172
-
173
- // Track form submissions
174
- JSXOpeningElement(p) {
175
- const tagName = p.node.name?.name;
176
- if (tagName === "form") {
177
- const actionAttr = p.node.attributes.find(
178
- a => t.isJSXAttribute(a) && a.name?.name === "action"
179
- );
180
-
181
- if (actionAttr && t.isJSXExpressionContainer(actionAttr.value)) {
182
- // Server action form
183
- const binding = {
184
- bindingId: generateBindingId(fileRel, p.node.loc),
185
- file: fileRel,
186
- lines: getLines(p.node.loc),
187
- event: "formAction",
188
- elementType: "form",
189
- labelHint: extractLabelHint(p.parentPath, code),
190
- selectorHint: buildSelectorHint(p),
191
- handlerType: "serverAction",
192
- calls: [], // Will be linked later
193
- evidence: [{
194
- id: generateEvidenceId(fileRel, p.node.loc),
195
- kind: "file",
196
- file: fileRel,
197
- lines: getLines(p.node.loc),
198
- snippetHash: hashSnippet(getSnippet(code, p.node.loc)),
199
- reason: "Form with server action",
200
- }],
201
- };
202
- bindings.push(binding);
203
- }
204
- }
205
- },
206
- });
207
-
208
- return bindings;
209
- }
210
-
211
- /**
212
- * Extract event binding details
213
- */
214
- function extractEventBinding(attrPath, eventName, fileRel, hookScopes, code) {
215
- const value = attrPath.node.value;
216
- if (!value) return null;
217
-
218
- let handlerType = "unknown";
219
- let handlerName = null;
220
- let usesTransition = false;
221
- let transitionHook = null;
222
-
223
- if (t.isJSXExpressionContainer(value)) {
224
- const expr = value.expression;
225
-
226
- // onClick={handleClick}
227
- if (t.isIdentifier(expr)) {
228
- handlerName = expr.name;
229
- handlerType = "function";
230
-
231
- // Check if handler uses a transition hook
232
- if (hookScopes.has(expr.name)) {
233
- usesTransition = true;
234
- transitionHook = hookScopes.get(expr.name).hook;
235
- }
236
- }
237
- // onClick={() => handleClick()}
238
- else if (t.isArrowFunctionExpression(expr) || t.isFunctionExpression(expr)) {
239
- handlerType = "inline";
240
-
241
- // Check body for transition calls
242
- const body = expr.body;
243
- if (t.isCallExpression(body) && t.isIdentifier(body.callee)) {
244
- handlerName = body.callee.name;
245
- if (hookScopes.has(body.callee.name)) {
246
- usesTransition = true;
247
- transitionHook = hookScopes.get(body.callee.name).hook;
248
- }
249
- }
250
- }
251
- // onClick={mutation.mutate}
252
- else if (t.isMemberExpression(expr)) {
253
- handlerType = "method";
254
- if (t.isIdentifier(expr.object) && t.isIdentifier(expr.property)) {
255
- handlerName = `${expr.object.name}.${expr.property.name}`;
256
- }
257
- }
258
- }
259
-
260
- // Get parent element info
261
- const elementPath = attrPath.parentPath;
262
- const elementType = getElementType(elementPath);
263
- const labelHint = extractLabelHint(elementPath, code);
264
- const selectorHint = buildSelectorHint(elementPath);
265
-
266
- const loc = attrPath.node.loc;
267
-
268
- return {
269
- bindingId: generateBindingId(fileRel, loc),
270
- file: fileRel,
271
- lines: getLines(loc),
272
- event: eventName,
273
- elementType,
274
- labelHint,
275
- selectorHint,
276
- handlerType,
277
- handlerName,
278
- usesTransition,
279
- transitionHook,
280
- calls: [], // Will be linked by call extractor
281
- evidence: [{
282
- id: generateEvidenceId(fileRel, loc),
283
- kind: "file",
284
- file: fileRel,
285
- lines: getLines(loc),
286
- snippetHash: hashSnippet(getSnippet(code, loc)),
287
- reason: `${eventName} handler on ${elementType}`,
288
- }],
289
- };
290
- }
291
-
292
- /**
293
- * Extract form action binding
294
- */
295
- function extractFormActionBinding(attrPath, attrName, fileRel, code) {
296
- const value = attrPath.node.value;
297
- if (!t.isJSXExpressionContainer(value)) return null;
298
-
299
- const expr = value.expression;
300
- let actionName = null;
301
- let handlerType = "serverAction";
302
-
303
- if (t.isIdentifier(expr)) {
304
- actionName = expr.name;
305
- } else if (t.isMemberExpression(expr) && t.isIdentifier(expr.property)) {
306
- actionName = expr.property.name;
307
- }
308
-
309
- const elementPath = attrPath.parentPath;
310
- const elementType = getElementType(elementPath);
311
- const labelHint = extractLabelHint(elementPath, code);
312
- const loc = attrPath.node.loc;
313
-
314
- return {
315
- bindingId: generateBindingId(fileRel, loc),
316
- file: fileRel,
317
- lines: getLines(loc),
318
- event: "formAction",
319
- elementType,
320
- labelHint,
321
- selectorHint: buildSelectorHint(elementPath),
322
- handlerType,
323
- handlerName: actionName,
324
- calls: [],
325
- evidence: [{
326
- id: generateEvidenceId(fileRel, loc),
327
- kind: "file",
328
- file: fileRel,
329
- lines: getLines(loc),
330
- snippetHash: hashSnippet(getSnippet(code, loc)),
331
- reason: `Server action ${attrName}`,
332
- }],
333
- };
334
- }
335
-
336
- // =============================================================================
337
- // HELPERS
338
- // =============================================================================
339
-
340
- function getElementType(elementPath) {
341
- if (!elementPath || !elementPath.node) return "unknown";
342
-
343
- const opening = elementPath.node.openingElement || elementPath.node;
344
- if (!opening || !opening.name) return "unknown";
345
-
346
- if (t.isJSXIdentifier(opening.name)) {
347
- return opening.name.name;
348
- }
349
- return "unknown";
350
- }
351
-
352
- function extractLabelHint(elementPath, code) {
353
- if (!elementPath || !elementPath.node) return null;
354
-
355
- const opening = elementPath.node.openingElement || elementPath.node;
356
- if (!opening || !opening.attributes) return null;
357
-
358
- // Try aria-label
359
- for (const attr of opening.attributes) {
360
- if (t.isJSXAttribute(attr) && attr.name?.name === "aria-label") {
361
- if (t.isStringLiteral(attr.value)) {
362
- return attr.value.value;
363
- }
364
- }
365
- }
366
-
367
- // Try children text content
368
- const children = elementPath.node.children;
369
- if (children) {
370
- for (const child of children) {
371
- if (t.isJSXText(child)) {
372
- const text = child.value.trim();
373
- if (text && text.length < 50) return text;
374
- }
375
- if (t.isJSXExpressionContainer(child) && t.isStringLiteral(child.expression)) {
376
- return child.expression.value;
377
- }
378
- }
379
- }
380
-
381
- // Try title attribute
382
- for (const attr of opening.attributes) {
383
- if (t.isJSXAttribute(attr) && attr.name?.name === "title") {
384
- if (t.isStringLiteral(attr.value)) {
385
- return attr.value.value;
386
- }
387
- }
388
- }
389
-
390
- return null;
391
- }
392
-
393
- function buildSelectorHint(elementPath) {
394
- if (!elementPath || !elementPath.node) return null;
395
-
396
- const opening = elementPath.node.openingElement || elementPath.node;
397
- if (!opening || !opening.attributes) return null;
398
-
399
- let tagName = "unknown";
400
- if (opening.name && t.isJSXIdentifier(opening.name)) {
401
- tagName = opening.name.name.toLowerCase();
402
- }
403
-
404
- // Check for id
405
- for (const attr of opening.attributes) {
406
- if (t.isJSXAttribute(attr) && attr.name?.name === "id") {
407
- if (t.isStringLiteral(attr.value)) {
408
- return `#${attr.value.value}`;
409
- }
410
- }
411
- }
412
-
413
- // Check for data-testid
414
- for (const attr of opening.attributes) {
415
- if (t.isJSXAttribute(attr) && (attr.name?.name === "data-testid" || attr.name?.name === "data-test-id")) {
416
- if (t.isStringLiteral(attr.value)) {
417
- return `[data-testid="${attr.value.value}"]`;
418
- }
419
- }
420
- }
421
-
422
- // Check for className
423
- for (const attr of opening.attributes) {
424
- if (t.isJSXAttribute(attr) && (attr.name?.name === "className" || attr.name?.name === "class")) {
425
- if (t.isStringLiteral(attr.value)) {
426
- const firstClass = attr.value.value.split(/\s+/)[0];
427
- if (firstClass) return `${tagName}.${firstClass}`;
428
- }
429
- }
430
- }
431
-
432
- // Check for type on buttons
433
- if (tagName === "button" || tagName === "input") {
434
- for (const attr of opening.attributes) {
435
- if (t.isJSXAttribute(attr) && attr.name?.name === "type") {
436
- if (t.isStringLiteral(attr.value)) {
437
- return `${tagName}[type="${attr.value.value}"]`;
438
- }
439
- }
440
- }
441
- }
442
-
443
- return tagName;
444
- }
445
-
446
- function getLines(loc) {
447
- if (!loc) return "1-1";
448
- const start = loc.start?.line || 1;
449
- const end = loc.end?.line || start;
450
- return `${start}-${end}`;
451
- }
452
-
453
- function getSnippet(code, loc) {
454
- if (!loc) return "";
455
- const lines = code.split(/\r?\n/);
456
- const start = Math.max(1, loc.start?.line || 1);
457
- const end = Math.min(lines.length, loc.end?.line || start);
458
- return lines.slice(start - 1, end).join("\n");
459
- }
460
-
461
- function hashSnippet(snippet) {
462
- return "sha256:" + crypto.createHash("sha256").update(snippet).digest("hex").slice(0, 16);
463
- }
464
-
465
- function generateBindingId(file, loc) {
466
- const hash = crypto.createHash("sha256")
467
- .update(`${file}:${loc?.start?.line || 0}:${loc?.start?.column || 0}`)
468
- .digest("hex")
469
- .slice(0, 8)
470
- .toUpperCase();
471
- return `UIB_${hash}`;
472
- }
473
-
474
- function generateEvidenceId(file, loc) {
475
- const hash = crypto.createHash("sha256")
476
- .update(`${file}:${loc?.start?.line || 0}`)
477
- .digest("hex")
478
- .slice(0, 12)
479
- .toUpperCase();
480
- return `E_${hash}`;
481
- }
482
-
483
- function countByEvent(bindings) {
484
- const counts = {};
485
- for (const b of bindings) {
486
- counts[b.event] = (counts[b.event] || 0) + 1;
487
- }
488
- return counts;
489
- }
490
-
491
- // =============================================================================
492
- // LINKING
493
- // =============================================================================
494
-
495
- /**
496
- * Link UI bindings to client calls
497
- * Called after both extractions complete
498
- */
499
- function linkBindingsToClientCalls(bindings, clientCalls, options = {}) {
500
- const { projectRoot = "" } = options;
501
-
502
- // Index client calls by file
503
- const callsByFile = new Map();
504
- for (const call of clientCalls) {
505
- for (const ev of call.evidence || []) {
506
- if (ev.file) {
507
- if (!callsByFile.has(ev.file)) {
508
- callsByFile.set(ev.file, []);
509
- }
510
- callsByFile.get(ev.file).push(call);
511
- }
512
- }
513
- }
514
-
515
- // Link bindings to calls in same file (heuristic: same function scope)
516
- for (const binding of bindings) {
517
- const fileCalls = callsByFile.get(binding.file) || [];
518
-
519
- // Simple heuristic: calls in nearby lines
520
- const bindingLine = parseInt(binding.lines.split("-")[0], 10);
521
- const nearbyCalls = fileCalls.filter(call => {
522
- for (const ev of call.evidence || []) {
523
- if (ev.file === binding.file && ev.lines) {
524
- const callLine = parseInt(ev.lines.split("-")[0], 10);
525
- // Within 50 lines - rough heuristic for same function
526
- if (Math.abs(callLine - bindingLine) < 50) {
527
- return true;
528
- }
529
- }
530
- }
531
- return false;
532
- });
533
-
534
- binding.calls = nearbyCalls.map(c => c.id);
535
- }
536
-
537
- return bindings;
538
- }
539
-
540
- module.exports = {
541
- extractUIBindings,
542
- extractBindingsFromFile,
543
- linkBindingsToClientCalls,
544
- UI_EVENT_ATTRIBUTES,
545
- FORM_ACTION_PATTERNS,
546
- TRANSITION_HOOKS,
547
- };
1
+ /**
2
+ * UI Bindings Extractor v2
3
+ *
4
+ * Extracts static UI event bindings from TSX/JSX for proof graph.
5
+ * Detects onClick, onSubmit, form actions, useTransition, useFormState patterns.
6
+ */
7
+
8
+ "use strict";
9
+
10
+ const fs = require("fs");
11
+ const path = require("path");
12
+ const crypto = require("crypto");
13
+ const fg = require("fast-glob");
14
+
15
+ let parser, traverse, t;
16
+ try {
17
+ parser = require("@babel/parser");
18
+ traverse = require("@babel/traverse").default;
19
+ t = require("@babel/types");
20
+ } catch {
21
+ parser = null;
22
+ traverse = null;
23
+ t = null;
24
+ }
25
+
26
+ // =============================================================================
27
+ // CONSTANTS
28
+ // =============================================================================
29
+
30
+ const UI_EVENT_ATTRIBUTES = [
31
+ "onClick", "onSubmit", "onBlur", "onChange", "onFocus",
32
+ "onDoubleClick", "onMouseDown", "onMouseUp", "onKeyDown", "onKeyUp",
33
+ ];
34
+
35
+ const FORM_ACTION_PATTERNS = [
36
+ "action", // <form action={serverAction}>
37
+ "formAction", // <button formAction={...}>
38
+ ];
39
+
40
+ const TRANSITION_HOOKS = [
41
+ "useTransition",
42
+ "useFormState",
43
+ "useActionState",
44
+ "useFormStatus",
45
+ ];
46
+
47
+ const IGNORE_PATTERNS = [
48
+ "**/node_modules/**",
49
+ "**/.next/**",
50
+ "**/dist/**",
51
+ "**/build/**",
52
+ "**/coverage/**",
53
+ "**/*.d.ts",
54
+ "**/*.min.js",
55
+ ];
56
+
57
+ // =============================================================================
58
+ // EXTRACTION
59
+ // =============================================================================
60
+
61
+ /**
62
+ * Extract all UI bindings from a project
63
+ */
64
+ function extractUIBindings(projectRoot, options = {}) {
65
+ if (!parser || !traverse) {
66
+ return { bindings: [], errors: [{ message: "Babel parser not available" }], stats: {} };
67
+ }
68
+
69
+ const {
70
+ include = ["**/*.tsx", "**/*.jsx", "**/*.js"],
71
+ exclude = IGNORE_PATTERNS,
72
+ } = options;
73
+
74
+ const files = fg.sync(include, {
75
+ cwd: projectRoot,
76
+ absolute: true,
77
+ ignore: exclude,
78
+ });
79
+
80
+ const bindings = [];
81
+ const errors = [];
82
+ const transitionScopes = new Map(); // Track useTransition scopes
83
+
84
+ for (const fileAbs of files) {
85
+ try {
86
+ const fileRel = path.relative(projectRoot, fileAbs).replace(/\\/g, "/");
87
+ const code = fs.readFileSync(fileAbs, "utf8");
88
+
89
+ const fileBindings = extractBindingsFromFile(code, fileAbs, fileRel, transitionScopes);
90
+ bindings.push(...fileBindings);
91
+ } catch (e) {
92
+ errors.push({ file: fileAbs, message: e.message });
93
+ }
94
+ }
95
+
96
+ return {
97
+ bindings,
98
+ errors,
99
+ stats: {
100
+ filesScanned: files.length,
101
+ bindingsFound: bindings.length,
102
+ byEvent: countByEvent(bindings),
103
+ },
104
+ };
105
+ }
106
+
107
+ /**
108
+ * Extract bindings from a single file
109
+ */
110
+ function extractBindingsFromFile(code, fileAbs, fileRel, transitionScopes) {
111
+ const bindings = [];
112
+
113
+ let ast;
114
+ try {
115
+ ast = parser.parse(code, {
116
+ sourceType: "unambiguous",
117
+ plugins: ["typescript", "jsx"],
118
+ errorRecovery: true,
119
+ });
120
+ } catch {
121
+ return bindings;
122
+ }
123
+
124
+ // First pass: find useTransition and similar hooks
125
+ const hookScopes = new Map();
126
+
127
+ traverse(ast, {
128
+ VariableDeclarator(p) {
129
+ const init = p.node.init;
130
+ if (!t.isCallExpression(init)) return;
131
+
132
+ const callee = init.callee;
133
+ let hookName = null;
134
+
135
+ if (t.isIdentifier(callee) && TRANSITION_HOOKS.includes(callee.name)) {
136
+ hookName = callee.name;
137
+ }
138
+
139
+ if (hookName && t.isArrayPattern(p.node.id)) {
140
+ // const [isPending, startTransition] = useTransition()
141
+ for (const el of p.node.id.elements) {
142
+ if (t.isIdentifier(el)) {
143
+ hookScopes.set(el.name, {
144
+ hook: hookName,
145
+ file: fileRel,
146
+ line: p.node.loc?.start?.line,
147
+ });
148
+ }
149
+ }
150
+ }
151
+ },
152
+ });
153
+
154
+ // Second pass: extract JSX event bindings
155
+ traverse(ast, {
156
+ JSXAttribute(p) {
157
+ const attrName = p.node.name?.name;
158
+ if (!attrName) return;
159
+
160
+ // Check for event handlers
161
+ if (UI_EVENT_ATTRIBUTES.includes(attrName)) {
162
+ const binding = extractEventBinding(p, attrName, fileRel, hookScopes, code);
163
+ if (binding) bindings.push(binding);
164
+ }
165
+
166
+ // Check for form actions
167
+ if (FORM_ACTION_PATTERNS.includes(attrName)) {
168
+ const binding = extractFormActionBinding(p, attrName, fileRel, code);
169
+ if (binding) bindings.push(binding);
170
+ }
171
+ },
172
+
173
+ // Track form submissions
174
+ JSXOpeningElement(p) {
175
+ const tagName = p.node.name?.name;
176
+ if (tagName === "form") {
177
+ const actionAttr = p.node.attributes.find(
178
+ a => t.isJSXAttribute(a) && a.name?.name === "action"
179
+ );
180
+
181
+ if (actionAttr && t.isJSXExpressionContainer(actionAttr.value)) {
182
+ // Server action form
183
+ const binding = {
184
+ bindingId: generateBindingId(fileRel, p.node.loc),
185
+ file: fileRel,
186
+ lines: getLines(p.node.loc),
187
+ event: "formAction",
188
+ elementType: "form",
189
+ labelHint: extractLabelHint(p.parentPath, code),
190
+ selectorHint: buildSelectorHint(p),
191
+ handlerType: "serverAction",
192
+ calls: [], // Will be linked later
193
+ evidence: [{
194
+ id: generateEvidenceId(fileRel, p.node.loc),
195
+ kind: "file",
196
+ file: fileRel,
197
+ lines: getLines(p.node.loc),
198
+ snippetHash: hashSnippet(getSnippet(code, p.node.loc)),
199
+ reason: "Form with server action",
200
+ }],
201
+ };
202
+ bindings.push(binding);
203
+ }
204
+ }
205
+ },
206
+ });
207
+
208
+ return bindings;
209
+ }
210
+
211
+ /**
212
+ * Extract event binding details
213
+ */
214
+ function extractEventBinding(attrPath, eventName, fileRel, hookScopes, code) {
215
+ const value = attrPath.node.value;
216
+ if (!value) return null;
217
+
218
+ let handlerType = "unknown";
219
+ let handlerName = null;
220
+ let usesTransition = false;
221
+ let transitionHook = null;
222
+
223
+ if (t.isJSXExpressionContainer(value)) {
224
+ const expr = value.expression;
225
+
226
+ // onClick={handleClick}
227
+ if (t.isIdentifier(expr)) {
228
+ handlerName = expr.name;
229
+ handlerType = "function";
230
+
231
+ // Check if handler uses a transition hook
232
+ if (hookScopes.has(expr.name)) {
233
+ usesTransition = true;
234
+ transitionHook = hookScopes.get(expr.name).hook;
235
+ }
236
+ }
237
+ // onClick={() => handleClick()}
238
+ else if (t.isArrowFunctionExpression(expr) || t.isFunctionExpression(expr)) {
239
+ handlerType = "inline";
240
+
241
+ // Check body for transition calls
242
+ const body = expr.body;
243
+ if (t.isCallExpression(body) && t.isIdentifier(body.callee)) {
244
+ handlerName = body.callee.name;
245
+ if (hookScopes.has(body.callee.name)) {
246
+ usesTransition = true;
247
+ transitionHook = hookScopes.get(body.callee.name).hook;
248
+ }
249
+ }
250
+ }
251
+ // onClick={mutation.mutate}
252
+ else if (t.isMemberExpression(expr)) {
253
+ handlerType = "method";
254
+ if (t.isIdentifier(expr.object) && t.isIdentifier(expr.property)) {
255
+ handlerName = `${expr.object.name}.${expr.property.name}`;
256
+ }
257
+ }
258
+ }
259
+
260
+ // Get parent element info
261
+ const elementPath = attrPath.parentPath;
262
+ const elementType = getElementType(elementPath);
263
+ const labelHint = extractLabelHint(elementPath, code);
264
+ const selectorHint = buildSelectorHint(elementPath);
265
+
266
+ const loc = attrPath.node.loc;
267
+
268
+ return {
269
+ bindingId: generateBindingId(fileRel, loc),
270
+ file: fileRel,
271
+ lines: getLines(loc),
272
+ event: eventName,
273
+ elementType,
274
+ labelHint,
275
+ selectorHint,
276
+ handlerType,
277
+ handlerName,
278
+ usesTransition,
279
+ transitionHook,
280
+ calls: [], // Will be linked by call extractor
281
+ evidence: [{
282
+ id: generateEvidenceId(fileRel, loc),
283
+ kind: "file",
284
+ file: fileRel,
285
+ lines: getLines(loc),
286
+ snippetHash: hashSnippet(getSnippet(code, loc)),
287
+ reason: `${eventName} handler on ${elementType}`,
288
+ }],
289
+ };
290
+ }
291
+
292
+ /**
293
+ * Extract form action binding
294
+ */
295
+ function extractFormActionBinding(attrPath, attrName, fileRel, code) {
296
+ const value = attrPath.node.value;
297
+ if (!t.isJSXExpressionContainer(value)) return null;
298
+
299
+ const expr = value.expression;
300
+ let actionName = null;
301
+ let handlerType = "serverAction";
302
+
303
+ if (t.isIdentifier(expr)) {
304
+ actionName = expr.name;
305
+ } else if (t.isMemberExpression(expr) && t.isIdentifier(expr.property)) {
306
+ actionName = expr.property.name;
307
+ }
308
+
309
+ const elementPath = attrPath.parentPath;
310
+ const elementType = getElementType(elementPath);
311
+ const labelHint = extractLabelHint(elementPath, code);
312
+ const loc = attrPath.node.loc;
313
+
314
+ return {
315
+ bindingId: generateBindingId(fileRel, loc),
316
+ file: fileRel,
317
+ lines: getLines(loc),
318
+ event: "formAction",
319
+ elementType,
320
+ labelHint,
321
+ selectorHint: buildSelectorHint(elementPath),
322
+ handlerType,
323
+ handlerName: actionName,
324
+ calls: [],
325
+ evidence: [{
326
+ id: generateEvidenceId(fileRel, loc),
327
+ kind: "file",
328
+ file: fileRel,
329
+ lines: getLines(loc),
330
+ snippetHash: hashSnippet(getSnippet(code, loc)),
331
+ reason: `Server action ${attrName}`,
332
+ }],
333
+ };
334
+ }
335
+
336
+ // =============================================================================
337
+ // HELPERS
338
+ // =============================================================================
339
+
340
+ function getElementType(elementPath) {
341
+ if (!elementPath || !elementPath.node) return "unknown";
342
+
343
+ const opening = elementPath.node.openingElement || elementPath.node;
344
+ if (!opening || !opening.name) return "unknown";
345
+
346
+ if (t.isJSXIdentifier(opening.name)) {
347
+ return opening.name.name;
348
+ }
349
+ return "unknown";
350
+ }
351
+
352
+ function extractLabelHint(elementPath, code) {
353
+ if (!elementPath || !elementPath.node) return null;
354
+
355
+ const opening = elementPath.node.openingElement || elementPath.node;
356
+ if (!opening || !opening.attributes) return null;
357
+
358
+ // Try aria-label
359
+ for (const attr of opening.attributes) {
360
+ if (t.isJSXAttribute(attr) && attr.name?.name === "aria-label") {
361
+ if (t.isStringLiteral(attr.value)) {
362
+ return attr.value.value;
363
+ }
364
+ }
365
+ }
366
+
367
+ // Try children text content
368
+ const children = elementPath.node.children;
369
+ if (children) {
370
+ for (const child of children) {
371
+ if (t.isJSXText(child)) {
372
+ const text = child.value.trim();
373
+ if (text && text.length < 50) return text;
374
+ }
375
+ if (t.isJSXExpressionContainer(child) && t.isStringLiteral(child.expression)) {
376
+ return child.expression.value;
377
+ }
378
+ }
379
+ }
380
+
381
+ // Try title attribute
382
+ for (const attr of opening.attributes) {
383
+ if (t.isJSXAttribute(attr) && attr.name?.name === "title") {
384
+ if (t.isStringLiteral(attr.value)) {
385
+ return attr.value.value;
386
+ }
387
+ }
388
+ }
389
+
390
+ return null;
391
+ }
392
+
393
+ function buildSelectorHint(elementPath) {
394
+ if (!elementPath || !elementPath.node) return null;
395
+
396
+ const opening = elementPath.node.openingElement || elementPath.node;
397
+ if (!opening || !opening.attributes) return null;
398
+
399
+ let tagName = "unknown";
400
+ if (opening.name && t.isJSXIdentifier(opening.name)) {
401
+ tagName = opening.name.name.toLowerCase();
402
+ }
403
+
404
+ // Check for id
405
+ for (const attr of opening.attributes) {
406
+ if (t.isJSXAttribute(attr) && attr.name?.name === "id") {
407
+ if (t.isStringLiteral(attr.value)) {
408
+ return `#${attr.value.value}`;
409
+ }
410
+ }
411
+ }
412
+
413
+ // Check for data-testid
414
+ for (const attr of opening.attributes) {
415
+ if (t.isJSXAttribute(attr) && (attr.name?.name === "data-testid" || attr.name?.name === "data-test-id")) {
416
+ if (t.isStringLiteral(attr.value)) {
417
+ return `[data-testid="${attr.value.value}"]`;
418
+ }
419
+ }
420
+ }
421
+
422
+ // Check for className
423
+ for (const attr of opening.attributes) {
424
+ if (t.isJSXAttribute(attr) && (attr.name?.name === "className" || attr.name?.name === "class")) {
425
+ if (t.isStringLiteral(attr.value)) {
426
+ const firstClass = attr.value.value.split(/\s+/)[0];
427
+ if (firstClass) return `${tagName}.${firstClass}`;
428
+ }
429
+ }
430
+ }
431
+
432
+ // Check for type on buttons
433
+ if (tagName === "button" || tagName === "input") {
434
+ for (const attr of opening.attributes) {
435
+ if (t.isJSXAttribute(attr) && attr.name?.name === "type") {
436
+ if (t.isStringLiteral(attr.value)) {
437
+ return `${tagName}[type="${attr.value.value}"]`;
438
+ }
439
+ }
440
+ }
441
+ }
442
+
443
+ return tagName;
444
+ }
445
+
446
+ function getLines(loc) {
447
+ if (!loc) return "1-1";
448
+ const start = loc.start?.line || 1;
449
+ const end = loc.end?.line || start;
450
+ return `${start}-${end}`;
451
+ }
452
+
453
+ function getSnippet(code, loc) {
454
+ if (!loc) return "";
455
+ const lines = code.split(/\r?\n/);
456
+ const start = Math.max(1, loc.start?.line || 1);
457
+ const end = Math.min(lines.length, loc.end?.line || start);
458
+ return lines.slice(start - 1, end).join("\n");
459
+ }
460
+
461
+ function hashSnippet(snippet) {
462
+ return "sha256:" + crypto.createHash("sha256").update(snippet).digest("hex").slice(0, 16);
463
+ }
464
+
465
+ function generateBindingId(file, loc) {
466
+ const hash = crypto.createHash("sha256")
467
+ .update(`${file}:${loc?.start?.line || 0}:${loc?.start?.column || 0}`)
468
+ .digest("hex")
469
+ .slice(0, 8)
470
+ .toUpperCase();
471
+ return `UIB_${hash}`;
472
+ }
473
+
474
+ function generateEvidenceId(file, loc) {
475
+ const hash = crypto.createHash("sha256")
476
+ .update(`${file}:${loc?.start?.line || 0}`)
477
+ .digest("hex")
478
+ .slice(0, 12)
479
+ .toUpperCase();
480
+ return `E_${hash}`;
481
+ }
482
+
483
+ function countByEvent(bindings) {
484
+ const counts = {};
485
+ for (const b of bindings) {
486
+ counts[b.event] = (counts[b.event] || 0) + 1;
487
+ }
488
+ return counts;
489
+ }
490
+
491
+ // =============================================================================
492
+ // LINKING
493
+ // =============================================================================
494
+
495
+ /**
496
+ * Link UI bindings to client calls
497
+ * Called after both extractions complete
498
+ */
499
+ function linkBindingsToClientCalls(bindings, clientCalls, options = {}) {
500
+ const { projectRoot = "" } = options;
501
+
502
+ // Index client calls by file
503
+ const callsByFile = new Map();
504
+ for (const call of clientCalls) {
505
+ for (const ev of call.evidence || []) {
506
+ if (ev.file) {
507
+ if (!callsByFile.has(ev.file)) {
508
+ callsByFile.set(ev.file, []);
509
+ }
510
+ callsByFile.get(ev.file).push(call);
511
+ }
512
+ }
513
+ }
514
+
515
+ // Link bindings to calls in same file (heuristic: same function scope)
516
+ for (const binding of bindings) {
517
+ const fileCalls = callsByFile.get(binding.file) || [];
518
+
519
+ // Simple heuristic: calls in nearby lines
520
+ const bindingLine = parseInt(binding.lines.split("-")[0], 10);
521
+ const nearbyCalls = fileCalls.filter(call => {
522
+ for (const ev of call.evidence || []) {
523
+ if (ev.file === binding.file && ev.lines) {
524
+ const callLine = parseInt(ev.lines.split("-")[0], 10);
525
+ // Within 50 lines - rough heuristic for same function
526
+ if (Math.abs(callLine - bindingLine) < 50) {
527
+ return true;
528
+ }
529
+ }
530
+ }
531
+ return false;
532
+ });
533
+
534
+ binding.calls = nearbyCalls.map(c => c.id);
535
+ }
536
+
537
+ return bindings;
538
+ }
539
+
540
+ module.exports = {
541
+ extractUIBindings,
542
+ extractBindingsFromFile,
543
+ linkBindingsToClientCalls,
544
+ UI_EVENT_ATTRIBUTES,
545
+ FORM_ACTION_PATTERNS,
546
+ TRANSITION_HOOKS,
547
+ };