@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,518 +1,518 @@
1
- /**
2
- * Static Edge Extractor
3
- * Extracts causal edges from AST analysis:
4
- * - UI actions → client functions
5
- * - Client functions → network calls
6
- * - Network calls → server routes
7
- * - Server routes → handlers
8
- * - Handlers → DB/external calls
9
- */
10
-
11
- "use strict";
12
-
13
- const fg = require("fast-glob");
14
- const fs = require("fs");
15
- const path = require("path");
16
- const crypto = require("crypto");
17
- const parser = require("@babel/parser");
18
- const traverse = require("@babel/traverse").default;
19
- const t = require("@babel/types");
20
-
21
- function sha256(text) {
22
- return crypto.createHash("sha256").update(text).digest("hex").slice(0, 16);
23
- }
24
-
25
- function nodeId(type, file, line) {
26
- return `${type}_${sha256(file + ":" + line)}`;
27
- }
28
-
29
- function parseFile(code) {
30
- return parser.parse(code, {
31
- sourceType: "unambiguous",
32
- plugins: ["typescript", "jsx", "decorators-legacy"]
33
- });
34
- }
35
-
36
- function safeRead(fileAbs) {
37
- return fs.readFileSync(fileAbs, "utf8");
38
- }
39
-
40
- function getSnippet(code, loc) {
41
- if (!loc) return "";
42
- const lines = code.split(/\r?\n/);
43
- const start = Math.max(0, (loc.start?.line || 1) - 1);
44
- const end = Math.min(lines.length, (loc.end?.line || start + 1));
45
- return lines.slice(start, end).join("\n").slice(0, 200);
46
- }
47
-
48
- /**
49
- * Extract UI action nodes (onClick, onSubmit, etc.)
50
- */
51
- async function extractUIActions(repoRoot) {
52
- const files = await fg(["**/*.{tsx,jsx}"], {
53
- cwd: repoRoot,
54
- absolute: true,
55
- ignore: ["**/node_modules/**", "**/.next/**", "**/dist/**", "**/build/**"]
56
- });
57
-
58
- const nodes = [];
59
- const edges = [];
60
-
61
- for (const fileAbs of files) {
62
- const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
63
- const code = safeRead(fileAbs);
64
-
65
- let ast;
66
- try { ast = parseFile(code); } catch { continue; }
67
-
68
- traverse(ast, {
69
- JSXAttribute(p) {
70
- const name = p.node.name?.name;
71
- if (!name || !["onClick", "onSubmit", "onPress", "onChange"].includes(name)) return;
72
-
73
- const line = p.node.loc?.start?.line || 0;
74
- const snippet = getSnippet(code, p.node.loc);
75
- const id = nodeId("ui_action", fileRel, line);
76
-
77
- nodes.push({
78
- id,
79
- type: "ui_action",
80
- file: fileRel,
81
- line,
82
- snippet,
83
- snippetHash: sha256(snippet),
84
- actionType: name
85
- });
86
-
87
- // Try to find what function this calls
88
- const value = p.node.value;
89
- if (t.isJSXExpressionContainer(value)) {
90
- const expr = value.expression;
91
-
92
- // Direct function reference: onClick={handleClick}
93
- if (t.isIdentifier(expr)) {
94
- edges.push({
95
- id: `edge_${sha256(id + "_" + expr.name)}`,
96
- from: id,
97
- toRef: expr.name,
98
- type: "calls",
99
- confidence: "high",
100
- file: fileRel
101
- });
102
- }
103
-
104
- // Arrow function with call: onClick={() => handleClick()}
105
- if (t.isArrowFunctionExpression(expr) || t.isFunctionExpression(expr)) {
106
- traverse(expr.body, {
107
- CallExpression(cp) {
108
- if (t.isIdentifier(cp.node.callee)) {
109
- edges.push({
110
- id: `edge_${sha256(id + "_" + cp.node.callee.name)}`,
111
- from: id,
112
- toRef: cp.node.callee.name,
113
- type: "calls",
114
- confidence: "med",
115
- file: fileRel
116
- });
117
- }
118
- }
119
- }, p.scope, p);
120
- }
121
- }
122
- }
123
- });
124
- }
125
-
126
- return { nodes, edges };
127
- }
128
-
129
- /**
130
- * Extract client function nodes that make network calls
131
- */
132
- async function extractClientFunctions(repoRoot) {
133
- const files = await fg(["**/*.{ts,tsx,js,jsx}"], {
134
- cwd: repoRoot,
135
- absolute: true,
136
- ignore: ["**/node_modules/**", "**/.next/**", "**/dist/**", "**/build/**", "**/server/**", "**/api/**"]
137
- });
138
-
139
- const nodes = [];
140
- const edges = [];
141
-
142
- for (const fileAbs of files) {
143
- const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
144
- const code = safeRead(fileAbs);
145
-
146
- let ast;
147
- try { ast = parseFile(code); } catch { continue; }
148
-
149
- // Track function declarations that contain fetch/axios
150
- const funcMap = new Map();
151
-
152
- traverse(ast, {
153
- "FunctionDeclaration|ArrowFunctionExpression|FunctionExpression"(p) {
154
- const funcName = p.node.id?.name ||
155
- (t.isVariableDeclarator(p.parent) && t.isIdentifier(p.parent.id) ? p.parent.id.name : null);
156
-
157
- if (!funcName) return;
158
-
159
- const line = p.node.loc?.start?.line || 0;
160
- const snippet = getSnippet(code, p.node.loc);
161
- const id = nodeId("client_function", fileRel, line);
162
-
163
- funcMap.set(funcName, { id, line, snippet });
164
- }
165
- });
166
-
167
- traverse(ast, {
168
- CallExpression(p) {
169
- const callee = p.node.callee;
170
- let fetchUrl = null;
171
- let method = "*";
172
- let callType = null;
173
-
174
- // fetch("/api/x")
175
- if (t.isIdentifier(callee) && callee.name === "fetch") {
176
- const arg0 = p.node.arguments[0];
177
- if (t.isStringLiteral(arg0)) {
178
- fetchUrl = arg0.value;
179
- callType = "fetch";
180
- }
181
- // Check method in options
182
- const arg1 = p.node.arguments[1];
183
- if (t.isObjectExpression(arg1)) {
184
- for (const prop of arg1.properties) {
185
- if (t.isObjectProperty(prop) &&
186
- ((t.isIdentifier(prop.key) && prop.key.name === "method") ||
187
- (t.isStringLiteral(prop.key) && prop.key.value === "method")) &&
188
- t.isStringLiteral(prop.value)) {
189
- method = prop.value.value.toUpperCase();
190
- }
191
- }
192
- }
193
- }
194
-
195
- // axios.get("/api/x")
196
- if (t.isMemberExpression(callee) &&
197
- t.isIdentifier(callee.object) && callee.object.name === "axios" &&
198
- t.isIdentifier(callee.property)) {
199
- const verb = callee.property.name;
200
- if (["get", "post", "put", "patch", "delete"].includes(verb)) {
201
- const arg0 = p.node.arguments[0];
202
- if (t.isStringLiteral(arg0)) {
203
- fetchUrl = arg0.value;
204
- method = verb.toUpperCase();
205
- callType = "axios";
206
- }
207
- }
208
- }
209
-
210
- if (fetchUrl && fetchUrl.startsWith("/")) {
211
- const line = p.node.loc?.start?.line || 0;
212
- const snippet = getSnippet(code, p.node.loc);
213
- const networkId = nodeId("network_call", fileRel, line);
214
-
215
- nodes.push({
216
- id: networkId,
217
- type: "network_call",
218
- file: fileRel,
219
- line,
220
- snippet,
221
- snippetHash: sha256(snippet),
222
- url: fetchUrl,
223
- method,
224
- callType
225
- });
226
-
227
- // Find enclosing function
228
- let funcScope = p.scope;
229
- while (funcScope) {
230
- const funcNode = funcScope.block;
231
- if (t.isFunction(funcNode)) {
232
- const funcName = funcNode.id?.name ||
233
- (t.isVariableDeclarator(funcScope.parentBlock) ? funcScope.parentBlock.id?.name : null);
234
-
235
- if (funcName && funcMap.has(funcName)) {
236
- const funcData = funcMap.get(funcName);
237
-
238
- // Add function node if not already added
239
- if (!nodes.find(n => n.id === funcData.id)) {
240
- nodes.push({
241
- id: funcData.id,
242
- type: "client_function",
243
- file: fileRel,
244
- line: funcData.line,
245
- snippet: funcData.snippet,
246
- snippetHash: sha256(funcData.snippet),
247
- name: funcName
248
- });
249
- }
250
-
251
- edges.push({
252
- id: `edge_${sha256(funcData.id + "_" + networkId)}`,
253
- from: funcData.id,
254
- to: networkId,
255
- type: "fetches",
256
- confidence: "high"
257
- });
258
- }
259
- break;
260
- }
261
- funcScope = funcScope.parent;
262
- }
263
-
264
- // Create edge to server route (to be resolved later)
265
- edges.push({
266
- id: `edge_${sha256(networkId + "_route_" + fetchUrl)}`,
267
- from: networkId,
268
- toRoute: { method, path: fetchUrl },
269
- type: "calls_route",
270
- confidence: "high"
271
- });
272
- }
273
- }
274
- });
275
- }
276
-
277
- return { nodes, edges };
278
- }
279
-
280
- /**
281
- * Extract server route nodes from truthpack
282
- */
283
- function extractServerRoutes(truthpack) {
284
- const nodes = [];
285
- const serverRoutes = truthpack?.routes?.server || [];
286
-
287
- for (const route of serverRoutes) {
288
- const id = nodeId("server_route", route.handler || "unknown", route.path.length);
289
-
290
- nodes.push({
291
- id,
292
- type: "server_route",
293
- file: route.handler || "unknown",
294
- line: 0,
295
- snippet: `${route.method} ${route.path}`,
296
- snippetHash: sha256(`${route.method} ${route.path}`),
297
- method: route.method,
298
- path: route.path,
299
- confidence: route.confidence
300
- });
301
- }
302
-
303
- return { nodes, edges: [] };
304
- }
305
-
306
- /**
307
- * Extract handler → DB/external call edges
308
- */
309
- async function extractHandlerCalls(repoRoot) {
310
- const files = await fg(["**/api/**/*.{ts,js}", "**/routes/**/*.{ts,js}", "**/server/**/*.{ts,js}"], {
311
- cwd: repoRoot,
312
- absolute: true,
313
- ignore: ["**/node_modules/**", "**/.next/**", "**/dist/**", "**/build/**"]
314
- });
315
-
316
- const nodes = [];
317
- const edges = [];
318
-
319
- for (const fileAbs of files) {
320
- const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
321
- const code = safeRead(fileAbs);
322
-
323
- let ast;
324
- try { ast = parseFile(code); } catch { continue; }
325
-
326
- traverse(ast, {
327
- CallExpression(p) {
328
- const callee = p.node.callee;
329
- let callType = null;
330
- let target = null;
331
-
332
- // Prisma: prisma.user.findMany(), prisma.$transaction()
333
- if (t.isMemberExpression(callee)) {
334
- const obj = callee.object;
335
- if (t.isIdentifier(obj) && obj.name === "prisma") {
336
- callType = "db_call";
337
- target = "prisma";
338
- } else if (t.isMemberExpression(obj) &&
339
- t.isIdentifier(obj.object) && obj.object.name === "prisma") {
340
- callType = "db_call";
341
- target = `prisma.${obj.property?.name || "unknown"}`;
342
- }
343
- }
344
-
345
- // External: stripe.customers.create()
346
- if (t.isMemberExpression(callee)) {
347
- const rootObj = getRootObject(callee);
348
- if (rootObj && ["stripe", "github", "sendgrid", "twilio", "aws"].includes(rootObj.toLowerCase())) {
349
- callType = "external_call";
350
- target = rootObj;
351
- }
352
- }
353
-
354
- if (callType) {
355
- const line = p.node.loc?.start?.line || 0;
356
- const snippet = getSnippet(code, p.node.loc);
357
- const id = nodeId(callType, fileRel, line);
358
-
359
- nodes.push({
360
- id,
361
- type: callType,
362
- file: fileRel,
363
- line,
364
- snippet,
365
- snippetHash: sha256(snippet),
366
- target
367
- });
368
- }
369
- }
370
- });
371
- }
372
-
373
- return { nodes, edges };
374
- }
375
-
376
- function getRootObject(node) {
377
- if (t.isIdentifier(node)) return node.name;
378
- if (t.isMemberExpression(node)) return getRootObject(node.object);
379
- return null;
380
- }
381
-
382
- /**
383
- * Build complete static graph
384
- */
385
- async function extractStaticGraph(repoRoot, truthpack) {
386
- const uiActions = await extractUIActions(repoRoot);
387
- const clientFuncs = await extractClientFunctions(repoRoot);
388
- const serverRoutes = extractServerRoutes(truthpack);
389
- const handlerCalls = await extractHandlerCalls(repoRoot);
390
-
391
- const allNodes = [
392
- ...uiActions.nodes,
393
- ...clientFuncs.nodes,
394
- ...serverRoutes.nodes,
395
- ...handlerCalls.nodes
396
- ];
397
-
398
- const allEdges = [
399
- ...uiActions.edges,
400
- ...clientFuncs.edges,
401
- ...serverRoutes.edges,
402
- ...handlerCalls.edges
403
- ];
404
-
405
- // Resolve function reference edges
406
- const resolvedEdges = resolveEdges(allNodes, allEdges, truthpack);
407
-
408
- return {
409
- nodes: dedupeNodes(allNodes),
410
- edges: resolvedEdges
411
- };
412
- }
413
-
414
- function dedupeNodes(nodes) {
415
- const seen = new Map();
416
- for (const n of nodes) {
417
- if (!seen.has(n.id)) seen.set(n.id, n);
418
- }
419
- return Array.from(seen.values());
420
- }
421
-
422
- function resolveEdges(nodes, edges, truthpack) {
423
- const resolved = [];
424
- const nodeById = new Map(nodes.map(n => [n.id, n]));
425
- const funcByName = new Map();
426
- const routeNodes = nodes.filter(n => n.type === "server_route");
427
-
428
- // Build function name → node map
429
- for (const n of nodes) {
430
- if (n.type === "client_function" && n.name) {
431
- funcByName.set(n.name, n);
432
- }
433
- }
434
-
435
- for (const edge of edges) {
436
- // Resolve toRef (function name) → actual node
437
- if (edge.toRef) {
438
- const target = funcByName.get(edge.toRef);
439
- if (target) {
440
- resolved.push({
441
- ...edge,
442
- to: target.id,
443
- toRef: undefined
444
- });
445
- } else {
446
- // Unresolved function call - might be external
447
- resolved.push({
448
- ...edge,
449
- to: `unresolved_${edge.toRef}`,
450
- confidence: "low"
451
- });
452
- }
453
- continue;
454
- }
455
-
456
- // Resolve toRoute → server route node
457
- if (edge.toRoute) {
458
- const route = findMatchingRoute(routeNodes, edge.toRoute.method, edge.toRoute.path);
459
- if (route) {
460
- resolved.push({
461
- ...edge,
462
- to: route.id,
463
- toRoute: undefined
464
- });
465
- } else {
466
- // No matching route - this is a broken edge
467
- resolved.push({
468
- ...edge,
469
- to: `missing_route_${edge.toRoute.method}_${edge.toRoute.path}`,
470
- toRoute: edge.toRoute,
471
- broken: true,
472
- brokenReason: `Route ${edge.toRoute.method} ${edge.toRoute.path} not found on server`
473
- });
474
- }
475
- continue;
476
- }
477
-
478
- resolved.push(edge);
479
- }
480
-
481
- return resolved;
482
- }
483
-
484
- function findMatchingRoute(routeNodes, method, path) {
485
- // Normalize path
486
- const normPath = path.replace(/\/$/, "") || "/";
487
-
488
- for (const r of routeNodes) {
489
- if (r.method === "*" || r.method === method || method === "*") {
490
- if (r.path === normPath) return r;
491
- // Check parameterized match
492
- if (matchesParameterized(r.path, normPath)) return r;
493
- }
494
- }
495
- return null;
496
- }
497
-
498
- function matchesParameterized(pattern, actual) {
499
- const patternParts = pattern.split("/").filter(Boolean);
500
- const actualParts = actual.split("/").filter(Boolean);
501
-
502
- if (patternParts.length !== actualParts.length) return false;
503
-
504
- for (let i = 0; i < patternParts.length; i++) {
505
- const p = patternParts[i];
506
- if (p.startsWith(":") || p.startsWith("*")) continue;
507
- if (p !== actualParts[i]) return false;
508
- }
509
- return true;
510
- }
511
-
512
- module.exports = {
513
- extractStaticGraph,
514
- extractUIActions,
515
- extractClientFunctions,
516
- extractServerRoutes,
517
- extractHandlerCalls
518
- };
1
+ /**
2
+ * Static Edge Extractor
3
+ * Extracts causal edges from AST analysis:
4
+ * - UI actions → client functions
5
+ * - Client functions → network calls
6
+ * - Network calls → server routes
7
+ * - Server routes → handlers
8
+ * - Handlers → DB/external calls
9
+ */
10
+
11
+ "use strict";
12
+
13
+ const fg = require("fast-glob");
14
+ const fs = require("fs");
15
+ const path = require("path");
16
+ const crypto = require("crypto");
17
+ const parser = require("@babel/parser");
18
+ const traverse = require("@babel/traverse").default;
19
+ const t = require("@babel/types");
20
+
21
+ function sha256(text) {
22
+ return crypto.createHash("sha256").update(text).digest("hex").slice(0, 16);
23
+ }
24
+
25
+ function nodeId(type, file, line) {
26
+ return `${type}_${sha256(file + ":" + line)}`;
27
+ }
28
+
29
+ function parseFile(code) {
30
+ return parser.parse(code, {
31
+ sourceType: "unambiguous",
32
+ plugins: ["typescript", "jsx", "decorators-legacy"]
33
+ });
34
+ }
35
+
36
+ function safeRead(fileAbs) {
37
+ return fs.readFileSync(fileAbs, "utf8");
38
+ }
39
+
40
+ function getSnippet(code, loc) {
41
+ if (!loc) return "";
42
+ const lines = code.split(/\r?\n/);
43
+ const start = Math.max(0, (loc.start?.line || 1) - 1);
44
+ const end = Math.min(lines.length, (loc.end?.line || start + 1));
45
+ return lines.slice(start, end).join("\n").slice(0, 200);
46
+ }
47
+
48
+ /**
49
+ * Extract UI action nodes (onClick, onSubmit, etc.)
50
+ */
51
+ async function extractUIActions(repoRoot) {
52
+ const files = await fg(["**/*.{tsx,jsx}"], {
53
+ cwd: repoRoot,
54
+ absolute: true,
55
+ ignore: ["**/node_modules/**", "**/.next/**", "**/dist/**", "**/build/**"]
56
+ });
57
+
58
+ const nodes = [];
59
+ const edges = [];
60
+
61
+ for (const fileAbs of files) {
62
+ const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
63
+ const code = safeRead(fileAbs);
64
+
65
+ let ast;
66
+ try { ast = parseFile(code); } catch { continue; }
67
+
68
+ traverse(ast, {
69
+ JSXAttribute(p) {
70
+ const name = p.node.name?.name;
71
+ if (!name || !["onClick", "onSubmit", "onPress", "onChange"].includes(name)) return;
72
+
73
+ const line = p.node.loc?.start?.line || 0;
74
+ const snippet = getSnippet(code, p.node.loc);
75
+ const id = nodeId("ui_action", fileRel, line);
76
+
77
+ nodes.push({
78
+ id,
79
+ type: "ui_action",
80
+ file: fileRel,
81
+ line,
82
+ snippet,
83
+ snippetHash: sha256(snippet),
84
+ actionType: name
85
+ });
86
+
87
+ // Try to find what function this calls
88
+ const value = p.node.value;
89
+ if (t.isJSXExpressionContainer(value)) {
90
+ const expr = value.expression;
91
+
92
+ // Direct function reference: onClick={handleClick}
93
+ if (t.isIdentifier(expr)) {
94
+ edges.push({
95
+ id: `edge_${sha256(id + "_" + expr.name)}`,
96
+ from: id,
97
+ toRef: expr.name,
98
+ type: "calls",
99
+ confidence: "high",
100
+ file: fileRel
101
+ });
102
+ }
103
+
104
+ // Arrow function with call: onClick={() => handleClick()}
105
+ if (t.isArrowFunctionExpression(expr) || t.isFunctionExpression(expr)) {
106
+ traverse(expr.body, {
107
+ CallExpression(cp) {
108
+ if (t.isIdentifier(cp.node.callee)) {
109
+ edges.push({
110
+ id: `edge_${sha256(id + "_" + cp.node.callee.name)}`,
111
+ from: id,
112
+ toRef: cp.node.callee.name,
113
+ type: "calls",
114
+ confidence: "med",
115
+ file: fileRel
116
+ });
117
+ }
118
+ }
119
+ }, p.scope, p);
120
+ }
121
+ }
122
+ }
123
+ });
124
+ }
125
+
126
+ return { nodes, edges };
127
+ }
128
+
129
+ /**
130
+ * Extract client function nodes that make network calls
131
+ */
132
+ async function extractClientFunctions(repoRoot) {
133
+ const files = await fg(["**/*.{ts,tsx,js,jsx}"], {
134
+ cwd: repoRoot,
135
+ absolute: true,
136
+ ignore: ["**/node_modules/**", "**/.next/**", "**/dist/**", "**/build/**", "**/server/**", "**/api/**"]
137
+ });
138
+
139
+ const nodes = [];
140
+ const edges = [];
141
+
142
+ for (const fileAbs of files) {
143
+ const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
144
+ const code = safeRead(fileAbs);
145
+
146
+ let ast;
147
+ try { ast = parseFile(code); } catch { continue; }
148
+
149
+ // Track function declarations that contain fetch/axios
150
+ const funcMap = new Map();
151
+
152
+ traverse(ast, {
153
+ "FunctionDeclaration|ArrowFunctionExpression|FunctionExpression"(p) {
154
+ const funcName = p.node.id?.name ||
155
+ (t.isVariableDeclarator(p.parent) && t.isIdentifier(p.parent.id) ? p.parent.id.name : null);
156
+
157
+ if (!funcName) return;
158
+
159
+ const line = p.node.loc?.start?.line || 0;
160
+ const snippet = getSnippet(code, p.node.loc);
161
+ const id = nodeId("client_function", fileRel, line);
162
+
163
+ funcMap.set(funcName, { id, line, snippet });
164
+ }
165
+ });
166
+
167
+ traverse(ast, {
168
+ CallExpression(p) {
169
+ const callee = p.node.callee;
170
+ let fetchUrl = null;
171
+ let method = "*";
172
+ let callType = null;
173
+
174
+ // fetch("/api/x")
175
+ if (t.isIdentifier(callee) && callee.name === "fetch") {
176
+ const arg0 = p.node.arguments[0];
177
+ if (t.isStringLiteral(arg0)) {
178
+ fetchUrl = arg0.value;
179
+ callType = "fetch";
180
+ }
181
+ // Check method in options
182
+ const arg1 = p.node.arguments[1];
183
+ if (t.isObjectExpression(arg1)) {
184
+ for (const prop of arg1.properties) {
185
+ if (t.isObjectProperty(prop) &&
186
+ ((t.isIdentifier(prop.key) && prop.key.name === "method") ||
187
+ (t.isStringLiteral(prop.key) && prop.key.value === "method")) &&
188
+ t.isStringLiteral(prop.value)) {
189
+ method = prop.value.value.toUpperCase();
190
+ }
191
+ }
192
+ }
193
+ }
194
+
195
+ // axios.get("/api/x")
196
+ if (t.isMemberExpression(callee) &&
197
+ t.isIdentifier(callee.object) && callee.object.name === "axios" &&
198
+ t.isIdentifier(callee.property)) {
199
+ const verb = callee.property.name;
200
+ if (["get", "post", "put", "patch", "delete"].includes(verb)) {
201
+ const arg0 = p.node.arguments[0];
202
+ if (t.isStringLiteral(arg0)) {
203
+ fetchUrl = arg0.value;
204
+ method = verb.toUpperCase();
205
+ callType = "axios";
206
+ }
207
+ }
208
+ }
209
+
210
+ if (fetchUrl && fetchUrl.startsWith("/")) {
211
+ const line = p.node.loc?.start?.line || 0;
212
+ const snippet = getSnippet(code, p.node.loc);
213
+ const networkId = nodeId("network_call", fileRel, line);
214
+
215
+ nodes.push({
216
+ id: networkId,
217
+ type: "network_call",
218
+ file: fileRel,
219
+ line,
220
+ snippet,
221
+ snippetHash: sha256(snippet),
222
+ url: fetchUrl,
223
+ method,
224
+ callType
225
+ });
226
+
227
+ // Find enclosing function
228
+ let funcScope = p.scope;
229
+ while (funcScope) {
230
+ const funcNode = funcScope.block;
231
+ if (t.isFunction(funcNode)) {
232
+ const funcName = funcNode.id?.name ||
233
+ (t.isVariableDeclarator(funcScope.parentBlock) ? funcScope.parentBlock.id?.name : null);
234
+
235
+ if (funcName && funcMap.has(funcName)) {
236
+ const funcData = funcMap.get(funcName);
237
+
238
+ // Add function node if not already added
239
+ if (!nodes.find(n => n.id === funcData.id)) {
240
+ nodes.push({
241
+ id: funcData.id,
242
+ type: "client_function",
243
+ file: fileRel,
244
+ line: funcData.line,
245
+ snippet: funcData.snippet,
246
+ snippetHash: sha256(funcData.snippet),
247
+ name: funcName
248
+ });
249
+ }
250
+
251
+ edges.push({
252
+ id: `edge_${sha256(funcData.id + "_" + networkId)}`,
253
+ from: funcData.id,
254
+ to: networkId,
255
+ type: "fetches",
256
+ confidence: "high"
257
+ });
258
+ }
259
+ break;
260
+ }
261
+ funcScope = funcScope.parent;
262
+ }
263
+
264
+ // Create edge to server route (to be resolved later)
265
+ edges.push({
266
+ id: `edge_${sha256(networkId + "_route_" + fetchUrl)}`,
267
+ from: networkId,
268
+ toRoute: { method, path: fetchUrl },
269
+ type: "calls_route",
270
+ confidence: "high"
271
+ });
272
+ }
273
+ }
274
+ });
275
+ }
276
+
277
+ return { nodes, edges };
278
+ }
279
+
280
+ /**
281
+ * Extract server route nodes from truthpack
282
+ */
283
+ function extractServerRoutes(truthpack) {
284
+ const nodes = [];
285
+ const serverRoutes = truthpack?.routes?.server || [];
286
+
287
+ for (const route of serverRoutes) {
288
+ const id = nodeId("server_route", route.handler || "unknown", route.path.length);
289
+
290
+ nodes.push({
291
+ id,
292
+ type: "server_route",
293
+ file: route.handler || "unknown",
294
+ line: 0,
295
+ snippet: `${route.method} ${route.path}`,
296
+ snippetHash: sha256(`${route.method} ${route.path}`),
297
+ method: route.method,
298
+ path: route.path,
299
+ confidence: route.confidence
300
+ });
301
+ }
302
+
303
+ return { nodes, edges: [] };
304
+ }
305
+
306
+ /**
307
+ * Extract handler → DB/external call edges
308
+ */
309
+ async function extractHandlerCalls(repoRoot) {
310
+ const files = await fg(["**/api/**/*.{ts,js}", "**/routes/**/*.{ts,js}", "**/server/**/*.{ts,js}"], {
311
+ cwd: repoRoot,
312
+ absolute: true,
313
+ ignore: ["**/node_modules/**", "**/.next/**", "**/dist/**", "**/build/**"]
314
+ });
315
+
316
+ const nodes = [];
317
+ const edges = [];
318
+
319
+ for (const fileAbs of files) {
320
+ const fileRel = path.relative(repoRoot, fileAbs).replace(/\\/g, "/");
321
+ const code = safeRead(fileAbs);
322
+
323
+ let ast;
324
+ try { ast = parseFile(code); } catch { continue; }
325
+
326
+ traverse(ast, {
327
+ CallExpression(p) {
328
+ const callee = p.node.callee;
329
+ let callType = null;
330
+ let target = null;
331
+
332
+ // Prisma: prisma.user.findMany(), prisma.$transaction()
333
+ if (t.isMemberExpression(callee)) {
334
+ const obj = callee.object;
335
+ if (t.isIdentifier(obj) && obj.name === "prisma") {
336
+ callType = "db_call";
337
+ target = "prisma";
338
+ } else if (t.isMemberExpression(obj) &&
339
+ t.isIdentifier(obj.object) && obj.object.name === "prisma") {
340
+ callType = "db_call";
341
+ target = `prisma.${obj.property?.name || "unknown"}`;
342
+ }
343
+ }
344
+
345
+ // External: stripe.customers.create()
346
+ if (t.isMemberExpression(callee)) {
347
+ const rootObj = getRootObject(callee);
348
+ if (rootObj && ["stripe", "github", "sendgrid", "twilio", "aws"].includes(rootObj.toLowerCase())) {
349
+ callType = "external_call";
350
+ target = rootObj;
351
+ }
352
+ }
353
+
354
+ if (callType) {
355
+ const line = p.node.loc?.start?.line || 0;
356
+ const snippet = getSnippet(code, p.node.loc);
357
+ const id = nodeId(callType, fileRel, line);
358
+
359
+ nodes.push({
360
+ id,
361
+ type: callType,
362
+ file: fileRel,
363
+ line,
364
+ snippet,
365
+ snippetHash: sha256(snippet),
366
+ target
367
+ });
368
+ }
369
+ }
370
+ });
371
+ }
372
+
373
+ return { nodes, edges };
374
+ }
375
+
376
+ function getRootObject(node) {
377
+ if (t.isIdentifier(node)) return node.name;
378
+ if (t.isMemberExpression(node)) return getRootObject(node.object);
379
+ return null;
380
+ }
381
+
382
+ /**
383
+ * Build complete static graph
384
+ */
385
+ async function extractStaticGraph(repoRoot, truthpack) {
386
+ const uiActions = await extractUIActions(repoRoot);
387
+ const clientFuncs = await extractClientFunctions(repoRoot);
388
+ const serverRoutes = extractServerRoutes(truthpack);
389
+ const handlerCalls = await extractHandlerCalls(repoRoot);
390
+
391
+ const allNodes = [
392
+ ...uiActions.nodes,
393
+ ...clientFuncs.nodes,
394
+ ...serverRoutes.nodes,
395
+ ...handlerCalls.nodes
396
+ ];
397
+
398
+ const allEdges = [
399
+ ...uiActions.edges,
400
+ ...clientFuncs.edges,
401
+ ...serverRoutes.edges,
402
+ ...handlerCalls.edges
403
+ ];
404
+
405
+ // Resolve function reference edges
406
+ const resolvedEdges = resolveEdges(allNodes, allEdges, truthpack);
407
+
408
+ return {
409
+ nodes: dedupeNodes(allNodes),
410
+ edges: resolvedEdges
411
+ };
412
+ }
413
+
414
+ function dedupeNodes(nodes) {
415
+ const seen = new Map();
416
+ for (const n of nodes) {
417
+ if (!seen.has(n.id)) seen.set(n.id, n);
418
+ }
419
+ return Array.from(seen.values());
420
+ }
421
+
422
+ function resolveEdges(nodes, edges, truthpack) {
423
+ const resolved = [];
424
+ const nodeById = new Map(nodes.map(n => [n.id, n]));
425
+ const funcByName = new Map();
426
+ const routeNodes = nodes.filter(n => n.type === "server_route");
427
+
428
+ // Build function name → node map
429
+ for (const n of nodes) {
430
+ if (n.type === "client_function" && n.name) {
431
+ funcByName.set(n.name, n);
432
+ }
433
+ }
434
+
435
+ for (const edge of edges) {
436
+ // Resolve toRef (function name) → actual node
437
+ if (edge.toRef) {
438
+ const target = funcByName.get(edge.toRef);
439
+ if (target) {
440
+ resolved.push({
441
+ ...edge,
442
+ to: target.id,
443
+ toRef: undefined
444
+ });
445
+ } else {
446
+ // Unresolved function call - might be external
447
+ resolved.push({
448
+ ...edge,
449
+ to: `unresolved_${edge.toRef}`,
450
+ confidence: "low"
451
+ });
452
+ }
453
+ continue;
454
+ }
455
+
456
+ // Resolve toRoute → server route node
457
+ if (edge.toRoute) {
458
+ const route = findMatchingRoute(routeNodes, edge.toRoute.method, edge.toRoute.path);
459
+ if (route) {
460
+ resolved.push({
461
+ ...edge,
462
+ to: route.id,
463
+ toRoute: undefined
464
+ });
465
+ } else {
466
+ // No matching route - this is a broken edge
467
+ resolved.push({
468
+ ...edge,
469
+ to: `missing_route_${edge.toRoute.method}_${edge.toRoute.path}`,
470
+ toRoute: edge.toRoute,
471
+ broken: true,
472
+ brokenReason: `Route ${edge.toRoute.method} ${edge.toRoute.path} not found on server`
473
+ });
474
+ }
475
+ continue;
476
+ }
477
+
478
+ resolved.push(edge);
479
+ }
480
+
481
+ return resolved;
482
+ }
483
+
484
+ function findMatchingRoute(routeNodes, method, path) {
485
+ // Normalize path
486
+ const normPath = path.replace(/\/$/, "") || "/";
487
+
488
+ for (const r of routeNodes) {
489
+ if (r.method === "*" || r.method === method || method === "*") {
490
+ if (r.path === normPath) return r;
491
+ // Check parameterized match
492
+ if (matchesParameterized(r.path, normPath)) return r;
493
+ }
494
+ }
495
+ return null;
496
+ }
497
+
498
+ function matchesParameterized(pattern, actual) {
499
+ const patternParts = pattern.split("/").filter(Boolean);
500
+ const actualParts = actual.split("/").filter(Boolean);
501
+
502
+ if (patternParts.length !== actualParts.length) return false;
503
+
504
+ for (let i = 0; i < patternParts.length; i++) {
505
+ const p = patternParts[i];
506
+ if (p.startsWith(":") || p.startsWith("*")) continue;
507
+ if (p !== actualParts[i]) return false;
508
+ }
509
+ return true;
510
+ }
511
+
512
+ module.exports = {
513
+ extractStaticGraph,
514
+ extractUIActions,
515
+ extractClientFunctions,
516
+ extractServerRoutes,
517
+ extractHandlerCalls
518
+ };