@vibecheckai/cli 3.1.0 → 3.1.2

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 (160) hide show
  1. package/bin/.generated +25 -25
  2. package/bin/dev/run-v2-torture.js +30 -30
  3. package/bin/registry.js +105 -105
  4. package/bin/runners/lib/__tests__/entitlements-v2.test.js +295 -295
  5. package/bin/runners/lib/analysis-core.js +271 -271
  6. package/bin/runners/lib/analyzers.js +579 -579
  7. package/bin/runners/lib/auth-truth.js +193 -193
  8. package/bin/runners/lib/backup.js +62 -62
  9. package/bin/runners/lib/billing.js +107 -107
  10. package/bin/runners/lib/claims.js +118 -118
  11. package/bin/runners/lib/cli-output.js +368 -368
  12. package/bin/runners/lib/cli-ui.js +540 -540
  13. package/bin/runners/lib/contracts/auth-contract.js +202 -202
  14. package/bin/runners/lib/contracts/env-contract.js +181 -181
  15. package/bin/runners/lib/contracts/external-contract.js +206 -206
  16. package/bin/runners/lib/contracts/guard.js +168 -168
  17. package/bin/runners/lib/contracts/index.js +89 -89
  18. package/bin/runners/lib/contracts/plan-validator.js +311 -311
  19. package/bin/runners/lib/contracts/route-contract.js +199 -199
  20. package/bin/runners/lib/contracts.js +804 -804
  21. package/bin/runners/lib/detect.js +89 -89
  22. package/bin/runners/lib/detectors-v2.js +703 -703
  23. package/bin/runners/lib/doctor/autofix.js +254 -254
  24. package/bin/runners/lib/doctor/index.js +37 -37
  25. package/bin/runners/lib/doctor/modules/dependencies.js +325 -325
  26. package/bin/runners/lib/doctor/modules/index.js +46 -46
  27. package/bin/runners/lib/doctor/modules/network.js +250 -250
  28. package/bin/runners/lib/doctor/modules/project.js +312 -312
  29. package/bin/runners/lib/doctor/modules/runtime.js +224 -224
  30. package/bin/runners/lib/doctor/modules/security.js +348 -348
  31. package/bin/runners/lib/doctor/modules/system.js +213 -213
  32. package/bin/runners/lib/doctor/modules/vibecheck.js +394 -394
  33. package/bin/runners/lib/doctor/reporter.js +262 -262
  34. package/bin/runners/lib/doctor/service.js +262 -262
  35. package/bin/runners/lib/doctor/types.js +113 -113
  36. package/bin/runners/lib/doctor/ui.js +263 -263
  37. package/bin/runners/lib/doctor-v2.js +608 -608
  38. package/bin/runners/lib/drift.js +425 -425
  39. package/bin/runners/lib/enforcement.js +72 -72
  40. package/bin/runners/lib/enterprise-detect.js +603 -603
  41. package/bin/runners/lib/enterprise-init.js +942 -942
  42. package/bin/runners/lib/entitlements-v2.js +490 -489
  43. package/bin/runners/lib/entitlements.js +6 -3
  44. package/bin/runners/lib/env-resolver.js +417 -417
  45. package/bin/runners/lib/env-template.js +66 -66
  46. package/bin/runners/lib/env.js +189 -189
  47. package/bin/runners/lib/extractors/client-calls.js +990 -990
  48. package/bin/runners/lib/extractors/fastify-route-dump.js +573 -573
  49. package/bin/runners/lib/extractors/fastify-routes.js +426 -426
  50. package/bin/runners/lib/extractors/index.js +363 -363
  51. package/bin/runners/lib/extractors/next-routes.js +524 -524
  52. package/bin/runners/lib/extractors/proof-graph.js +431 -431
  53. package/bin/runners/lib/extractors/route-matcher.js +451 -451
  54. package/bin/runners/lib/extractors/truthpack-v2.js +377 -377
  55. package/bin/runners/lib/extractors/ui-bindings.js +547 -547
  56. package/bin/runners/lib/findings-schema.js +281 -281
  57. package/bin/runners/lib/firewall-prompt.js +50 -50
  58. package/bin/runners/lib/graph/graph-builder.js +265 -265
  59. package/bin/runners/lib/graph/html-renderer.js +413 -413
  60. package/bin/runners/lib/graph/index.js +32 -32
  61. package/bin/runners/lib/graph/runtime-collector.js +215 -215
  62. package/bin/runners/lib/graph/static-extractor.js +518 -518
  63. package/bin/runners/lib/html-report.js +650 -650
  64. package/bin/runners/lib/init-wizard.js +308 -308
  65. package/bin/runners/lib/llm.js +75 -75
  66. package/bin/runners/lib/meter.js +61 -61
  67. package/bin/runners/lib/missions/evidence.js +126 -126
  68. package/bin/runners/lib/missions/plan.js +69 -69
  69. package/bin/runners/lib/missions/templates.js +192 -192
  70. package/bin/runners/lib/patch.js +40 -40
  71. package/bin/runners/lib/permissions/auth-model.js +213 -213
  72. package/bin/runners/lib/permissions/idor-prover.js +205 -205
  73. package/bin/runners/lib/permissions/index.js +45 -45
  74. package/bin/runners/lib/permissions/matrix-builder.js +198 -198
  75. package/bin/runners/lib/pkgjson.js +28 -28
  76. package/bin/runners/lib/policy.js +295 -295
  77. package/bin/runners/lib/preflight.js +142 -142
  78. package/bin/runners/lib/reality/correlation-detectors.js +359 -359
  79. package/bin/runners/lib/reality/index.js +318 -318
  80. package/bin/runners/lib/reality/request-hashing.js +416 -416
  81. package/bin/runners/lib/reality/request-mapper.js +453 -453
  82. package/bin/runners/lib/reality/safety-rails.js +463 -463
  83. package/bin/runners/lib/reality/semantic-snapshot.js +408 -408
  84. package/bin/runners/lib/reality/toast-detector.js +393 -393
  85. package/bin/runners/lib/reality-findings.js +84 -84
  86. package/bin/runners/lib/receipts.js +179 -179
  87. package/bin/runners/lib/redact.js +29 -29
  88. package/bin/runners/lib/replay/capsule-manager.js +154 -154
  89. package/bin/runners/lib/replay/index.js +263 -263
  90. package/bin/runners/lib/replay/player.js +348 -348
  91. package/bin/runners/lib/replay/recorder.js +331 -331
  92. package/bin/runners/lib/report-engine.js +447 -447
  93. package/bin/runners/lib/report-html.js +1499 -1499
  94. package/bin/runners/lib/report-templates.js +969 -969
  95. package/bin/runners/lib/report.js +135 -135
  96. package/bin/runners/lib/route-detection.js +1140 -1140
  97. package/bin/runners/lib/route-truth.js +477 -477
  98. package/bin/runners/lib/sandbox/index.js +59 -59
  99. package/bin/runners/lib/sandbox/proof-chain.js +399 -399
  100. package/bin/runners/lib/sandbox/sandbox-runner.js +205 -205
  101. package/bin/runners/lib/sandbox/worktree.js +174 -174
  102. package/bin/runners/lib/schema-validator.js +350 -350
  103. package/bin/runners/lib/schemas/contracts.schema.json +160 -160
  104. package/bin/runners/lib/schemas/finding.schema.json +100 -100
  105. package/bin/runners/lib/schemas/mission-pack.schema.json +206 -206
  106. package/bin/runners/lib/schemas/proof-graph.schema.json +176 -176
  107. package/bin/runners/lib/schemas/reality-report.schema.json +162 -162
  108. package/bin/runners/lib/schemas/share-pack.schema.json +180 -180
  109. package/bin/runners/lib/schemas/ship-report.schema.json +117 -117
  110. package/bin/runners/lib/schemas/truthpack-v2.schema.json +303 -303
  111. package/bin/runners/lib/schemas/validator.js +438 -438
  112. package/bin/runners/lib/score-history.js +282 -282
  113. package/bin/runners/lib/server-usage.js +12 -0
  114. package/bin/runners/lib/share-pack.js +239 -239
  115. package/bin/runners/lib/snippets.js +67 -67
  116. package/bin/runners/lib/truth.js +667 -667
  117. package/bin/runners/lib/upsell.js +510 -510
  118. package/bin/runners/lib/usage.js +153 -153
  119. package/bin/runners/lib/validate-patch.js +156 -156
  120. package/bin/runners/lib/verdict-engine.js +628 -628
  121. package/bin/runners/reality/engine.js +917 -917
  122. package/bin/runners/reality/flows.js +122 -122
  123. package/bin/runners/reality/report.js +378 -378
  124. package/bin/runners/reality/session.js +193 -193
  125. package/bin/runners/runAuth.js +51 -0
  126. package/bin/runners/runClaimVerifier.js +483 -483
  127. package/bin/runners/runContext.js +56 -56
  128. package/bin/runners/runContextCompiler.js +385 -385
  129. package/bin/runners/runCtx.js +674 -674
  130. package/bin/runners/runCtxDiff.js +301 -301
  131. package/bin/runners/runCtxGuard.js +176 -176
  132. package/bin/runners/runCtxSync.js +116 -116
  133. package/bin/runners/runGate.js +17 -17
  134. package/bin/runners/runGraph.js +454 -454
  135. package/bin/runners/runGuard.js +168 -168
  136. package/bin/runners/runInitGha.js +164 -164
  137. package/bin/runners/runInstall.js +277 -277
  138. package/bin/runners/runInteractive.js +388 -388
  139. package/bin/runners/runLabs.js +340 -340
  140. package/bin/runners/runMissionGenerator.js +282 -282
  141. package/bin/runners/runPR.js +255 -255
  142. package/bin/runners/runPermissions.js +304 -304
  143. package/bin/runners/runPreflight.js +580 -553
  144. package/bin/runners/runProve.js +1252 -1252
  145. package/bin/runners/runReality.js +1328 -1328
  146. package/bin/runners/runReplay.js +499 -499
  147. package/bin/runners/runReport.js +584 -584
  148. package/bin/runners/runShare.js +212 -212
  149. package/bin/runners/runStatus.js +138 -138
  150. package/bin/runners/runTruthpack.js +636 -636
  151. package/bin/runners/runVerify.js +272 -272
  152. package/bin/runners/runWatch.js +407 -407
  153. package/bin/vibecheck.js +2 -1
  154. package/mcp-server/consolidated-tools.js +804 -804
  155. package/mcp-server/package.json +1 -1
  156. package/mcp-server/tools/index.js +72 -72
  157. package/mcp-server/truth-context.js +581 -581
  158. package/mcp-server/truth-firewall-tools.js +1500 -1500
  159. package/package.json +1 -1
  160. package/bin/runners/runProof.zip +0 -0
@@ -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
+ };