vibe-splain 2.6.0 → 2.7.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.
package/dist/index.js CHANGED
@@ -89,10 +89,10 @@ import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema
89
89
 
90
90
  // ../brain/dist/scanner.js
91
91
  import { extname as extname4 } from "path";
92
- import { readFile as readFile7 } from "fs/promises";
92
+ import { readFile as readFile8 } from "fs/promises";
93
93
 
94
94
  // ../brain/dist/pipeline/orchestrator.js
95
- import { join as join8 } from "path";
95
+ import { join as join9 } from "path";
96
96
 
97
97
  // ../brain/dist/graph.js
98
98
  import { join as join2 } from "path";
@@ -572,6 +572,122 @@ function extractImports(source, lang) {
572
572
  specs.push(m[1] || m[2]);
573
573
  return specs;
574
574
  }
575
+ function extractNamedImports(source, lang, tree) {
576
+ const imports = [];
577
+ if (lang !== "typescript" && lang !== "tsx" && lang !== "javascript") {
578
+ return imports;
579
+ }
580
+ const walk = (node) => {
581
+ if (node.type === "import_statement") {
582
+ const moduleSpecifierNode = node.childForFieldName("source");
583
+ if (!moduleSpecifierNode)
584
+ return;
585
+ const moduleSpecifier = moduleSpecifierNode.text.replace(/['"]/g, "");
586
+ const importKeyword = node.children.find((c) => c.type === "import");
587
+ let isGlobalTypeOnly = false;
588
+ if (importKeyword) {
589
+ const nextNode = importKeyword.nextSibling;
590
+ if (nextNode && nextNode.type === "type") {
591
+ isGlobalTypeOnly = true;
592
+ }
593
+ }
594
+ const importClause = node.children.find((c) => c.type === "import_clause");
595
+ if (!importClause) {
596
+ imports.push({
597
+ localName: "",
598
+ importedName: "",
599
+ moduleSpecifier,
600
+ importKind: "side_effect",
601
+ isTypeOnly: isGlobalTypeOnly,
602
+ sourceLine: node.startPosition.row + 1,
603
+ rawText: firstLine(node.text)
604
+ });
605
+ return;
606
+ }
607
+ const defaultIdentifier = importClause.children.find((c) => c.type === "identifier");
608
+ if (defaultIdentifier) {
609
+ imports.push({
610
+ localName: defaultIdentifier.text,
611
+ importedName: "default",
612
+ moduleSpecifier,
613
+ importKind: "default",
614
+ isTypeOnly: isGlobalTypeOnly,
615
+ sourceLine: node.startPosition.row + 1,
616
+ rawText: firstLine(node.text)
617
+ });
618
+ }
619
+ const namespaceImport = importClause.children.find((c) => c.type === "namespace_import");
620
+ if (namespaceImport) {
621
+ const identifier = namespaceImport.children.find((c) => c.type === "identifier");
622
+ if (identifier) {
623
+ imports.push({
624
+ localName: identifier.text,
625
+ importedName: "*",
626
+ moduleSpecifier,
627
+ importKind: "namespace",
628
+ isTypeOnly: isGlobalTypeOnly,
629
+ sourceLine: node.startPosition.row + 1,
630
+ rawText: firstLine(node.text)
631
+ });
632
+ }
633
+ }
634
+ const namedImports = importClause.children.find((c) => c.type === "named_imports");
635
+ if (namedImports) {
636
+ for (const specifier of namedImports.children.filter((c) => c.type === "import_specifier")) {
637
+ const isTypeKeyword = specifier.children.some((c) => c.type === "type");
638
+ const isSpecifierTypeOnly = isGlobalTypeOnly || isTypeKeyword;
639
+ const nameNode = specifier.childForFieldName("name");
640
+ const aliasNode = specifier.childForFieldName("alias");
641
+ if (nameNode) {
642
+ imports.push({
643
+ localName: aliasNode ? aliasNode.text : nameNode.text,
644
+ importedName: nameNode.text,
645
+ moduleSpecifier,
646
+ importKind: "named",
647
+ isTypeOnly: isSpecifierTypeOnly,
648
+ sourceLine: node.startPosition.row + 1,
649
+ rawText: firstLine(node.text)
650
+ });
651
+ }
652
+ }
653
+ }
654
+ } else if (node.type === "variable_declarator") {
655
+ const init = node.childForFieldName("value");
656
+ if (init && init.type === "call_expression") {
657
+ const fnNameNode = init.childForFieldName("function");
658
+ if (fnNameNode && fnNameNode.text === "require") {
659
+ const args = init.childForFieldName("arguments");
660
+ if (args && args.namedChildCount > 0) {
661
+ const specNode = args.namedChildren[0];
662
+ if (specNode.type === "string") {
663
+ const specifier = specNode.text.replace(/['"]/g, "");
664
+ const idNode = node.childForFieldName("name");
665
+ if (idNode) {
666
+ if (idNode.type === "identifier") {
667
+ imports.push({
668
+ localName: idNode.text,
669
+ importedName: "default",
670
+ moduleSpecifier: specifier,
671
+ importKind: "default",
672
+ isTypeOnly: false,
673
+ sourceLine: node.startPosition.row + 1,
674
+ rawText: firstLine(node.text)
675
+ });
676
+ } else if (idNode.type === "object_pattern") {
677
+ }
678
+ }
679
+ }
680
+ }
681
+ }
682
+ }
683
+ }
684
+ for (const child of node.children) {
685
+ walk(child);
686
+ }
687
+ };
688
+ walk(tree.rootNode);
689
+ return imports;
690
+ }
575
691
  async function detectStackAndEntrypoints(projectRoot, files) {
576
692
  const stack = /* @__PURE__ */ new Set();
577
693
  const entrypoints = /* @__PURE__ */ new Set();
@@ -1064,6 +1180,7 @@ async function runInventory(projectRoot) {
1064
1180
  continue;
1065
1181
  const ast = analyzeAst(source, lang, tree);
1066
1182
  const importSpecs = extractImports(source, lang);
1183
+ const rawNamedImports = extractNamedImports(source, lang, tree);
1067
1184
  const frameworkRole = inferFrameworkRole(rel);
1068
1185
  const productDomain = inferProductDomain(rel, importSpecs);
1069
1186
  work.push({
@@ -1072,7 +1189,9 @@ async function runInventory(projectRoot) {
1072
1189
  lang,
1073
1190
  source,
1074
1191
  ast,
1192
+ tree,
1075
1193
  importSpecs,
1194
+ rawNamedImports,
1076
1195
  pathDemote: pathDemoteReason(rel),
1077
1196
  frameworkRole,
1078
1197
  productDomain
@@ -2051,9 +2170,516 @@ async function runClassification(projectRoot, inv, res) {
2051
2170
  return { projectRoot, classified, stack: inv.stack, entrypoints, map, communities };
2052
2171
  }
2053
2172
 
2054
- // ../brain/dist/pipeline/scoring.js
2173
+ // ../brain/dist/pipeline/binding.js
2055
2174
  import { join as join7 } from "path";
2056
- import { writeFile as writeFile7, mkdir as mkdir6, readFile as readFile6 } from "fs/promises";
2175
+ import { writeFile as writeFile7, readFile as readFile6 } from "fs/promises";
2176
+ var FUNCTION_TYPES2 = /* @__PURE__ */ new Set([
2177
+ "function_declaration",
2178
+ "function",
2179
+ "function_expression",
2180
+ "arrow_function",
2181
+ "method_definition",
2182
+ "function_definition",
2183
+ "method_declaration",
2184
+ "func_literal",
2185
+ "function_item",
2186
+ "closure_expression",
2187
+ "constructor_declaration",
2188
+ "generator_function_declaration",
2189
+ "generator_function"
2190
+ ]);
2191
+ function firstLine2(s) {
2192
+ return s.split("\n")[0].trim();
2193
+ }
2194
+ function resolveCallee(node) {
2195
+ if (node.type === "identifier") {
2196
+ return { root: node.text, prop: null };
2197
+ }
2198
+ if (node.type === "member_expression" || node.type === "property_identifier") {
2199
+ const obj = node.childForFieldName("object");
2200
+ const prop = node.childForFieldName("property");
2201
+ 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
+ }
2210
+ }
2211
+ }
2212
+ return null;
2213
+ }
2214
+ async function runActionBinding(projectRoot, inv, res) {
2215
+ const artifact = {
2216
+ schemaVersion: 1,
2217
+ projectRoot,
2218
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
2219
+ files: {},
2220
+ functionIndex: {},
2221
+ actionIndex: {},
2222
+ entrypointIndex: {}
2223
+ };
2224
+ let filesProcessed = 0;
2225
+ let functionsExtracted = 0;
2226
+ let callsExtracted = 0;
2227
+ let callsResolved = 0;
2228
+ let semanticActionsExtracted = 0;
2229
+ let entrypointsFound = 0;
2230
+ let namedImportsExtracted = 0;
2231
+ for (const w of inv.work) {
2232
+ if (w.pathDemote)
2233
+ continue;
2234
+ filesProcessed++;
2235
+ const filePath = w.rel;
2236
+ const imports = [];
2237
+ for (const raw of w.rawNamedImports) {
2238
+ namedImportsExtracted++;
2239
+ const { resolved, isAlias } = resolveImportWithAliasMap(raw.moduleSpecifier, w.abs, w.lang, projectRoot, inv.fileSet, inv.basenameIndex, res.aliasMap);
2240
+ let confidence = "low";
2241
+ if (resolved)
2242
+ confidence = "high";
2243
+ else if (isAlias)
2244
+ confidence = "medium";
2245
+ imports.push({
2246
+ localName: raw.localName,
2247
+ importedName: raw.importedName,
2248
+ moduleSpecifier: raw.moduleSpecifier,
2249
+ resolvedFilePath: resolved,
2250
+ importKind: raw.importKind,
2251
+ isTypeOnly: raw.isTypeOnly,
2252
+ sourceLine: raw.sourceLine,
2253
+ confidence,
2254
+ evidenceText: raw.rawText.slice(0, 200)
2255
+ });
2256
+ }
2257
+ const functions = [];
2258
+ if (!w.tree)
2259
+ continue;
2260
+ const walkNodes = (node, cb) => {
2261
+ cb(node);
2262
+ for (const child of node.children)
2263
+ walkNodes(child, cb);
2264
+ };
2265
+ const functionNodes = [];
2266
+ walkNodes(w.tree.rootNode, (n) => {
2267
+ if (FUNCTION_TYPES2.has(n.type))
2268
+ functionNodes.push(n);
2269
+ });
2270
+ for (const node of functionNodes) {
2271
+ functionsExtracted++;
2272
+ let displayName = "";
2273
+ let nameSource = "position_fallback";
2274
+ const p = node.parent;
2275
+ const startLine = node.startPosition.row + 1;
2276
+ const startCol = node.startPosition.column;
2277
+ if (node.childForFieldName("name")) {
2278
+ displayName = node.childForFieldName("name").text;
2279
+ nameSource = node.type === "method_definition" ? "method_definition" : "function_declaration";
2280
+ } else if (p?.type === "variable_declarator" && p.childForFieldName("name")) {
2281
+ displayName = p.childForFieldName("name").text;
2282
+ nameSource = "parent_variable_declarator";
2283
+ } else if (p?.type === "assignment_expression" && p.childForFieldName("left")) {
2284
+ displayName = p.childForFieldName("left").text;
2285
+ nameSource = "parent_assignment";
2286
+ } else if (p?.type === "pair" && p.childForFieldName("key")) {
2287
+ displayName = p.childForFieldName("key").text;
2288
+ nameSource = "object_property_key";
2289
+ } else if (p?.type === "export_statement") {
2290
+ const idNode = p.children.find((c) => c.type === "identifier");
2291
+ if (idNode) {
2292
+ displayName = idNode.text;
2293
+ nameSource = "export_const";
2294
+ }
2295
+ } else if (p?.type === "lexical_declaration" || p?.type === "variable_declaration") {
2296
+ const decl = p.children.find((c) => c.type === "variable_declarator");
2297
+ if (decl && decl.childForFieldName("name")) {
2298
+ displayName = decl.childForFieldName("name").text;
2299
+ nameSource = "parent_variable_declarator";
2300
+ }
2301
+ }
2302
+ if (!displayName) {
2303
+ displayName = `anonymous@${startLine}:${startCol}`;
2304
+ nameSource = "position_fallback";
2305
+ }
2306
+ const functionId = `${filePath}::${displayName}::${startLine}:${startCol}`;
2307
+ let isExported = false;
2308
+ if (p?.type === "export_statement")
2309
+ isExported = true;
2310
+ if (p?.parent?.type === "export_statement")
2311
+ isExported = true;
2312
+ if (w.ast.exportedNames.includes(displayName))
2313
+ isExported = true;
2314
+ const isEntrypoint = (w.frameworkRole.includes("route") || w.frameworkRole.includes("page")) && isExported;
2315
+ if (isEntrypoint)
2316
+ entrypointsFound++;
2317
+ const fnRecord = {
2318
+ functionId,
2319
+ displayName,
2320
+ nameSource,
2321
+ functionKind: node.type,
2322
+ filePath,
2323
+ startLine,
2324
+ endLine: node.endPosition.row + 1,
2325
+ startCol,
2326
+ isExported,
2327
+ isEntrypoint,
2328
+ calls: [],
2329
+ semanticActions: [],
2330
+ evidenceText: firstLine2(node.text).slice(0, 200)
2331
+ };
2332
+ functions.push(fnRecord);
2333
+ }
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)
2337
+ 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];
2362
+ }
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
+ }
2391
+ }
2392
+ }
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)) {
2404
+ isSemantic = true;
2405
+ 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
+ }
2431
+ }
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
+ }
2460
+ }
2461
+ } 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";
2470
+ confidence = "high";
2471
+ } 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
+ }
2484
+ }
2485
+ }
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
+ }
2503
+ }
2504
+ }
2505
+ artifact.files[filePath] = {
2506
+ filePath,
2507
+ language: w.lang,
2508
+ sourceRole: w.frameworkRole === "test" ? "test" : "production",
2509
+ imports,
2510
+ functions
2511
+ };
2512
+ for (const fn of functions) {
2513
+ artifact.functionIndex[fn.functionId] = {
2514
+ filePath,
2515
+ displayName: fn.displayName,
2516
+ startLine: fn.startLine,
2517
+ endLine: fn.endLine
2518
+ };
2519
+ if (fn.isEntrypoint) {
2520
+ if (!artifact.entrypointIndex[filePath])
2521
+ artifact.entrypointIndex[filePath] = [];
2522
+ artifact.entrypointIndex[filePath].push(fn.functionId);
2523
+ }
2524
+ }
2525
+ }
2526
+ for (const fileRec of Object.values(artifact.files)) {
2527
+ for (const fnRec of fileRec.functions) {
2528
+ for (const callRec of fnRec.calls) {
2529
+ if (callRec.resolutionKind === "named_import_match" && callRec.resolvedFilePath) {
2530
+ const targetFile = artifact.files[callRec.resolvedFilePath];
2531
+ if (targetFile) {
2532
+ const targetFn = targetFile.functions.find((f) => f.displayName === callRec.calleeRoot);
2533
+ if (targetFn) {
2534
+ callRec.resolvedTargetFunctionId = targetFn.functionId;
2535
+ }
2536
+ }
2537
+ }
2538
+ }
2539
+ }
2540
+ }
2541
+ await writeFile7(join7(projectRoot, ".vibe-splainer", "action_bindings.json"), JSON.stringify(artifact, null, 2), "utf8");
2542
+ const summary = {
2543
+ filesProcessed,
2544
+ functionsExtracted,
2545
+ callsExtracted,
2546
+ callsResolved,
2547
+ semanticActionsExtracted,
2548
+ entrypointsFound,
2549
+ namedImportsExtracted
2550
+ };
2551
+ await writeFile7(join7(projectRoot, ".vibe-splainer", "stage-09-action-bindings-summary.json"), JSON.stringify(summary, null, 2), "utf8");
2552
+ return { artifact };
2553
+ }
2554
+ async function traverseCallChain(projectRoot, args) {
2555
+ const artifactPath = join7(projectRoot, ".vibe-splainer", "action_bindings.json");
2556
+ let artifact;
2557
+ try {
2558
+ const raw = await readFile6(artifactPath, "utf8");
2559
+ artifact = JSON.parse(raw);
2560
+ } catch {
2561
+ throw new Error("action_bindings.json not found. Run scan_project first.");
2562
+ }
2563
+ const { entrypointPath, maxDepth = 6, targetActionKind, targetModel, targetOperation, targetFunctionName, includeTests = false } = args;
2564
+ let seedFunctionIds = [];
2565
+ if (artifact.entrypointIndex[entrypointPath]) {
2566
+ seedFunctionIds = artifact.entrypointIndex[entrypointPath];
2567
+ } else if (artifact.files[entrypointPath]) {
2568
+ const fileRec = artifact.files[entrypointPath];
2569
+ seedFunctionIds = fileRec.functions.filter((f) => f.isEntrypoint).map((f) => f.functionId);
2570
+ if (seedFunctionIds.length === 0) {
2571
+ const firstExported = fileRec.functions.find((f) => f.isExported);
2572
+ if (firstExported)
2573
+ seedFunctionIds.push(firstExported.functionId);
2574
+ }
2575
+ }
2576
+ if (seedFunctionIds.length === 0) {
2577
+ throw new Error(`No entrypoint functions found in ${entrypointPath}`);
2578
+ }
2579
+ const chain = [];
2580
+ const unresolvedEdges = [];
2581
+ const visited = /* @__PURE__ */ new Set();
2582
+ const queue = seedFunctionIds.map((id) => ({ functionId: id, depth: 0 }));
2583
+ let targetReached = false;
2584
+ let truncatedAtDepth = false;
2585
+ while (queue.length > 0) {
2586
+ const { functionId, depth } = queue.shift();
2587
+ if (visited.has(functionId))
2588
+ continue;
2589
+ visited.add(functionId);
2590
+ const indexEntry = artifact.functionIndex[functionId];
2591
+ if (!indexEntry)
2592
+ continue;
2593
+ const fileRec = artifact.files[indexEntry.filePath];
2594
+ if (!fileRec)
2595
+ continue;
2596
+ if (!includeTests && fileRec.sourceRole === "test")
2597
+ continue;
2598
+ const fnRec = fileRec.functions.find((f) => f.functionId === functionId);
2599
+ if (!fnRec)
2600
+ continue;
2601
+ for (const call of fnRec.calls) {
2602
+ if (call.resolutionKind === "semantic_action_only")
2603
+ continue;
2604
+ if (call.resolvedTargetFunctionId) {
2605
+ if (depth < maxDepth) {
2606
+ queue.push({ functionId: call.resolvedTargetFunctionId, depth: depth + 1 });
2607
+ let isTarget = false;
2608
+ if (targetFunctionName && call.calleeRoot === targetFunctionName)
2609
+ isTarget = true;
2610
+ if (isTarget)
2611
+ targetReached = true;
2612
+ chain.push({
2613
+ functionId: call.resolvedTargetFunctionId,
2614
+ displayName: call.calleeRoot,
2615
+ filePath: call.resolvedFilePath || "unknown",
2616
+ startLine: call.sourceLine,
2617
+ edgeKind: "call_edge",
2618
+ confidence: call.confidence,
2619
+ evidenceText: call.evidenceText,
2620
+ isTarget,
2621
+ depth
2622
+ });
2623
+ } else {
2624
+ unresolvedEdges.push({
2625
+ fromFunctionId: functionId,
2626
+ calleeText: call.calleeText,
2627
+ sourceLine: call.sourceLine,
2628
+ reason: "depth limit reached"
2629
+ });
2630
+ truncatedAtDepth = true;
2631
+ }
2632
+ } else {
2633
+ unresolvedEdges.push({
2634
+ fromFunctionId: functionId,
2635
+ calleeText: call.calleeText,
2636
+ sourceLine: call.sourceLine,
2637
+ reason: call.resolutionKind
2638
+ });
2639
+ }
2640
+ }
2641
+ for (const action of fnRec.semanticActions) {
2642
+ let isTarget = false;
2643
+ if (targetActionKind && action.actionKind === targetActionKind) {
2644
+ isTarget = true;
2645
+ if (targetModel && action.targetModel !== targetModel)
2646
+ isTarget = false;
2647
+ if (targetOperation && action.targetOperation !== targetOperation)
2648
+ isTarget = false;
2649
+ } else if (targetModel && action.targetModel === targetModel) {
2650
+ isTarget = true;
2651
+ if (targetOperation && action.targetOperation !== targetOperation)
2652
+ isTarget = false;
2653
+ }
2654
+ if (isTarget)
2655
+ targetReached = true;
2656
+ chain.push({
2657
+ functionId: action.sourceFunctionId,
2658
+ displayName: action.calleeText,
2659
+ filePath: fileRec.filePath,
2660
+ startLine: action.sourceLine,
2661
+ edgeKind: "semantic_action",
2662
+ actionKind: action.actionKind,
2663
+ targetModel: action.targetModel || void 0,
2664
+ targetOperation: action.targetOperation || void 0,
2665
+ confidence: action.confidence,
2666
+ evidenceText: action.evidenceText,
2667
+ isTarget,
2668
+ depth
2669
+ });
2670
+ }
2671
+ }
2672
+ return {
2673
+ targetReached,
2674
+ truncatedAtDepth,
2675
+ chain,
2676
+ unresolvedEdges
2677
+ };
2678
+ }
2679
+
2680
+ // ../brain/dist/pipeline/scoring.js
2681
+ import { join as join8 } from "path";
2682
+ import { writeFile as writeFile8, mkdir as mkdir6, readFile as readFile7 } from "fs/promises";
2057
2683
  import { createHash } from "crypto";
2058
2684
  function computeSeverity(sideEffectProfile, productDomain, gravity, heat, maxNesting, hasLongFunctions, swallowedCatches, runtimeEntrypoints) {
2059
2685
  let score = 0;
@@ -2271,9 +2897,17 @@ function deriveConfidence(fanIn, gravity) {
2271
2897
  return "medium";
2272
2898
  return "low";
2273
2899
  }
2274
- async function runScoring(projectRoot, cr) {
2275
- const dir = join7(projectRoot, ".vibe-splainer");
2900
+ async function runScoring(projectRoot, cr, binding) {
2901
+ const dir = join8(projectRoot, ".vibe-splainer");
2276
2902
  await mkdir6(dir, { recursive: true });
2903
+ let bindingArtifact = binding?.artifact;
2904
+ if (!bindingArtifact) {
2905
+ try {
2906
+ const raw = await readFile7(join8(projectRoot, ".vibe-splainer", "action_bindings.json"), "utf8");
2907
+ bindingArtifact = JSON.parse(raw);
2908
+ } catch {
2909
+ }
2910
+ }
2277
2911
  const persisted = {};
2278
2912
  const severityBreakdowns = {};
2279
2913
  for (const f of cr.classified) {
@@ -2307,7 +2941,7 @@ async function runScoring(projectRoot, cr) {
2307
2941
  severityBreakdowns[f.rel] = `severity=${pf.canonicalSeverity} loadBearing=${pf.canonicalLoadBearing} effects=${pf.sideEffectProfile.join(",")} domain=${pf.productDomain}`;
2308
2942
  }
2309
2943
  const stage09 = Object.fromEntries(Object.entries(persisted).filter(([, pf]) => pf.isRealSource).map(([rel, pf]) => [rel, { canonicalSeverity: pf.canonicalSeverity, canonicalLoadBearing: pf.canonicalLoadBearing, scoreBreakdown: severityBreakdowns[rel] }]));
2310
- await writeFile7(join7(dir, "stage-09-severity.json"), JSON.stringify(stage09, null, 2), "utf8");
2944
+ await writeFile8(join8(dir, "stage-09-severity.json"), JSON.stringify(stage09, null, 2), "utf8");
2311
2945
  const store = { files: persisted };
2312
2946
  const importedByMapForDelta = /* @__PURE__ */ new Map();
2313
2947
  for (const [rel, pf] of Object.entries(persisted)) {
@@ -2338,6 +2972,79 @@ async function runScoring(projectRoot, cr) {
2338
2972
  excerpt: span.snippet,
2339
2973
  isTruncated: span.rawExcerpt.length > 2e3
2340
2974
  }));
2975
+ let criticalFunctions = void 0;
2976
+ if (bindingArtifact) {
2977
+ const fileBinding = bindingArtifact.files[pf.relativePath];
2978
+ if (fileBinding) {
2979
+ const scoredFunctions = fileBinding.functions.map((fn) => {
2980
+ let fnScore = 0;
2981
+ const reasons = [];
2982
+ if (fn.semanticActions.length > 0) {
2983
+ fnScore += 3;
2984
+ reasons.push("Contains semantic actions");
2985
+ }
2986
+ if (fn.isEntrypoint) {
2987
+ fnScore += 2;
2988
+ reasons.push("Is a framework entrypoint");
2989
+ }
2990
+ const resolvedOutbound = fn.calls.filter((c) => c.resolvedTargetFunctionId).length;
2991
+ if (resolvedOutbound > 0) {
2992
+ const callPts = Math.min(3, resolvedOutbound);
2993
+ fnScore += callPts;
2994
+ reasons.push(`Has ${resolvedOutbound} resolved outbound calls`);
2995
+ }
2996
+ const writesModel = fn.semanticActions.some((a) => a.actionKind === "database_write" && a.targetModel);
2997
+ if (writesModel) {
2998
+ fnScore += 2;
2999
+ reasons.push("Writes to a database model");
3000
+ }
3001
+ const authOrValid = fn.semanticActions.some((a) => a.actionKind === "auth_check" || a.actionKind === "validation");
3002
+ if (authOrValid) {
3003
+ fnScore += 1;
3004
+ reasons.push("Performs auth/validation");
3005
+ }
3006
+ return { fn, fnScore, reasons };
3007
+ });
3008
+ scoredFunctions.sort((a, b) => b.fnScore - a.fnScore);
3009
+ const topFns = scoredFunctions.slice(0, 5);
3010
+ if (topFns.length > 0) {
3011
+ criticalFunctions = topFns.map(({ fn, reasons }) => {
3012
+ const evidence = fn.semanticActions.slice(0, 5).sort((a, b) => a.sourceLine - b.sourceLine).map((a) => ({
3013
+ sourceLine: a.sourceLine,
3014
+ text: a.evidenceText,
3015
+ actionKind: a.actionKind,
3016
+ targetModel: a.targetModel,
3017
+ targetOperation: a.targetOperation,
3018
+ confidence: a.confidence
3019
+ }));
3020
+ const confidences = fn.semanticActions.map((a) => a.confidence);
3021
+ let confidence2 = "high";
3022
+ if (confidences.includes("low"))
3023
+ confidence2 = "low";
3024
+ else if (confidences.includes("medium"))
3025
+ confidence2 = "medium";
3026
+ return {
3027
+ functionId: fn.functionId,
3028
+ displayName: fn.displayName,
3029
+ functionKind: fn.functionKind,
3030
+ startLine: fn.startLine,
3031
+ endLine: fn.endLine,
3032
+ isEntrypoint: fn.isEntrypoint,
3033
+ isExported: fn.isExported,
3034
+ actionKinds: [...new Set(fn.semanticActions.map((a) => a.actionKind))],
3035
+ targetModels: [...new Set(fn.semanticActions.map((a) => a.targetModel).filter(Boolean))],
3036
+ targetOperations: [...new Set(fn.semanticActions.map((a) => a.targetOperation).filter(Boolean))],
3037
+ outboundCallCount: fn.calls.length,
3038
+ resolvedOutboundCallCount: fn.calls.filter((c) => c.resolvedTargetFunctionId).length,
3039
+ semanticActionCount: fn.semanticActions.length,
3040
+ evidence,
3041
+ confidence: confidence2,
3042
+ reasons
3043
+ };
3044
+ });
3045
+ }
3046
+ }
3047
+ }
2341
3048
  return {
2342
3049
  path: pf.relativePath,
2343
3050
  frameworkRole: pf.frameworkRole,
@@ -2362,17 +3069,18 @@ async function runScoring(projectRoot, cr) {
2362
3069
  testProbes: inferTestProbes(pf.writeIntents, observableOutputs),
2363
3070
  rawEvidence,
2364
3071
  displayEvidence,
3072
+ criticalFunctions,
2365
3073
  analysisAnnotation: `${pf.frameworkRole} in ${pf.productDomain} domain. fanIn=${pf.gravitySignals.fanIn} cyclomatic=${pf.gravitySignals.cyclomatic} loc=${pf.gravitySignals.loc}`,
2366
3074
  hashes: { fileHash, evidenceHash: rawEvidence.map((e) => e.evidenceHash).join("-") }
2367
3075
  };
2368
3076
  });
2369
- const dest = join7(dir, "delta_targets.json");
3077
+ const dest = join8(dir, "delta_targets.json");
2370
3078
  const tmp = dest + ".tmp";
2371
- await writeFile7(tmp, JSON.stringify(deltaTargets, null, 2), "utf8");
3079
+ await writeFile8(tmp, JSON.stringify(deltaTargets, null, 2), "utf8");
2372
3080
  const { rename } = await import("fs/promises");
2373
3081
  await rename(tmp, dest);
2374
3082
  const validationReport = await buildValidationReport(store, deltaTargets, projectRoot);
2375
- await writeFile7(join7(dir, "validation_report.json"), JSON.stringify(validationReport, null, 2), "utf8");
3083
+ await writeFile8(join8(dir, "validation_report.json"), JSON.stringify(validationReport, null, 2), "utf8");
2376
3084
  for (const e of validationReport.errors) {
2377
3085
  console.error(`[vibe-splain] VALIDATION ERROR [${e.rule}] ${e.file}: ${e.detail}`);
2378
3086
  }
@@ -2508,7 +3216,7 @@ async function buildValidationReport(store, deltaTargets, projectRoot) {
2508
3216
  let secondaryTrigger = false;
2509
3217
  if (!primaryTrigger && pf.productDomain !== "payments_webhooks") {
2510
3218
  try {
2511
- const src = await readFile6(join7(projectRoot, rel), "utf8");
3219
+ const src = await readFile7(join8(projectRoot, rel), "utf8");
2512
3220
  secondaryTrigger = PAYMENT_CONTENT_TERMS.some((t) => src.includes(t));
2513
3221
  } catch {
2514
3222
  }
@@ -2567,8 +3275,9 @@ async function buildValidationReport(store, deltaTargets, projectRoot) {
2567
3275
  async function runPipeline(projectRoot) {
2568
3276
  const inv = await runInventory(projectRoot);
2569
3277
  const res = await runResolution(projectRoot, inv);
3278
+ const binding = await runActionBinding(projectRoot, inv, res);
2570
3279
  const cr = await runClassification(projectRoot, inv, res);
2571
- const scoring = await runScoring(projectRoot, cr);
3280
+ const scoring = await runScoring(projectRoot, cr, binding);
2572
3281
  await writeGraph(projectRoot, res.graph);
2573
3282
  await writeAnalysis(projectRoot, scoring.store);
2574
3283
  const files = cr.classified.filter((f) => f.isRealSource).sort((a, b) => b.gravity - a.gravity).map((f) => ({
@@ -2603,7 +3312,7 @@ async function runPipeline(projectRoot) {
2603
3312
  productDomain: f.productDomain,
2604
3313
  sideEffectProfile: f.sideEffectProfile
2605
3314
  }));
2606
- const uiUrl = `file://${join8(projectRoot, ".vibe-splainer", "ui", "index.html")}`;
3315
+ const uiUrl = `file://${join9(projectRoot, ".vibe-splainer", "ui", "index.html")}`;
2607
3316
  return {
2608
3317
  projectRoot,
2609
3318
  totalFilesScanned: cr.classified.length,
@@ -2636,7 +3345,7 @@ async function getFileAnalysis(absPath) {
2636
3345
  return null;
2637
3346
  let source;
2638
3347
  try {
2639
- source = await readFile7(absPath, "utf8");
3348
+ source = await readFile8(absPath, "utf8");
2640
3349
  } catch {
2641
3350
  return null;
2642
3351
  }
@@ -2675,16 +3384,16 @@ async function getFileAnalysis(absPath) {
2675
3384
 
2676
3385
  // ../brain/dist/dossier.js
2677
3386
  import { Mutex } from "async-mutex";
2678
- import { join as join9, dirname as dirname3 } from "path";
3387
+ import { join as join10, dirname as dirname3 } from "path";
2679
3388
  import { fileURLToPath as fileURLToPath2 } from "url";
2680
- import { readFile as readFile8, writeFile as writeFile8, mkdir as mkdir7 } from "fs/promises";
3389
+ import { readFile as readFile9, writeFile as writeFile9, mkdir as mkdir7 } from "fs/promises";
2681
3390
  import { existsSync as existsSync4, cpSync } from "fs";
2682
3391
  var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
2683
3392
  var dossierMutex = new Mutex();
2684
3393
  async function readDossier(projectRoot) {
2685
- const dossierPath = join9(projectRoot, ".vibe-splainer", "dossier.json");
3394
+ const dossierPath = join10(projectRoot, ".vibe-splainer", "dossier.json");
2686
3395
  try {
2687
- const raw = await readFile8(dossierPath, "utf8");
3396
+ const raw = await readFile9(dossierPath, "utf8");
2688
3397
  return JSON.parse(raw);
2689
3398
  } catch {
2690
3399
  return null;
@@ -2696,33 +3405,33 @@ async function writeDossier(projectRoot, dossier) {
2696
3405
  p.decisions = p.decisions.filter((c) => !(c.severity === 1 && c.category === "Convention"));
2697
3406
  p.cardCount = p.decisions.length;
2698
3407
  }
2699
- const dir = join9(projectRoot, ".vibe-splainer");
3408
+ const dir = join10(projectRoot, ".vibe-splainer");
2700
3409
  await mkdir7(dir, { recursive: true });
2701
- const dossierPath = join9(dir, "dossier.json");
3410
+ const dossierPath = join10(dir, "dossier.json");
2702
3411
  const tmp = dossierPath + ".tmp";
2703
- await writeFile8(tmp, JSON.stringify(dossier, null, 2), "utf8");
3412
+ await writeFile9(tmp, JSON.stringify(dossier, null, 2), "utf8");
2704
3413
  const { rename } = await import("fs/promises");
2705
3414
  await rename(tmp, dossierPath);
2706
3415
  await regenerateUI(projectRoot, dossier);
2707
3416
  });
2708
3417
  }
2709
3418
  async function regenerateUI(projectRoot, dossier) {
2710
- const uiDir = join9(projectRoot, ".vibe-splainer", "ui");
3419
+ const uiDir = join10(projectRoot, ".vibe-splainer", "ui");
2711
3420
  await mkdir7(uiDir, { recursive: true });
2712
- let templateDir = join9(__dirname2, "ui");
3421
+ let templateDir = join10(__dirname2, "ui");
2713
3422
  if (!existsSync4(templateDir)) {
2714
- templateDir = join9(__dirname2, "../../cli/dist/ui");
3423
+ templateDir = join10(__dirname2, "../../cli/dist/ui");
2715
3424
  }
2716
3425
  if (!existsSync4(templateDir)) {
2717
3426
  console.error("[vibe-splain] UI template not found at", templateDir, "- skipping UI regeneration");
2718
3427
  return;
2719
3428
  }
2720
3429
  cpSync(templateDir, uiDir, { recursive: true });
2721
- let html = await readFile8(join9(templateDir, "index.html"), "utf8");
3430
+ let html = await readFile9(join10(templateDir, "index.html"), "utf8");
2722
3431
  const injection = `<script>window.__VIBE_DOSSIER__ = ${JSON.stringify(dossier)};</script>`;
2723
3432
  html = html.replace("<!-- VIBE_DOSSIER_INJECTION_POINT -->", injection);
2724
- await writeFile8(join9(uiDir, "index.html"), html, "utf8");
2725
- console.error("[vibe-splain] UI regenerated at", join9(uiDir, "index.html"));
3433
+ await writeFile9(join10(uiDir, "index.html"), html, "utf8");
3434
+ console.error("[vibe-splain] UI regenerated at", join10(uiDir, "index.html"));
2726
3435
  }
2727
3436
  function validateMermaidNodeCount(diagram) {
2728
3437
  if (!diagram)
@@ -2742,8 +3451,8 @@ function validateMermaidNodeCount(diagram) {
2742
3451
  // ../brain/dist/watcher.js
2743
3452
  import chokidar from "chokidar";
2744
3453
  import { createHash as createHash2 } from "crypto";
2745
- import { readFile as readFile9 } from "fs/promises";
2746
- import { join as join10 } from "path";
3454
+ import { readFile as readFile10 } from "fs/promises";
3455
+ import { join as join11 } from "path";
2747
3456
  function startWatcher(projectRoot, watchedPaths) {
2748
3457
  const watcher = chokidar.watch(watchedPaths.length > 0 ? watchedPaths : projectRoot, {
2749
3458
  ignoreInitial: true,
@@ -2755,14 +3464,14 @@ function startWatcher(projectRoot, watchedPaths) {
2755
3464
  const dossier = await readDossier(projectRoot);
2756
3465
  if (!dossier)
2757
3466
  return;
2758
- const content = await readFile9(filepath, "utf8");
3467
+ const content = await readFile10(filepath, "utf8");
2759
3468
  const newHash = createHash2("sha256").update(content).digest("hex");
2760
3469
  let mutated = false;
2761
3470
  for (const pillar of dossier.pillars) {
2762
3471
  for (const card of pillar.decisions) {
2763
3472
  if (!card.primaryFile)
2764
3473
  continue;
2765
- const absMatch = filepath === join10(projectRoot, card.primaryFile) || filepath.endsWith("/" + card.primaryFile);
3474
+ const absMatch = filepath === join11(projectRoot, card.primaryFile) || filepath.endsWith("/" + card.primaryFile);
2766
3475
  if (absMatch && card.lastScannedHash !== newHash) {
2767
3476
  card.status = "stale";
2768
3477
  const rel = card.primaryFile;
@@ -2932,8 +3641,8 @@ async function handleSetProjectBrief(args) {
2932
3641
  }
2933
3642
 
2934
3643
  // dist/mcp/tools/get_file_context.js
2935
- import { readFile as readFile10 } from "fs/promises";
2936
- import { join as join11, relative as relative3, isAbsolute } from "path";
3644
+ import { readFile as readFile11 } from "fs/promises";
3645
+ import { join as join12, relative as relative3, isAbsolute } from "path";
2937
3646
  var getFileContextTool = {
2938
3647
  name: "get_file_context",
2939
3648
  description: "Returns PRE-EXTRACTED evidence for a file so you do not have to read the whole thing and paraphrase its header comment. Returns: gravity/heat scores + signals, importedBy (named fan-in \u2014 use this for blastRadius), hotSpans (the gnarliest function bodies, comment-stripped, each with a reason), smellSpans (located tech debt with \xB13 lines of context), and signature (the exported API surface). Base your evidence on hotSpans/smellSpans \u2014 NEVER on header comments. Pass { full: true } only if you truly need the raw source.",
@@ -2953,7 +3662,7 @@ async function handleGetFileContext(args) {
2953
3662
  const full = args.full === true;
2954
3663
  if (!projectRoot || !filePath)
2955
3664
  throw new Error("projectRoot and filePath are required");
2956
- const fullPath = isAbsolute(filePath) ? filePath : join11(projectRoot, filePath);
3665
+ const fullPath = isAbsolute(filePath) ? filePath : join12(projectRoot, filePath);
2957
3666
  const relPath = relative3(projectRoot, fullPath);
2958
3667
  const evidence = await getFileAnalysis(fullPath);
2959
3668
  if (!evidence) {
@@ -2978,7 +3687,7 @@ async function handleGetFileContext(args) {
2978
3687
  smellSpans: evidence.smellSpans
2979
3688
  };
2980
3689
  if (full) {
2981
- result.source = await readFile10(fullPath, "utf8");
3690
+ result.source = await readFile11(fullPath, "utf8");
2982
3691
  }
2983
3692
  return result;
2984
3693
  }
@@ -2986,8 +3695,8 @@ async function handleGetFileContext(args) {
2986
3695
  // dist/mcp/tools/write_decision_card.js
2987
3696
  import { v4 as uuidv4 } from "uuid";
2988
3697
  import { createHash as createHash3 } from "crypto";
2989
- import { readFile as readFile11 } from "fs/promises";
2990
- import { join as join12 } from "path";
3698
+ import { readFile as readFile12 } from "fs/promises";
3699
+ import { join as join13 } from "path";
2991
3700
  var CATEGORIES = ["Bottleneck", "Hack", "Smart-Move", "Risk", "Convention", "Dead-Weight"];
2992
3701
  function normalizeSnippet(s) {
2993
3702
  let out = (s ?? "").replace(/\r\n/g, "\n");
@@ -3079,7 +3788,7 @@ async function handleWriteDecisionCard(args) {
3079
3788
  const heat = persisted ? Math.round(persisted.heat) : void 0;
3080
3789
  let primaryContent = "";
3081
3790
  try {
3082
- primaryContent = await readFile11(join12(projectRoot, primaryFile), "utf8");
3791
+ primaryContent = await readFile12(join13(projectRoot, primaryFile), "utf8");
3083
3792
  } catch {
3084
3793
  }
3085
3794
  const hash = createHash3("sha256").update(primaryContent).digest("hex");
@@ -3299,12 +4008,62 @@ async function handleMarkStale(args) {
3299
4008
  };
3300
4009
  }
3301
4010
 
4011
+ // dist/mcp/tools/get_call_chain.js
4012
+ var getCallChainTool = {
4013
+ name: "get_call_chain",
4014
+ description: `Trace how behavior is reached from an entrypoint by following function call edges through the codebase. Returns a step-by-step chain with exact function names, file paths, line numbers, action kinds, and evidence text. Every edge has a confidence level; unresolved edges are listed explicitly.
4015
+
4016
+ Use structured filters when you know the target:
4017
+ targetModel + targetOperation: "where does Booking get created?"
4018
+ targetActionKind: "where is auth enforced?"
4019
+ targetFunctionName: "how is function X reached?"
4020
+ No filter returns the full call tree up to maxDepth.
4021
+
4022
+ Run scan_project first \u2014 this tool reads from the generated action_bindings.json.`,
4023
+ inputSchema: {
4024
+ type: "object",
4025
+ properties: {
4026
+ projectRoot: { type: "string" },
4027
+ entrypointPath: { type: "string", description: "Relative path to the entrypoint file" },
4028
+ maxDepth: { type: "number", description: "Max traversal depth. Default 6, max 12." },
4029
+ targetActionKind: { type: "string", description: "Stop at this semantic action kind." },
4030
+ targetModel: { type: "string", description: "Stop at functions touching this model." },
4031
+ targetOperation: { type: "string", description: "Narrow targetModel to this operation." },
4032
+ targetFunctionName: { type: "string", description: "Stop at a specific function name." },
4033
+ includeTests: { type: "boolean", description: "Include test files in traversal. Default false." }
4034
+ },
4035
+ required: ["projectRoot", "entrypointPath"]
4036
+ }
4037
+ };
4038
+ async function handleGetCallChain(args) {
4039
+ const projectRoot = args.projectRoot;
4040
+ const entrypointPath = args.entrypointPath;
4041
+ if (!projectRoot || !entrypointPath)
4042
+ throw new Error("projectRoot and entrypointPath are required");
4043
+ const getCallChainArgs = {
4044
+ entrypointPath,
4045
+ maxDepth: typeof args.maxDepth === "number" ? args.maxDepth : void 0,
4046
+ targetActionKind: typeof args.targetActionKind === "string" ? args.targetActionKind : void 0,
4047
+ targetModel: typeof args.targetModel === "string" ? args.targetModel : void 0,
4048
+ targetOperation: typeof args.targetOperation === "string" ? args.targetOperation : void 0,
4049
+ targetFunctionName: typeof args.targetFunctionName === "string" ? args.targetFunctionName : void 0,
4050
+ includeTests: typeof args.includeTests === "boolean" ? args.includeTests : void 0
4051
+ };
4052
+ try {
4053
+ const result = await traverseCallChain(projectRoot, getCallChainArgs);
4054
+ return result;
4055
+ } catch (error) {
4056
+ throw new Error(`get_call_chain failed: ${error instanceof Error ? error.message : String(error)}`);
4057
+ }
4058
+ }
4059
+
3302
4060
  // dist/mcp/server.js
3303
4061
  var ALL_TOOLS = [
3304
4062
  scanProjectTool,
3305
4063
  getProjectMapTool,
3306
4064
  setProjectBriefTool,
3307
4065
  getFileContextTool,
4066
+ getCallChainTool,
3308
4067
  writeDecisionCardTool,
3309
4068
  getStrategicOverviewTool,
3310
4069
  inspectPillarTool,
@@ -3316,6 +4075,7 @@ var TOOL_HANDLERS = {
3316
4075
  get_project_map: handleGetProjectMap,
3317
4076
  set_project_brief: handleSetProjectBrief,
3318
4077
  get_file_context: handleGetFileContext,
4078
+ get_call_chain: handleGetCallChain,
3319
4079
  write_decision_card: handleWriteDecisionCard,
3320
4080
  get_strategic_overview: handleGetStrategicOverview,
3321
4081
  inspect_pillar: handleInspectPillar,
@@ -11,6 +11,7 @@ import { handleGetStrategicOverview, getStrategicOverviewTool } from './tools/ge
11
11
  import { handleInspectPillar, inspectPillarTool } from './tools/inspect_pillar.js';
12
12
  import { handleGetWildDiscoveries, getWildDiscoveriesTool } from './tools/get_wild_discoveries.js';
13
13
  import { handleMarkStale, markStaleTool } from './tools/mark_stale.js';
14
+ import { handleGetCallChain, getCallChainTool } from './tools/get_call_chain.js';
14
15
  // ⚠️ CRITICAL: Never use console.log() anywhere in this codebase.
15
16
  // stdout is owned by the MCP SDK for protocol messages.
16
17
  // Use console.error() for all diagnostic output.
@@ -19,6 +20,7 @@ const ALL_TOOLS = [
19
20
  getProjectMapTool,
20
21
  setProjectBriefTool,
21
22
  getFileContextTool,
23
+ getCallChainTool,
22
24
  writeDecisionCardTool,
23
25
  getStrategicOverviewTool,
24
26
  inspectPillarTool,
@@ -30,6 +32,7 @@ const TOOL_HANDLERS = {
30
32
  get_project_map: handleGetProjectMap,
31
33
  set_project_brief: handleSetProjectBrief,
32
34
  get_file_context: handleGetFileContext,
35
+ get_call_chain: handleGetCallChain,
33
36
  write_decision_card: handleWriteDecisionCard,
34
37
  get_strategic_overview: handleGetStrategicOverview,
35
38
  inspect_pillar: handleInspectPillar,
@@ -0,0 +1,42 @@
1
+ export declare const getCallChainTool: {
2
+ name: string;
3
+ description: string;
4
+ inputSchema: {
5
+ type: "object";
6
+ properties: {
7
+ projectRoot: {
8
+ type: string;
9
+ };
10
+ entrypointPath: {
11
+ type: string;
12
+ description: string;
13
+ };
14
+ maxDepth: {
15
+ type: string;
16
+ description: string;
17
+ };
18
+ targetActionKind: {
19
+ type: string;
20
+ description: string;
21
+ };
22
+ targetModel: {
23
+ type: string;
24
+ description: string;
25
+ };
26
+ targetOperation: {
27
+ type: string;
28
+ description: string;
29
+ };
30
+ targetFunctionName: {
31
+ type: string;
32
+ description: string;
33
+ };
34
+ includeTests: {
35
+ type: string;
36
+ description: string;
37
+ };
38
+ };
39
+ required: string[];
40
+ };
41
+ };
42
+ export declare function handleGetCallChain(args: Record<string, unknown>): Promise<unknown>;
@@ -0,0 +1,50 @@
1
+ import { traverseCallChain } from '@vibe-splain/brain';
2
+ export const getCallChainTool = {
3
+ name: 'get_call_chain',
4
+ description: `Trace how behavior is reached from an entrypoint by following function call edges through the codebase. Returns a step-by-step chain with exact function names, file paths, line numbers, action kinds, and evidence text. Every edge has a confidence level; unresolved edges are listed explicitly.
5
+
6
+ Use structured filters when you know the target:
7
+ targetModel + targetOperation: "where does Booking get created?"
8
+ targetActionKind: "where is auth enforced?"
9
+ targetFunctionName: "how is function X reached?"
10
+ No filter returns the full call tree up to maxDepth.
11
+
12
+ Run scan_project first — this tool reads from the generated action_bindings.json.`,
13
+ inputSchema: {
14
+ type: 'object',
15
+ properties: {
16
+ projectRoot: { type: 'string' },
17
+ entrypointPath: { type: 'string', description: 'Relative path to the entrypoint file' },
18
+ maxDepth: { type: 'number', description: 'Max traversal depth. Default 6, max 12.' },
19
+ targetActionKind: { type: 'string', description: 'Stop at this semantic action kind.' },
20
+ targetModel: { type: 'string', description: 'Stop at functions touching this model.' },
21
+ targetOperation: { type: 'string', description: 'Narrow targetModel to this operation.' },
22
+ targetFunctionName: { type: 'string', description: 'Stop at a specific function name.' },
23
+ includeTests: { type: 'boolean', description: 'Include test files in traversal. Default false.' },
24
+ },
25
+ required: ['projectRoot', 'entrypointPath'],
26
+ },
27
+ };
28
+ export async function handleGetCallChain(args) {
29
+ const projectRoot = args.projectRoot;
30
+ const entrypointPath = args.entrypointPath;
31
+ if (!projectRoot || !entrypointPath)
32
+ throw new Error('projectRoot and entrypointPath are required');
33
+ const getCallChainArgs = {
34
+ entrypointPath,
35
+ maxDepth: typeof args.maxDepth === 'number' ? args.maxDepth : undefined,
36
+ targetActionKind: typeof args.targetActionKind === 'string' ? args.targetActionKind : undefined,
37
+ targetModel: typeof args.targetModel === 'string' ? args.targetModel : undefined,
38
+ targetOperation: typeof args.targetOperation === 'string' ? args.targetOperation : undefined,
39
+ targetFunctionName: typeof args.targetFunctionName === 'string' ? args.targetFunctionName : undefined,
40
+ includeTests: typeof args.includeTests === 'boolean' ? args.includeTests : undefined,
41
+ };
42
+ try {
43
+ const result = await traverseCallChain(projectRoot, getCallChainArgs);
44
+ return result;
45
+ }
46
+ catch (error) {
47
+ throw new Error(`get_call_chain failed: ${error instanceof Error ? error.message : String(error)}`);
48
+ }
49
+ }
50
+ //# sourceMappingURL=get_call_chain.js.map
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibe-splain",
3
- "version": "2.6.0",
3
+ "version": "2.7.0",
4
4
  "description": "Architectural dossier engine for vibe-coded TypeScript/JavaScript projects. Runs as an MCP server inside your coding agent.",
5
5
  "type": "module",
6
6
  "license": "MIT",