vibe-splain 2.7.0 → 2.7.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 (2) hide show
  1. package/dist/index.js +195 -175
  2. package/package.json +2 -2
package/dist/index.js CHANGED
@@ -2175,7 +2175,6 @@ import { join as join7 } from "path";
2175
2175
  import { writeFile as writeFile7, readFile as readFile6 } from "fs/promises";
2176
2176
  var FUNCTION_TYPES2 = /* @__PURE__ */ new Set([
2177
2177
  "function_declaration",
2178
- "function",
2179
2178
  "function_expression",
2180
2179
  "arrow_function",
2181
2180
  "method_definition",
@@ -2191,24 +2190,33 @@ var FUNCTION_TYPES2 = /* @__PURE__ */ new Set([
2191
2190
  function firstLine2(s) {
2192
2191
  return s.split("\n")[0].trim();
2193
2192
  }
2193
+ function walkNodes(node, cb) {
2194
+ cb(node);
2195
+ for (let i = 0; i < node.childCount; i++) {
2196
+ const child = node.child(i);
2197
+ if (child)
2198
+ walkNodes(child, cb);
2199
+ }
2200
+ }
2194
2201
  function resolveCallee(node) {
2195
2202
  if (node.type === "identifier") {
2196
2203
  return { root: node.text, prop: null };
2197
2204
  }
2198
- if (node.type === "member_expression" || node.type === "property_identifier") {
2205
+ if (node.type === "member_expression") {
2199
2206
  const obj = node.childForFieldName("object");
2200
2207
  const prop = node.childForFieldName("property");
2201
2208
  if (obj && prop) {
2202
- if (obj.type === "identifier") {
2203
- return { root: obj.text, prop: prop.text };
2204
- } else {
2205
- const nested = resolveCallee(obj);
2206
- if (nested) {
2207
- return { root: nested.root, prop: nested.prop ? `${nested.prop}.${prop.text}` : prop.text };
2208
- }
2209
+ const nested = resolveCallee(obj);
2210
+ if (nested) {
2211
+ return { root: nested.root, prop: nested.prop ? `${nested.prop}.${prop.text}` : prop.text };
2209
2212
  }
2210
2213
  }
2211
2214
  }
2215
+ if (node.type === "parenthesized_expression" || node.type === "await_expression") {
2216
+ const inner = node.namedChildren.find((c) => c.type !== "(" && c.type !== ")" && c.type !== "await");
2217
+ if (inner)
2218
+ return resolveCallee(inner);
2219
+ }
2212
2220
  return null;
2213
2221
  }
2214
2222
  async function runActionBinding(projectRoot, inv, res) {
@@ -2255,25 +2263,26 @@ async function runActionBinding(projectRoot, inv, res) {
2255
2263
  });
2256
2264
  }
2257
2265
  const functions = [];
2266
+ const nodeToRecord = /* @__PURE__ */ new Map();
2258
2267
  if (!w.tree)
2259
2268
  continue;
2260
- const walkNodes = (node, cb) => {
2261
- cb(node);
2262
- for (const child of node.children)
2263
- walkNodes(child, cb);
2264
- };
2265
- const functionNodes = [];
2269
+ const allNodes = [];
2266
2270
  walkNodes(w.tree.rootNode, (n) => {
2267
- if (FUNCTION_TYPES2.has(n.type))
2268
- functionNodes.push(n);
2271
+ allNodes.push(n);
2269
2272
  });
2270
- for (const node of functionNodes) {
2273
+ for (const node of allNodes) {
2274
+ if (!FUNCTION_TYPES2.has(node.type))
2275
+ continue;
2276
+ const startLine = node.startPosition.row + 1;
2277
+ const startCol = node.startPosition.column;
2278
+ const endLine = node.endPosition.row + 1;
2279
+ const isDuplicate = functions.some((f) => f.startLine === startLine && f.endLine === endLine);
2280
+ if (isDuplicate)
2281
+ continue;
2271
2282
  functionsExtracted++;
2272
2283
  let displayName = "";
2273
2284
  let nameSource = "position_fallback";
2274
2285
  const p = node.parent;
2275
- const startLine = node.startPosition.row + 1;
2276
- const startCol = node.startPosition.column;
2277
2286
  if (node.childForFieldName("name")) {
2278
2287
  displayName = node.childForFieldName("name").text;
2279
2288
  nameSource = node.type === "method_definition" ? "method_definition" : "function_declaration";
@@ -2330,176 +2339,179 @@ async function runActionBinding(projectRoot, inv, res) {
2330
2339
  evidenceText: firstLine2(node.text).slice(0, 200)
2331
2340
  };
2332
2341
  functions.push(fnRecord);
2342
+ nodeToRecord.set(node.id, fnRecord);
2333
2343
  }
2334
- for (const fnRecord of functions) {
2335
- const fnNode = functionNodes.find((n) => n.startPosition.row + 1 === fnRecord.startLine && n.startPosition.column === fnRecord.startCol);
2336
- if (!fnNode)
2344
+ for (const node of allNodes) {
2345
+ if (node.type !== "call_expression" && node.type !== "call")
2337
2346
  continue;
2338
- const callNodes = [];
2339
- walkNodes(fnNode, (n) => {
2340
- if (n.type === "call_expression")
2341
- callNodes.push(n);
2342
- });
2343
- for (const callNode of callNodes) {
2344
- let isNested = false;
2345
- let curr = callNode.parent;
2346
- while (curr && curr !== fnNode) {
2347
- if (FUNCTION_TYPES2.has(curr.type)) {
2348
- isNested = true;
2349
- break;
2350
- }
2351
- curr = curr.parent;
2352
- }
2353
- if (isNested)
2354
- continue;
2355
- callsExtracted++;
2356
- const calleeNode = callNode.childForFieldName("function");
2357
- if (!calleeNode)
2358
- continue;
2359
- let actualCalleeNode = calleeNode;
2360
- if (calleeNode.type === "await_expression" && calleeNode.children.length > 1) {
2361
- actualCalleeNode = calleeNode.children[1];
2347
+ let curr = node.parent;
2348
+ let containingFnRecord = null;
2349
+ while (curr) {
2350
+ const rec = nodeToRecord.get(curr.id);
2351
+ if (rec) {
2352
+ containingFnRecord = rec;
2353
+ break;
2362
2354
  }
2363
- const resolvedCallee = resolveCallee(actualCalleeNode);
2364
- if (!resolvedCallee)
2365
- continue;
2366
- const { root: calleeRoot, prop: calleeProperty } = resolvedCallee;
2367
- const calleeText = actualCalleeNode.text.slice(0, 100);
2368
- const sourceLine = callNode.startPosition.row + 1;
2369
- const callId = `${fnRecord.functionId}::${calleeText}::${sourceLine}`;
2370
- let isSemantic = false;
2371
- let actionKind = null;
2372
- let targetModel = null;
2373
- let targetOperation = null;
2374
- if (calleeRoot === "prisma" && calleeProperty) {
2375
- if (/\.(create|update|upsert|delete|deleteMany|updateMany|createMany|executeRaw|queryRaw)$/.test("." + calleeProperty)) {
2376
- isSemantic = true;
2377
- actionKind = "database_write";
2378
- const parts = calleeProperty.split(".");
2379
- if (parts.length >= 2) {
2380
- targetModel = parts[0].charAt(0).toUpperCase() + parts[0].slice(1);
2381
- targetOperation = parts[parts.length - 1];
2382
- }
2383
- } else if (/\.(findMany|findUnique|findFirst|findFirstOrThrow|findUniqueOrThrow|count|aggregate|groupBy)$/.test("." + calleeProperty)) {
2384
- isSemantic = true;
2385
- actionKind = "database_read";
2386
- const parts = calleeProperty.split(".");
2387
- if (parts.length >= 2) {
2388
- targetModel = parts[0].charAt(0).toUpperCase() + parts[0].slice(1);
2389
- targetOperation = parts[parts.length - 1];
2390
- }
2355
+ curr = curr.parent;
2356
+ }
2357
+ if (!containingFnRecord)
2358
+ continue;
2359
+ callsExtracted++;
2360
+ const calleeNode = node.childForFieldName("function") || node.namedChild(0);
2361
+ if (!calleeNode)
2362
+ continue;
2363
+ const resolvedCallee = resolveCallee(calleeNode);
2364
+ if (!resolvedCallee)
2365
+ continue;
2366
+ const { root: calleeRoot, prop: calleeProperty } = resolvedCallee;
2367
+ const calleeText = calleeNode.text.slice(0, 100);
2368
+ const sourceLine = node.startPosition.row + 1;
2369
+ let isSemantic = false;
2370
+ let actionKind = null;
2371
+ let targetModel = null;
2372
+ let targetOperation = null;
2373
+ if (calleeRoot === "prisma" && calleeProperty) {
2374
+ if (/\.(create|update|upsert|delete|deleteMany|updateMany|createMany|executeRaw|queryRaw)$/.test("." + calleeProperty)) {
2375
+ isSemantic = true;
2376
+ actionKind = "database_write";
2377
+ const parts = calleeProperty.split(".");
2378
+ if (parts.length >= 2) {
2379
+ targetModel = parts[0].charAt(0).toUpperCase() + parts[0].slice(1);
2380
+ targetOperation = parts[parts.length - 1];
2381
+ }
2382
+ } else if (/\.(findMany|findUnique|findFirst|findFirstOrThrow|findUniqueOrThrow|count|aggregate|groupBy)$/.test("." + calleeProperty)) {
2383
+ isSemantic = true;
2384
+ actionKind = "database_read";
2385
+ const parts = calleeProperty.split(".");
2386
+ if (parts.length >= 2) {
2387
+ targetModel = parts[0].charAt(0).toUpperCase() + parts[0].slice(1);
2388
+ targetOperation = parts[parts.length - 1];
2391
2389
  }
2392
2390
  }
2393
- if (!isSemantic) {
2394
- if (calleeRoot === "fetch" || calleeRoot === "axios" && calleeProperty && /\.(get|post|put|patch|delete)$/.test("." + calleeProperty)) {
2395
- isSemantic = true;
2396
- actionKind = "external_api_call";
2397
- } else if (/validate|schema\.parse|schema\.safeParse|z\.parse/i.test(calleeText) || /validate|validator/i.test(calleeRoot)) {
2398
- isSemantic = true;
2399
- actionKind = "validation";
2400
- } else if (/getSession|getServerSession|auth\(\)|verifyToken|requireAuth|checkPermission/i.test(calleeText) || /auth|session/i.test(calleeRoot) && calleeProperty && /check|verify|get|require/i.test("." + calleeProperty)) {
2401
- isSemantic = true;
2402
- actionKind = "auth_check";
2403
- } else if (/sendEmail|sendMail|mailer\./i.test(calleeText)) {
2391
+ } else if (calleeRoot === "trpc" && calleeProperty && /\.(useMutation|mutate|mutateAsync)$/.test("." + calleeProperty)) {
2392
+ isSemantic = true;
2393
+ actionKind = "external_api_call";
2394
+ const parts = calleeProperty.split(".");
2395
+ if (parts.length >= 2) {
2396
+ targetModel = parts[0];
2397
+ targetOperation = parts[parts.length - 1];
2398
+ }
2399
+ }
2400
+ if (!isSemantic) {
2401
+ if (calleeRoot === "fetch" || calleeRoot === "axios" && calleeProperty && /\.(get|post|put|patch|delete)$/.test("." + calleeProperty)) {
2402
+ isSemantic = true;
2403
+ actionKind = "external_api_call";
2404
+ } else if (/validate|schema\.parse|schema\.safeParse|z\.parse/i.test(calleeText) || /validate|validator/i.test(calleeRoot)) {
2405
+ isSemantic = true;
2406
+ actionKind = "validation";
2407
+ } else if (/getSession|getServerSession|auth\(\)|verifyToken|requireAuth|checkPermission/i.test(calleeText) || /auth|session/i.test(calleeRoot) && calleeProperty && /check|verify|get|require/i.test("." + calleeProperty)) {
2408
+ isSemantic = true;
2409
+ actionKind = "auth_check";
2410
+ } else if (/sendEmail|sendMail|mailer\./i.test(calleeText)) {
2411
+ isSemantic = true;
2412
+ actionKind = "email_send";
2413
+ } else if (/createCalendarEvent|updateCalendarEvent|deleteCalendarEvent|calendar\.events\.(insert|update|delete)/i.test(calleeText)) {
2414
+ isSemantic = true;
2415
+ actionKind = "calendar_mutation";
2416
+ } else if (/triggerWebhook|sendWebhook|webhook\.send/i.test(calleeText)) {
2417
+ isSemantic = true;
2418
+ actionKind = "webhook_delivery";
2419
+ } else if (/stripe\.webhooks\.constructEvent|validateWebhook|verifySignature/i.test(calleeText)) {
2420
+ isSemantic = true;
2421
+ actionKind = "webhook_ingress";
2422
+ } else if (/revalidatePath|revalidateTag/i.test(calleeText)) {
2423
+ isSemantic = true;
2424
+ actionKind = "cache_revalidation";
2425
+ } else if (/posthog\.|mixpanel\.|amplitude\.|ga\(/i.test(calleeText)) {
2426
+ isSemantic = true;
2427
+ actionKind = "analytics_event";
2428
+ } else if (calleeRoot === "redirect" || /router|redirect|notFound|permanentRedirect/.test(calleeRoot) && calleeProperty && /push|replace|back/i.test("." + calleeProperty)) {
2429
+ isSemantic = true;
2430
+ actionKind = "redirect";
2431
+ } else if (/cookies\(\)|headers\(\)/.test(calleeText) || calleeRoot === "cookies" || calleeRoot === "headers") {
2432
+ isSemantic = true;
2433
+ actionKind = "side_effect";
2434
+ } else if (/checkRateLimitAndThrowError/i.test(calleeText)) {
2435
+ isSemantic = true;
2436
+ actionKind = "auth_check";
2437
+ } else {
2438
+ const emailImport = imports.find((i) => i.localName === calleeRoot && /nodemailer|resend|sendgrid|postmark|mailgun/i.test(i.moduleSpecifier));
2439
+ if (emailImport) {
2404
2440
  isSemantic = true;
2405
2441
  actionKind = "email_send";
2406
- } else if (/createCalendarEvent|updateCalendarEvent|deleteCalendarEvent|calendar\.events\.(insert|update|delete)/i.test(calleeText)) {
2407
- isSemantic = true;
2408
- actionKind = "calendar_mutation";
2409
- } else if (/triggerWebhook|sendWebhook|webhook\.send/i.test(calleeText)) {
2410
- isSemantic = true;
2411
- actionKind = "webhook_delivery";
2412
- } else if (/stripe\.webhooks\.constructEvent|validateWebhook|verifySignature/i.test(calleeText)) {
2413
- isSemantic = true;
2414
- actionKind = "webhook_ingress";
2415
- } else if (/revalidatePath|revalidateTag/i.test(calleeText)) {
2416
- isSemantic = true;
2417
- actionKind = "cache_revalidation";
2418
- } else if (/posthog\.|mixpanel\.|amplitude\.|ga\(/i.test(calleeText)) {
2419
- isSemantic = true;
2420
- actionKind = "analytics_event";
2421
- } else if (calleeRoot === "redirect" || /router|redirect|notFound|permanentRedirect/.test(calleeRoot) && calleeProperty && /push|replace|back/i.test("." + calleeProperty)) {
2422
- isSemantic = true;
2423
- actionKind = "redirect";
2424
- } else {
2425
- const emailImport = imports.find((i) => i.localName === calleeRoot && /nodemailer|resend|sendgrid|postmark|mailgun/i.test(i.moduleSpecifier));
2426
- if (emailImport) {
2427
- isSemantic = true;
2428
- actionKind = "email_send";
2429
- }
2430
2442
  }
2431
2443
  }
2432
- if (isSemantic && actionKind) {
2433
- semanticActionsExtracted++;
2434
- const actionId = `${fnRecord.functionId}::${actionKind}::${sourceLine}`;
2435
- fnRecord.semanticActions.push({
2436
- actionId,
2437
- sourceFunctionId: fnRecord.functionId,
2438
- actionKind,
2439
- targetModel,
2440
- targetOperation,
2441
- calleeText,
2442
- sourceLine,
2443
- confidence: "high",
2444
- evidenceText: firstLine2(callNode.text).slice(0, 200)
2445
- });
2446
- if (!artifact.actionIndex[actionKind])
2447
- artifact.actionIndex[actionKind] = [];
2448
- artifact.actionIndex[actionKind].push(fnRecord.functionId);
2449
- if (targetModel) {
2450
- const key1 = `${actionKind}::${targetModel}`;
2451
- if (!artifact.actionIndex[key1])
2452
- artifact.actionIndex[key1] = [];
2453
- artifact.actionIndex[key1].push(fnRecord.functionId);
2454
- if (targetOperation) {
2455
- const key2 = `${actionKind}::${targetModel}::${targetOperation}`;
2456
- if (!artifact.actionIndex[key2])
2457
- artifact.actionIndex[key2] = [];
2458
- artifact.actionIndex[key2].push(fnRecord.functionId);
2459
- }
2444
+ }
2445
+ if (isSemantic && actionKind) {
2446
+ semanticActionsExtracted++;
2447
+ const actionId = `${containingFnRecord.functionId}::${actionKind}::${sourceLine}`;
2448
+ containingFnRecord.semanticActions.push({
2449
+ actionId,
2450
+ sourceFunctionId: containingFnRecord.functionId,
2451
+ actionKind,
2452
+ targetModel,
2453
+ targetOperation,
2454
+ calleeText,
2455
+ sourceLine,
2456
+ confidence: "high",
2457
+ evidenceText: firstLine2(node.text).slice(0, 200)
2458
+ });
2459
+ if (!artifact.actionIndex[actionKind])
2460
+ artifact.actionIndex[actionKind] = [];
2461
+ artifact.actionIndex[actionKind].push(containingFnRecord.functionId);
2462
+ if (targetModel) {
2463
+ const key1 = `${actionKind}::${targetModel}`;
2464
+ if (!artifact.actionIndex[key1])
2465
+ artifact.actionIndex[key1] = [];
2466
+ artifact.actionIndex[key1].push(containingFnRecord.functionId);
2467
+ if (targetOperation) {
2468
+ const key2 = `${actionKind}::${targetModel}::${targetOperation}`;
2469
+ if (!artifact.actionIndex[key2])
2470
+ artifact.actionIndex[key2] = [];
2471
+ artifact.actionIndex[key2].push(containingFnRecord.functionId);
2460
2472
  }
2473
+ }
2474
+ } else {
2475
+ let resolvedTargetFunctionId = null;
2476
+ let resolvedFilePath = null;
2477
+ let resolutionKind = "unresolved";
2478
+ let confidence = "unresolved";
2479
+ const sameFileFn = functions.find((f) => f.displayName === calleeRoot);
2480
+ if (sameFileFn) {
2481
+ resolvedTargetFunctionId = sameFileFn.functionId;
2482
+ resolutionKind = "same_file_function";
2483
+ confidence = "high";
2461
2484
  } else {
2462
- let resolvedTargetFunctionId = null;
2463
- let resolvedFilePath = null;
2464
- let resolutionKind = "unresolved";
2465
- let confidence = "unresolved";
2466
- const sameFileFn = functions.find((f) => f.displayName === calleeRoot);
2467
- if (sameFileFn) {
2468
- resolvedTargetFunctionId = sameFileFn.functionId;
2469
- resolutionKind = "same_file_function";
2485
+ const namedImp = imports.find((i) => i.localName === calleeRoot && i.importKind !== "namespace" && !i.isTypeOnly);
2486
+ if (namedImp && namedImp.resolvedFilePath) {
2487
+ resolvedFilePath = namedImp.resolvedFilePath;
2488
+ resolutionKind = "named_import_match";
2470
2489
  confidence = "high";
2471
2490
  } else {
2472
- const namedImp = imports.find((i) => i.localName === calleeRoot && i.importKind !== "namespace" && !i.isTypeOnly);
2473
- if (namedImp && namedImp.resolvedFilePath) {
2474
- resolvedFilePath = namedImp.resolvedFilePath;
2475
- resolutionKind = "named_import_match";
2476
- confidence = "high";
2477
- } else {
2478
- const nsImp = imports.find((i) => i.localName === calleeRoot && i.importKind === "namespace");
2479
- if (nsImp && nsImp.resolvedFilePath) {
2480
- resolvedFilePath = nsImp.resolvedFilePath;
2481
- resolutionKind = "namespace_import_property";
2482
- confidence = "medium";
2483
- }
2491
+ const nsImp = imports.find((i) => i.localName === calleeRoot && i.importKind === "namespace");
2492
+ if (nsImp && nsImp.resolvedFilePath) {
2493
+ resolvedFilePath = nsImp.resolvedFilePath;
2494
+ resolutionKind = "namespace_import_property";
2495
+ confidence = "medium";
2484
2496
  }
2485
2497
  }
2486
- if (resolutionKind !== "unresolved")
2487
- callsResolved++;
2488
- fnRecord.calls.push({
2489
- callId,
2490
- sourceFunctionId: fnRecord.functionId,
2491
- calleeText,
2492
- calleeRoot,
2493
- calleeProperty,
2494
- sourceLine,
2495
- sourceSpan: { startLine: callNode.startPosition.row + 1, endLine: callNode.endPosition.row + 1 },
2496
- resolvedTargetFunctionId,
2497
- resolvedFilePath,
2498
- resolutionKind,
2499
- confidence,
2500
- evidenceText: firstLine2(callNode.text).slice(0, 200)
2501
- });
2502
2498
  }
2499
+ if (resolutionKind !== "unresolved")
2500
+ callsResolved++;
2501
+ containingFnRecord.calls.push({
2502
+ callId: `${containingFnRecord.functionId}::${calleeText}::${sourceLine}`,
2503
+ sourceFunctionId: containingFnRecord.functionId,
2504
+ calleeText,
2505
+ calleeRoot,
2506
+ calleeProperty,
2507
+ sourceLine,
2508
+ sourceSpan: { startLine: node.startPosition.row + 1, endLine: node.endPosition.row + 1 },
2509
+ resolvedTargetFunctionId,
2510
+ resolvedFilePath,
2511
+ resolutionKind,
2512
+ confidence,
2513
+ evidenceText: firstLine2(node.text).slice(0, 200)
2514
+ });
2503
2515
  }
2504
2516
  }
2505
2517
  artifact.files[filePath] = {
@@ -2992,6 +3004,9 @@ async function runScoring(projectRoot, cr, binding) {
2992
3004
  const callPts = Math.min(3, resolvedOutbound);
2993
3005
  fnScore += callPts;
2994
3006
  reasons.push(`Has ${resolvedOutbound} resolved outbound calls`);
3007
+ } else if (fn.calls.length > 0) {
3008
+ fnScore += 1;
3009
+ reasons.push(`Has ${fn.calls.length} outbound calls`);
2995
3010
  }
2996
3011
  const writesModel = fn.semanticActions.some((a) => a.actionKind === "database_write" && a.targetModel);
2997
3012
  if (writesModel) {
@@ -3003,10 +3018,15 @@ async function runScoring(projectRoot, cr, binding) {
3003
3018
  fnScore += 1;
3004
3019
  reasons.push("Performs auth/validation");
3005
3020
  }
3021
+ const hasEvidenceOverlap = rawEvidence.some((e) => fn.startLine <= e.endLine && fn.endLine >= e.startLine);
3022
+ if (hasEvidenceOverlap) {
3023
+ fnScore += 2;
3024
+ reasons.push("Overlaps with raw evidence span");
3025
+ }
3006
3026
  return { fn, fnScore, reasons };
3007
3027
  });
3008
3028
  scoredFunctions.sort((a, b) => b.fnScore - a.fnScore);
3009
- const topFns = scoredFunctions.slice(0, 5);
3029
+ const topFns = scoredFunctions.filter((x) => x.reasons.length > 0).slice(0, 5);
3010
3030
  if (topFns.length > 0) {
3011
3031
  criticalFunctions = topFns.map(({ fn, reasons }) => {
3012
3032
  const evidence = fn.semanticActions.slice(0, 5).sort((a, b) => a.sourceLine - b.sourceLine).map((a) => ({
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "vibe-splain",
3
- "version": "2.7.0",
4
- "description": "Architectural dossier engine for vibe-coded TypeScript/JavaScript projects. Runs as an MCP server inside your coding agent.",
3
+ "version": "2.7.2",
4
+ "description": "Architectural mapping and behavioral call-chain engine. Built on a language-agnostic foundation with specialized optimization for TypeScript/JavaScript projects.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "abp2204",