vibe-splain 2.6.0 → 2.7.1
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 +818 -38
- package/dist/mcp/server.js +3 -0
- package/dist/mcp/tools/get_call_chain.d.ts +42 -0
- package/dist/mcp/tools/get_call_chain.js +50 -0
- package/package.json +1 -1
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
|
|
92
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
93
93
|
|
|
94
94
|
// ../brain/dist/pipeline/orchestrator.js
|
|
95
|
-
import { join as
|
|
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,528 @@ 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/
|
|
2173
|
+
// ../brain/dist/pipeline/binding.js
|
|
2055
2174
|
import { join as join7 } from "path";
|
|
2056
|
-
import { writeFile as writeFile7,
|
|
2175
|
+
import { writeFile as writeFile7, readFile as readFile6 } from "fs/promises";
|
|
2176
|
+
var FUNCTION_TYPES2 = /* @__PURE__ */ new Set([
|
|
2177
|
+
"function_declaration",
|
|
2178
|
+
"function_expression",
|
|
2179
|
+
"arrow_function",
|
|
2180
|
+
"method_definition",
|
|
2181
|
+
"function_definition",
|
|
2182
|
+
"method_declaration",
|
|
2183
|
+
"func_literal",
|
|
2184
|
+
"function_item",
|
|
2185
|
+
"closure_expression",
|
|
2186
|
+
"constructor_declaration",
|
|
2187
|
+
"generator_function_declaration",
|
|
2188
|
+
"generator_function"
|
|
2189
|
+
]);
|
|
2190
|
+
function firstLine2(s) {
|
|
2191
|
+
return s.split("\n")[0].trim();
|
|
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
|
+
}
|
|
2201
|
+
function resolveCallee(node) {
|
|
2202
|
+
if (node.type === "identifier") {
|
|
2203
|
+
return { root: node.text, prop: null };
|
|
2204
|
+
}
|
|
2205
|
+
if (node.type === "member_expression") {
|
|
2206
|
+
const obj = node.childForFieldName("object");
|
|
2207
|
+
const prop = node.childForFieldName("property");
|
|
2208
|
+
if (obj && prop) {
|
|
2209
|
+
const nested = resolveCallee(obj);
|
|
2210
|
+
if (nested) {
|
|
2211
|
+
return { root: nested.root, prop: nested.prop ? `${nested.prop}.${prop.text}` : prop.text };
|
|
2212
|
+
}
|
|
2213
|
+
}
|
|
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
|
+
}
|
|
2220
|
+
return null;
|
|
2221
|
+
}
|
|
2222
|
+
async function runActionBinding(projectRoot, inv, res) {
|
|
2223
|
+
const artifact = {
|
|
2224
|
+
schemaVersion: 1,
|
|
2225
|
+
projectRoot,
|
|
2226
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2227
|
+
files: {},
|
|
2228
|
+
functionIndex: {},
|
|
2229
|
+
actionIndex: {},
|
|
2230
|
+
entrypointIndex: {}
|
|
2231
|
+
};
|
|
2232
|
+
let filesProcessed = 0;
|
|
2233
|
+
let functionsExtracted = 0;
|
|
2234
|
+
let callsExtracted = 0;
|
|
2235
|
+
let callsResolved = 0;
|
|
2236
|
+
let semanticActionsExtracted = 0;
|
|
2237
|
+
let entrypointsFound = 0;
|
|
2238
|
+
let namedImportsExtracted = 0;
|
|
2239
|
+
for (const w of inv.work) {
|
|
2240
|
+
if (w.pathDemote)
|
|
2241
|
+
continue;
|
|
2242
|
+
filesProcessed++;
|
|
2243
|
+
const filePath = w.rel;
|
|
2244
|
+
const imports = [];
|
|
2245
|
+
for (const raw of w.rawNamedImports) {
|
|
2246
|
+
namedImportsExtracted++;
|
|
2247
|
+
const { resolved, isAlias } = resolveImportWithAliasMap(raw.moduleSpecifier, w.abs, w.lang, projectRoot, inv.fileSet, inv.basenameIndex, res.aliasMap);
|
|
2248
|
+
let confidence = "low";
|
|
2249
|
+
if (resolved)
|
|
2250
|
+
confidence = "high";
|
|
2251
|
+
else if (isAlias)
|
|
2252
|
+
confidence = "medium";
|
|
2253
|
+
imports.push({
|
|
2254
|
+
localName: raw.localName,
|
|
2255
|
+
importedName: raw.importedName,
|
|
2256
|
+
moduleSpecifier: raw.moduleSpecifier,
|
|
2257
|
+
resolvedFilePath: resolved,
|
|
2258
|
+
importKind: raw.importKind,
|
|
2259
|
+
isTypeOnly: raw.isTypeOnly,
|
|
2260
|
+
sourceLine: raw.sourceLine,
|
|
2261
|
+
confidence,
|
|
2262
|
+
evidenceText: raw.rawText.slice(0, 200)
|
|
2263
|
+
});
|
|
2264
|
+
}
|
|
2265
|
+
const functions = [];
|
|
2266
|
+
const nodeToRecord = /* @__PURE__ */ new Map();
|
|
2267
|
+
if (!w.tree)
|
|
2268
|
+
continue;
|
|
2269
|
+
const allNodes = [];
|
|
2270
|
+
walkNodes(w.tree.rootNode, (n) => {
|
|
2271
|
+
allNodes.push(n);
|
|
2272
|
+
});
|
|
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;
|
|
2282
|
+
functionsExtracted++;
|
|
2283
|
+
let displayName = "";
|
|
2284
|
+
let nameSource = "position_fallback";
|
|
2285
|
+
const p = node.parent;
|
|
2286
|
+
if (node.childForFieldName("name")) {
|
|
2287
|
+
displayName = node.childForFieldName("name").text;
|
|
2288
|
+
nameSource = node.type === "method_definition" ? "method_definition" : "function_declaration";
|
|
2289
|
+
} else if (p?.type === "variable_declarator" && p.childForFieldName("name")) {
|
|
2290
|
+
displayName = p.childForFieldName("name").text;
|
|
2291
|
+
nameSource = "parent_variable_declarator";
|
|
2292
|
+
} else if (p?.type === "assignment_expression" && p.childForFieldName("left")) {
|
|
2293
|
+
displayName = p.childForFieldName("left").text;
|
|
2294
|
+
nameSource = "parent_assignment";
|
|
2295
|
+
} else if (p?.type === "pair" && p.childForFieldName("key")) {
|
|
2296
|
+
displayName = p.childForFieldName("key").text;
|
|
2297
|
+
nameSource = "object_property_key";
|
|
2298
|
+
} else if (p?.type === "export_statement") {
|
|
2299
|
+
const idNode = p.children.find((c) => c.type === "identifier");
|
|
2300
|
+
if (idNode) {
|
|
2301
|
+
displayName = idNode.text;
|
|
2302
|
+
nameSource = "export_const";
|
|
2303
|
+
}
|
|
2304
|
+
} else if (p?.type === "lexical_declaration" || p?.type === "variable_declaration") {
|
|
2305
|
+
const decl = p.children.find((c) => c.type === "variable_declarator");
|
|
2306
|
+
if (decl && decl.childForFieldName("name")) {
|
|
2307
|
+
displayName = decl.childForFieldName("name").text;
|
|
2308
|
+
nameSource = "parent_variable_declarator";
|
|
2309
|
+
}
|
|
2310
|
+
}
|
|
2311
|
+
if (!displayName) {
|
|
2312
|
+
displayName = `anonymous@${startLine}:${startCol}`;
|
|
2313
|
+
nameSource = "position_fallback";
|
|
2314
|
+
}
|
|
2315
|
+
const functionId = `${filePath}::${displayName}::${startLine}:${startCol}`;
|
|
2316
|
+
let isExported = false;
|
|
2317
|
+
if (p?.type === "export_statement")
|
|
2318
|
+
isExported = true;
|
|
2319
|
+
if (p?.parent?.type === "export_statement")
|
|
2320
|
+
isExported = true;
|
|
2321
|
+
if (w.ast.exportedNames.includes(displayName))
|
|
2322
|
+
isExported = true;
|
|
2323
|
+
const isEntrypoint = (w.frameworkRole.includes("route") || w.frameworkRole.includes("page")) && isExported;
|
|
2324
|
+
if (isEntrypoint)
|
|
2325
|
+
entrypointsFound++;
|
|
2326
|
+
const fnRecord = {
|
|
2327
|
+
functionId,
|
|
2328
|
+
displayName,
|
|
2329
|
+
nameSource,
|
|
2330
|
+
functionKind: node.type,
|
|
2331
|
+
filePath,
|
|
2332
|
+
startLine,
|
|
2333
|
+
endLine: node.endPosition.row + 1,
|
|
2334
|
+
startCol,
|
|
2335
|
+
isExported,
|
|
2336
|
+
isEntrypoint,
|
|
2337
|
+
calls: [],
|
|
2338
|
+
semanticActions: [],
|
|
2339
|
+
evidenceText: firstLine2(node.text).slice(0, 200)
|
|
2340
|
+
};
|
|
2341
|
+
functions.push(fnRecord);
|
|
2342
|
+
nodeToRecord.set(node.id, fnRecord);
|
|
2343
|
+
}
|
|
2344
|
+
for (const node of allNodes) {
|
|
2345
|
+
if (node.type !== "call_expression" && node.type !== "call")
|
|
2346
|
+
continue;
|
|
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;
|
|
2354
|
+
}
|
|
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];
|
|
2389
|
+
}
|
|
2390
|
+
}
|
|
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) {
|
|
2440
|
+
isSemantic = true;
|
|
2441
|
+
actionKind = "email_send";
|
|
2442
|
+
}
|
|
2443
|
+
}
|
|
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);
|
|
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";
|
|
2484
|
+
} else {
|
|
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";
|
|
2489
|
+
confidence = "high";
|
|
2490
|
+
} else {
|
|
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";
|
|
2496
|
+
}
|
|
2497
|
+
}
|
|
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
|
+
});
|
|
2515
|
+
}
|
|
2516
|
+
}
|
|
2517
|
+
artifact.files[filePath] = {
|
|
2518
|
+
filePath,
|
|
2519
|
+
language: w.lang,
|
|
2520
|
+
sourceRole: w.frameworkRole === "test" ? "test" : "production",
|
|
2521
|
+
imports,
|
|
2522
|
+
functions
|
|
2523
|
+
};
|
|
2524
|
+
for (const fn of functions) {
|
|
2525
|
+
artifact.functionIndex[fn.functionId] = {
|
|
2526
|
+
filePath,
|
|
2527
|
+
displayName: fn.displayName,
|
|
2528
|
+
startLine: fn.startLine,
|
|
2529
|
+
endLine: fn.endLine
|
|
2530
|
+
};
|
|
2531
|
+
if (fn.isEntrypoint) {
|
|
2532
|
+
if (!artifact.entrypointIndex[filePath])
|
|
2533
|
+
artifact.entrypointIndex[filePath] = [];
|
|
2534
|
+
artifact.entrypointIndex[filePath].push(fn.functionId);
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
for (const fileRec of Object.values(artifact.files)) {
|
|
2539
|
+
for (const fnRec of fileRec.functions) {
|
|
2540
|
+
for (const callRec of fnRec.calls) {
|
|
2541
|
+
if (callRec.resolutionKind === "named_import_match" && callRec.resolvedFilePath) {
|
|
2542
|
+
const targetFile = artifact.files[callRec.resolvedFilePath];
|
|
2543
|
+
if (targetFile) {
|
|
2544
|
+
const targetFn = targetFile.functions.find((f) => f.displayName === callRec.calleeRoot);
|
|
2545
|
+
if (targetFn) {
|
|
2546
|
+
callRec.resolvedTargetFunctionId = targetFn.functionId;
|
|
2547
|
+
}
|
|
2548
|
+
}
|
|
2549
|
+
}
|
|
2550
|
+
}
|
|
2551
|
+
}
|
|
2552
|
+
}
|
|
2553
|
+
await writeFile7(join7(projectRoot, ".vibe-splainer", "action_bindings.json"), JSON.stringify(artifact, null, 2), "utf8");
|
|
2554
|
+
const summary = {
|
|
2555
|
+
filesProcessed,
|
|
2556
|
+
functionsExtracted,
|
|
2557
|
+
callsExtracted,
|
|
2558
|
+
callsResolved,
|
|
2559
|
+
semanticActionsExtracted,
|
|
2560
|
+
entrypointsFound,
|
|
2561
|
+
namedImportsExtracted
|
|
2562
|
+
};
|
|
2563
|
+
await writeFile7(join7(projectRoot, ".vibe-splainer", "stage-09-action-bindings-summary.json"), JSON.stringify(summary, null, 2), "utf8");
|
|
2564
|
+
return { artifact };
|
|
2565
|
+
}
|
|
2566
|
+
async function traverseCallChain(projectRoot, args) {
|
|
2567
|
+
const artifactPath = join7(projectRoot, ".vibe-splainer", "action_bindings.json");
|
|
2568
|
+
let artifact;
|
|
2569
|
+
try {
|
|
2570
|
+
const raw = await readFile6(artifactPath, "utf8");
|
|
2571
|
+
artifact = JSON.parse(raw);
|
|
2572
|
+
} catch {
|
|
2573
|
+
throw new Error("action_bindings.json not found. Run scan_project first.");
|
|
2574
|
+
}
|
|
2575
|
+
const { entrypointPath, maxDepth = 6, targetActionKind, targetModel, targetOperation, targetFunctionName, includeTests = false } = args;
|
|
2576
|
+
let seedFunctionIds = [];
|
|
2577
|
+
if (artifact.entrypointIndex[entrypointPath]) {
|
|
2578
|
+
seedFunctionIds = artifact.entrypointIndex[entrypointPath];
|
|
2579
|
+
} else if (artifact.files[entrypointPath]) {
|
|
2580
|
+
const fileRec = artifact.files[entrypointPath];
|
|
2581
|
+
seedFunctionIds = fileRec.functions.filter((f) => f.isEntrypoint).map((f) => f.functionId);
|
|
2582
|
+
if (seedFunctionIds.length === 0) {
|
|
2583
|
+
const firstExported = fileRec.functions.find((f) => f.isExported);
|
|
2584
|
+
if (firstExported)
|
|
2585
|
+
seedFunctionIds.push(firstExported.functionId);
|
|
2586
|
+
}
|
|
2587
|
+
}
|
|
2588
|
+
if (seedFunctionIds.length === 0) {
|
|
2589
|
+
throw new Error(`No entrypoint functions found in ${entrypointPath}`);
|
|
2590
|
+
}
|
|
2591
|
+
const chain = [];
|
|
2592
|
+
const unresolvedEdges = [];
|
|
2593
|
+
const visited = /* @__PURE__ */ new Set();
|
|
2594
|
+
const queue = seedFunctionIds.map((id) => ({ functionId: id, depth: 0 }));
|
|
2595
|
+
let targetReached = false;
|
|
2596
|
+
let truncatedAtDepth = false;
|
|
2597
|
+
while (queue.length > 0) {
|
|
2598
|
+
const { functionId, depth } = queue.shift();
|
|
2599
|
+
if (visited.has(functionId))
|
|
2600
|
+
continue;
|
|
2601
|
+
visited.add(functionId);
|
|
2602
|
+
const indexEntry = artifact.functionIndex[functionId];
|
|
2603
|
+
if (!indexEntry)
|
|
2604
|
+
continue;
|
|
2605
|
+
const fileRec = artifact.files[indexEntry.filePath];
|
|
2606
|
+
if (!fileRec)
|
|
2607
|
+
continue;
|
|
2608
|
+
if (!includeTests && fileRec.sourceRole === "test")
|
|
2609
|
+
continue;
|
|
2610
|
+
const fnRec = fileRec.functions.find((f) => f.functionId === functionId);
|
|
2611
|
+
if (!fnRec)
|
|
2612
|
+
continue;
|
|
2613
|
+
for (const call of fnRec.calls) {
|
|
2614
|
+
if (call.resolutionKind === "semantic_action_only")
|
|
2615
|
+
continue;
|
|
2616
|
+
if (call.resolvedTargetFunctionId) {
|
|
2617
|
+
if (depth < maxDepth) {
|
|
2618
|
+
queue.push({ functionId: call.resolvedTargetFunctionId, depth: depth + 1 });
|
|
2619
|
+
let isTarget = false;
|
|
2620
|
+
if (targetFunctionName && call.calleeRoot === targetFunctionName)
|
|
2621
|
+
isTarget = true;
|
|
2622
|
+
if (isTarget)
|
|
2623
|
+
targetReached = true;
|
|
2624
|
+
chain.push({
|
|
2625
|
+
functionId: call.resolvedTargetFunctionId,
|
|
2626
|
+
displayName: call.calleeRoot,
|
|
2627
|
+
filePath: call.resolvedFilePath || "unknown",
|
|
2628
|
+
startLine: call.sourceLine,
|
|
2629
|
+
edgeKind: "call_edge",
|
|
2630
|
+
confidence: call.confidence,
|
|
2631
|
+
evidenceText: call.evidenceText,
|
|
2632
|
+
isTarget,
|
|
2633
|
+
depth
|
|
2634
|
+
});
|
|
2635
|
+
} else {
|
|
2636
|
+
unresolvedEdges.push({
|
|
2637
|
+
fromFunctionId: functionId,
|
|
2638
|
+
calleeText: call.calleeText,
|
|
2639
|
+
sourceLine: call.sourceLine,
|
|
2640
|
+
reason: "depth limit reached"
|
|
2641
|
+
});
|
|
2642
|
+
truncatedAtDepth = true;
|
|
2643
|
+
}
|
|
2644
|
+
} else {
|
|
2645
|
+
unresolvedEdges.push({
|
|
2646
|
+
fromFunctionId: functionId,
|
|
2647
|
+
calleeText: call.calleeText,
|
|
2648
|
+
sourceLine: call.sourceLine,
|
|
2649
|
+
reason: call.resolutionKind
|
|
2650
|
+
});
|
|
2651
|
+
}
|
|
2652
|
+
}
|
|
2653
|
+
for (const action of fnRec.semanticActions) {
|
|
2654
|
+
let isTarget = false;
|
|
2655
|
+
if (targetActionKind && action.actionKind === targetActionKind) {
|
|
2656
|
+
isTarget = true;
|
|
2657
|
+
if (targetModel && action.targetModel !== targetModel)
|
|
2658
|
+
isTarget = false;
|
|
2659
|
+
if (targetOperation && action.targetOperation !== targetOperation)
|
|
2660
|
+
isTarget = false;
|
|
2661
|
+
} else if (targetModel && action.targetModel === targetModel) {
|
|
2662
|
+
isTarget = true;
|
|
2663
|
+
if (targetOperation && action.targetOperation !== targetOperation)
|
|
2664
|
+
isTarget = false;
|
|
2665
|
+
}
|
|
2666
|
+
if (isTarget)
|
|
2667
|
+
targetReached = true;
|
|
2668
|
+
chain.push({
|
|
2669
|
+
functionId: action.sourceFunctionId,
|
|
2670
|
+
displayName: action.calleeText,
|
|
2671
|
+
filePath: fileRec.filePath,
|
|
2672
|
+
startLine: action.sourceLine,
|
|
2673
|
+
edgeKind: "semantic_action",
|
|
2674
|
+
actionKind: action.actionKind,
|
|
2675
|
+
targetModel: action.targetModel || void 0,
|
|
2676
|
+
targetOperation: action.targetOperation || void 0,
|
|
2677
|
+
confidence: action.confidence,
|
|
2678
|
+
evidenceText: action.evidenceText,
|
|
2679
|
+
isTarget,
|
|
2680
|
+
depth
|
|
2681
|
+
});
|
|
2682
|
+
}
|
|
2683
|
+
}
|
|
2684
|
+
return {
|
|
2685
|
+
targetReached,
|
|
2686
|
+
truncatedAtDepth,
|
|
2687
|
+
chain,
|
|
2688
|
+
unresolvedEdges
|
|
2689
|
+
};
|
|
2690
|
+
}
|
|
2691
|
+
|
|
2692
|
+
// ../brain/dist/pipeline/scoring.js
|
|
2693
|
+
import { join as join8 } from "path";
|
|
2694
|
+
import { writeFile as writeFile8, mkdir as mkdir6, readFile as readFile7 } from "fs/promises";
|
|
2057
2695
|
import { createHash } from "crypto";
|
|
2058
2696
|
function computeSeverity(sideEffectProfile, productDomain, gravity, heat, maxNesting, hasLongFunctions, swallowedCatches, runtimeEntrypoints) {
|
|
2059
2697
|
let score = 0;
|
|
@@ -2271,9 +2909,17 @@ function deriveConfidence(fanIn, gravity) {
|
|
|
2271
2909
|
return "medium";
|
|
2272
2910
|
return "low";
|
|
2273
2911
|
}
|
|
2274
|
-
async function runScoring(projectRoot, cr) {
|
|
2275
|
-
const dir =
|
|
2912
|
+
async function runScoring(projectRoot, cr, binding) {
|
|
2913
|
+
const dir = join8(projectRoot, ".vibe-splainer");
|
|
2276
2914
|
await mkdir6(dir, { recursive: true });
|
|
2915
|
+
let bindingArtifact = binding?.artifact;
|
|
2916
|
+
if (!bindingArtifact) {
|
|
2917
|
+
try {
|
|
2918
|
+
const raw = await readFile7(join8(projectRoot, ".vibe-splainer", "action_bindings.json"), "utf8");
|
|
2919
|
+
bindingArtifact = JSON.parse(raw);
|
|
2920
|
+
} catch {
|
|
2921
|
+
}
|
|
2922
|
+
}
|
|
2277
2923
|
const persisted = {};
|
|
2278
2924
|
const severityBreakdowns = {};
|
|
2279
2925
|
for (const f of cr.classified) {
|
|
@@ -2307,7 +2953,7 @@ async function runScoring(projectRoot, cr) {
|
|
|
2307
2953
|
severityBreakdowns[f.rel] = `severity=${pf.canonicalSeverity} loadBearing=${pf.canonicalLoadBearing} effects=${pf.sideEffectProfile.join(",")} domain=${pf.productDomain}`;
|
|
2308
2954
|
}
|
|
2309
2955
|
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
|
|
2956
|
+
await writeFile8(join8(dir, "stage-09-severity.json"), JSON.stringify(stage09, null, 2), "utf8");
|
|
2311
2957
|
const store = { files: persisted };
|
|
2312
2958
|
const importedByMapForDelta = /* @__PURE__ */ new Map();
|
|
2313
2959
|
for (const [rel, pf] of Object.entries(persisted)) {
|
|
@@ -2338,6 +2984,87 @@ async function runScoring(projectRoot, cr) {
|
|
|
2338
2984
|
excerpt: span.snippet,
|
|
2339
2985
|
isTruncated: span.rawExcerpt.length > 2e3
|
|
2340
2986
|
}));
|
|
2987
|
+
let criticalFunctions = void 0;
|
|
2988
|
+
if (bindingArtifact) {
|
|
2989
|
+
const fileBinding = bindingArtifact.files[pf.relativePath];
|
|
2990
|
+
if (fileBinding) {
|
|
2991
|
+
const scoredFunctions = fileBinding.functions.map((fn) => {
|
|
2992
|
+
let fnScore = 0;
|
|
2993
|
+
const reasons = [];
|
|
2994
|
+
if (fn.semanticActions.length > 0) {
|
|
2995
|
+
fnScore += 3;
|
|
2996
|
+
reasons.push("Contains semantic actions");
|
|
2997
|
+
}
|
|
2998
|
+
if (fn.isEntrypoint) {
|
|
2999
|
+
fnScore += 2;
|
|
3000
|
+
reasons.push("Is a framework entrypoint");
|
|
3001
|
+
}
|
|
3002
|
+
const resolvedOutbound = fn.calls.filter((c) => c.resolvedTargetFunctionId).length;
|
|
3003
|
+
if (resolvedOutbound > 0) {
|
|
3004
|
+
const callPts = Math.min(3, resolvedOutbound);
|
|
3005
|
+
fnScore += callPts;
|
|
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`);
|
|
3010
|
+
}
|
|
3011
|
+
const writesModel = fn.semanticActions.some((a) => a.actionKind === "database_write" && a.targetModel);
|
|
3012
|
+
if (writesModel) {
|
|
3013
|
+
fnScore += 2;
|
|
3014
|
+
reasons.push("Writes to a database model");
|
|
3015
|
+
}
|
|
3016
|
+
const authOrValid = fn.semanticActions.some((a) => a.actionKind === "auth_check" || a.actionKind === "validation");
|
|
3017
|
+
if (authOrValid) {
|
|
3018
|
+
fnScore += 1;
|
|
3019
|
+
reasons.push("Performs auth/validation");
|
|
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
|
+
}
|
|
3026
|
+
return { fn, fnScore, reasons };
|
|
3027
|
+
});
|
|
3028
|
+
scoredFunctions.sort((a, b) => b.fnScore - a.fnScore);
|
|
3029
|
+
const topFns = scoredFunctions.filter((x) => x.reasons.length > 0).slice(0, 5);
|
|
3030
|
+
if (topFns.length > 0) {
|
|
3031
|
+
criticalFunctions = topFns.map(({ fn, reasons }) => {
|
|
3032
|
+
const evidence = fn.semanticActions.slice(0, 5).sort((a, b) => a.sourceLine - b.sourceLine).map((a) => ({
|
|
3033
|
+
sourceLine: a.sourceLine,
|
|
3034
|
+
text: a.evidenceText,
|
|
3035
|
+
actionKind: a.actionKind,
|
|
3036
|
+
targetModel: a.targetModel,
|
|
3037
|
+
targetOperation: a.targetOperation,
|
|
3038
|
+
confidence: a.confidence
|
|
3039
|
+
}));
|
|
3040
|
+
const confidences = fn.semanticActions.map((a) => a.confidence);
|
|
3041
|
+
let confidence2 = "high";
|
|
3042
|
+
if (confidences.includes("low"))
|
|
3043
|
+
confidence2 = "low";
|
|
3044
|
+
else if (confidences.includes("medium"))
|
|
3045
|
+
confidence2 = "medium";
|
|
3046
|
+
return {
|
|
3047
|
+
functionId: fn.functionId,
|
|
3048
|
+
displayName: fn.displayName,
|
|
3049
|
+
functionKind: fn.functionKind,
|
|
3050
|
+
startLine: fn.startLine,
|
|
3051
|
+
endLine: fn.endLine,
|
|
3052
|
+
isEntrypoint: fn.isEntrypoint,
|
|
3053
|
+
isExported: fn.isExported,
|
|
3054
|
+
actionKinds: [...new Set(fn.semanticActions.map((a) => a.actionKind))],
|
|
3055
|
+
targetModels: [...new Set(fn.semanticActions.map((a) => a.targetModel).filter(Boolean))],
|
|
3056
|
+
targetOperations: [...new Set(fn.semanticActions.map((a) => a.targetOperation).filter(Boolean))],
|
|
3057
|
+
outboundCallCount: fn.calls.length,
|
|
3058
|
+
resolvedOutboundCallCount: fn.calls.filter((c) => c.resolvedTargetFunctionId).length,
|
|
3059
|
+
semanticActionCount: fn.semanticActions.length,
|
|
3060
|
+
evidence,
|
|
3061
|
+
confidence: confidence2,
|
|
3062
|
+
reasons
|
|
3063
|
+
};
|
|
3064
|
+
});
|
|
3065
|
+
}
|
|
3066
|
+
}
|
|
3067
|
+
}
|
|
2341
3068
|
return {
|
|
2342
3069
|
path: pf.relativePath,
|
|
2343
3070
|
frameworkRole: pf.frameworkRole,
|
|
@@ -2362,17 +3089,18 @@ async function runScoring(projectRoot, cr) {
|
|
|
2362
3089
|
testProbes: inferTestProbes(pf.writeIntents, observableOutputs),
|
|
2363
3090
|
rawEvidence,
|
|
2364
3091
|
displayEvidence,
|
|
3092
|
+
criticalFunctions,
|
|
2365
3093
|
analysisAnnotation: `${pf.frameworkRole} in ${pf.productDomain} domain. fanIn=${pf.gravitySignals.fanIn} cyclomatic=${pf.gravitySignals.cyclomatic} loc=${pf.gravitySignals.loc}`,
|
|
2366
3094
|
hashes: { fileHash, evidenceHash: rawEvidence.map((e) => e.evidenceHash).join("-") }
|
|
2367
3095
|
};
|
|
2368
3096
|
});
|
|
2369
|
-
const dest =
|
|
3097
|
+
const dest = join8(dir, "delta_targets.json");
|
|
2370
3098
|
const tmp = dest + ".tmp";
|
|
2371
|
-
await
|
|
3099
|
+
await writeFile8(tmp, JSON.stringify(deltaTargets, null, 2), "utf8");
|
|
2372
3100
|
const { rename } = await import("fs/promises");
|
|
2373
3101
|
await rename(tmp, dest);
|
|
2374
3102
|
const validationReport = await buildValidationReport(store, deltaTargets, projectRoot);
|
|
2375
|
-
await
|
|
3103
|
+
await writeFile8(join8(dir, "validation_report.json"), JSON.stringify(validationReport, null, 2), "utf8");
|
|
2376
3104
|
for (const e of validationReport.errors) {
|
|
2377
3105
|
console.error(`[vibe-splain] VALIDATION ERROR [${e.rule}] ${e.file}: ${e.detail}`);
|
|
2378
3106
|
}
|
|
@@ -2508,7 +3236,7 @@ async function buildValidationReport(store, deltaTargets, projectRoot) {
|
|
|
2508
3236
|
let secondaryTrigger = false;
|
|
2509
3237
|
if (!primaryTrigger && pf.productDomain !== "payments_webhooks") {
|
|
2510
3238
|
try {
|
|
2511
|
-
const src = await
|
|
3239
|
+
const src = await readFile7(join8(projectRoot, rel), "utf8");
|
|
2512
3240
|
secondaryTrigger = PAYMENT_CONTENT_TERMS.some((t) => src.includes(t));
|
|
2513
3241
|
} catch {
|
|
2514
3242
|
}
|
|
@@ -2567,8 +3295,9 @@ async function buildValidationReport(store, deltaTargets, projectRoot) {
|
|
|
2567
3295
|
async function runPipeline(projectRoot) {
|
|
2568
3296
|
const inv = await runInventory(projectRoot);
|
|
2569
3297
|
const res = await runResolution(projectRoot, inv);
|
|
3298
|
+
const binding = await runActionBinding(projectRoot, inv, res);
|
|
2570
3299
|
const cr = await runClassification(projectRoot, inv, res);
|
|
2571
|
-
const scoring = await runScoring(projectRoot, cr);
|
|
3300
|
+
const scoring = await runScoring(projectRoot, cr, binding);
|
|
2572
3301
|
await writeGraph(projectRoot, res.graph);
|
|
2573
3302
|
await writeAnalysis(projectRoot, scoring.store);
|
|
2574
3303
|
const files = cr.classified.filter((f) => f.isRealSource).sort((a, b) => b.gravity - a.gravity).map((f) => ({
|
|
@@ -2603,7 +3332,7 @@ async function runPipeline(projectRoot) {
|
|
|
2603
3332
|
productDomain: f.productDomain,
|
|
2604
3333
|
sideEffectProfile: f.sideEffectProfile
|
|
2605
3334
|
}));
|
|
2606
|
-
const uiUrl = `file://${
|
|
3335
|
+
const uiUrl = `file://${join9(projectRoot, ".vibe-splainer", "ui", "index.html")}`;
|
|
2607
3336
|
return {
|
|
2608
3337
|
projectRoot,
|
|
2609
3338
|
totalFilesScanned: cr.classified.length,
|
|
@@ -2636,7 +3365,7 @@ async function getFileAnalysis(absPath) {
|
|
|
2636
3365
|
return null;
|
|
2637
3366
|
let source;
|
|
2638
3367
|
try {
|
|
2639
|
-
source = await
|
|
3368
|
+
source = await readFile8(absPath, "utf8");
|
|
2640
3369
|
} catch {
|
|
2641
3370
|
return null;
|
|
2642
3371
|
}
|
|
@@ -2675,16 +3404,16 @@ async function getFileAnalysis(absPath) {
|
|
|
2675
3404
|
|
|
2676
3405
|
// ../brain/dist/dossier.js
|
|
2677
3406
|
import { Mutex } from "async-mutex";
|
|
2678
|
-
import { join as
|
|
3407
|
+
import { join as join10, dirname as dirname3 } from "path";
|
|
2679
3408
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2680
|
-
import { readFile as
|
|
3409
|
+
import { readFile as readFile9, writeFile as writeFile9, mkdir as mkdir7 } from "fs/promises";
|
|
2681
3410
|
import { existsSync as existsSync4, cpSync } from "fs";
|
|
2682
3411
|
var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
|
|
2683
3412
|
var dossierMutex = new Mutex();
|
|
2684
3413
|
async function readDossier(projectRoot) {
|
|
2685
|
-
const dossierPath =
|
|
3414
|
+
const dossierPath = join10(projectRoot, ".vibe-splainer", "dossier.json");
|
|
2686
3415
|
try {
|
|
2687
|
-
const raw = await
|
|
3416
|
+
const raw = await readFile9(dossierPath, "utf8");
|
|
2688
3417
|
return JSON.parse(raw);
|
|
2689
3418
|
} catch {
|
|
2690
3419
|
return null;
|
|
@@ -2696,33 +3425,33 @@ async function writeDossier(projectRoot, dossier) {
|
|
|
2696
3425
|
p.decisions = p.decisions.filter((c) => !(c.severity === 1 && c.category === "Convention"));
|
|
2697
3426
|
p.cardCount = p.decisions.length;
|
|
2698
3427
|
}
|
|
2699
|
-
const dir =
|
|
3428
|
+
const dir = join10(projectRoot, ".vibe-splainer");
|
|
2700
3429
|
await mkdir7(dir, { recursive: true });
|
|
2701
|
-
const dossierPath =
|
|
3430
|
+
const dossierPath = join10(dir, "dossier.json");
|
|
2702
3431
|
const tmp = dossierPath + ".tmp";
|
|
2703
|
-
await
|
|
3432
|
+
await writeFile9(tmp, JSON.stringify(dossier, null, 2), "utf8");
|
|
2704
3433
|
const { rename } = await import("fs/promises");
|
|
2705
3434
|
await rename(tmp, dossierPath);
|
|
2706
3435
|
await regenerateUI(projectRoot, dossier);
|
|
2707
3436
|
});
|
|
2708
3437
|
}
|
|
2709
3438
|
async function regenerateUI(projectRoot, dossier) {
|
|
2710
|
-
const uiDir =
|
|
3439
|
+
const uiDir = join10(projectRoot, ".vibe-splainer", "ui");
|
|
2711
3440
|
await mkdir7(uiDir, { recursive: true });
|
|
2712
|
-
let templateDir =
|
|
3441
|
+
let templateDir = join10(__dirname2, "ui");
|
|
2713
3442
|
if (!existsSync4(templateDir)) {
|
|
2714
|
-
templateDir =
|
|
3443
|
+
templateDir = join10(__dirname2, "../../cli/dist/ui");
|
|
2715
3444
|
}
|
|
2716
3445
|
if (!existsSync4(templateDir)) {
|
|
2717
3446
|
console.error("[vibe-splain] UI template not found at", templateDir, "- skipping UI regeneration");
|
|
2718
3447
|
return;
|
|
2719
3448
|
}
|
|
2720
3449
|
cpSync(templateDir, uiDir, { recursive: true });
|
|
2721
|
-
let html = await
|
|
3450
|
+
let html = await readFile9(join10(templateDir, "index.html"), "utf8");
|
|
2722
3451
|
const injection = `<script>window.__VIBE_DOSSIER__ = ${JSON.stringify(dossier)};</script>`;
|
|
2723
3452
|
html = html.replace("<!-- VIBE_DOSSIER_INJECTION_POINT -->", injection);
|
|
2724
|
-
await
|
|
2725
|
-
console.error("[vibe-splain] UI regenerated at",
|
|
3453
|
+
await writeFile9(join10(uiDir, "index.html"), html, "utf8");
|
|
3454
|
+
console.error("[vibe-splain] UI regenerated at", join10(uiDir, "index.html"));
|
|
2726
3455
|
}
|
|
2727
3456
|
function validateMermaidNodeCount(diagram) {
|
|
2728
3457
|
if (!diagram)
|
|
@@ -2742,8 +3471,8 @@ function validateMermaidNodeCount(diagram) {
|
|
|
2742
3471
|
// ../brain/dist/watcher.js
|
|
2743
3472
|
import chokidar from "chokidar";
|
|
2744
3473
|
import { createHash as createHash2 } from "crypto";
|
|
2745
|
-
import { readFile as
|
|
2746
|
-
import { join as
|
|
3474
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
3475
|
+
import { join as join11 } from "path";
|
|
2747
3476
|
function startWatcher(projectRoot, watchedPaths) {
|
|
2748
3477
|
const watcher = chokidar.watch(watchedPaths.length > 0 ? watchedPaths : projectRoot, {
|
|
2749
3478
|
ignoreInitial: true,
|
|
@@ -2755,14 +3484,14 @@ function startWatcher(projectRoot, watchedPaths) {
|
|
|
2755
3484
|
const dossier = await readDossier(projectRoot);
|
|
2756
3485
|
if (!dossier)
|
|
2757
3486
|
return;
|
|
2758
|
-
const content = await
|
|
3487
|
+
const content = await readFile10(filepath, "utf8");
|
|
2759
3488
|
const newHash = createHash2("sha256").update(content).digest("hex");
|
|
2760
3489
|
let mutated = false;
|
|
2761
3490
|
for (const pillar of dossier.pillars) {
|
|
2762
3491
|
for (const card of pillar.decisions) {
|
|
2763
3492
|
if (!card.primaryFile)
|
|
2764
3493
|
continue;
|
|
2765
|
-
const absMatch = filepath ===
|
|
3494
|
+
const absMatch = filepath === join11(projectRoot, card.primaryFile) || filepath.endsWith("/" + card.primaryFile);
|
|
2766
3495
|
if (absMatch && card.lastScannedHash !== newHash) {
|
|
2767
3496
|
card.status = "stale";
|
|
2768
3497
|
const rel = card.primaryFile;
|
|
@@ -2932,8 +3661,8 @@ async function handleSetProjectBrief(args) {
|
|
|
2932
3661
|
}
|
|
2933
3662
|
|
|
2934
3663
|
// dist/mcp/tools/get_file_context.js
|
|
2935
|
-
import { readFile as
|
|
2936
|
-
import { join as
|
|
3664
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
3665
|
+
import { join as join12, relative as relative3, isAbsolute } from "path";
|
|
2937
3666
|
var getFileContextTool = {
|
|
2938
3667
|
name: "get_file_context",
|
|
2939
3668
|
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 +3682,7 @@ async function handleGetFileContext(args) {
|
|
|
2953
3682
|
const full = args.full === true;
|
|
2954
3683
|
if (!projectRoot || !filePath)
|
|
2955
3684
|
throw new Error("projectRoot and filePath are required");
|
|
2956
|
-
const fullPath = isAbsolute(filePath) ? filePath :
|
|
3685
|
+
const fullPath = isAbsolute(filePath) ? filePath : join12(projectRoot, filePath);
|
|
2957
3686
|
const relPath = relative3(projectRoot, fullPath);
|
|
2958
3687
|
const evidence = await getFileAnalysis(fullPath);
|
|
2959
3688
|
if (!evidence) {
|
|
@@ -2978,7 +3707,7 @@ async function handleGetFileContext(args) {
|
|
|
2978
3707
|
smellSpans: evidence.smellSpans
|
|
2979
3708
|
};
|
|
2980
3709
|
if (full) {
|
|
2981
|
-
result.source = await
|
|
3710
|
+
result.source = await readFile11(fullPath, "utf8");
|
|
2982
3711
|
}
|
|
2983
3712
|
return result;
|
|
2984
3713
|
}
|
|
@@ -2986,8 +3715,8 @@ async function handleGetFileContext(args) {
|
|
|
2986
3715
|
// dist/mcp/tools/write_decision_card.js
|
|
2987
3716
|
import { v4 as uuidv4 } from "uuid";
|
|
2988
3717
|
import { createHash as createHash3 } from "crypto";
|
|
2989
|
-
import { readFile as
|
|
2990
|
-
import { join as
|
|
3718
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
3719
|
+
import { join as join13 } from "path";
|
|
2991
3720
|
var CATEGORIES = ["Bottleneck", "Hack", "Smart-Move", "Risk", "Convention", "Dead-Weight"];
|
|
2992
3721
|
function normalizeSnippet(s) {
|
|
2993
3722
|
let out = (s ?? "").replace(/\r\n/g, "\n");
|
|
@@ -3079,7 +3808,7 @@ async function handleWriteDecisionCard(args) {
|
|
|
3079
3808
|
const heat = persisted ? Math.round(persisted.heat) : void 0;
|
|
3080
3809
|
let primaryContent = "";
|
|
3081
3810
|
try {
|
|
3082
|
-
primaryContent = await
|
|
3811
|
+
primaryContent = await readFile12(join13(projectRoot, primaryFile), "utf8");
|
|
3083
3812
|
} catch {
|
|
3084
3813
|
}
|
|
3085
3814
|
const hash = createHash3("sha256").update(primaryContent).digest("hex");
|
|
@@ -3299,12 +4028,62 @@ async function handleMarkStale(args) {
|
|
|
3299
4028
|
};
|
|
3300
4029
|
}
|
|
3301
4030
|
|
|
4031
|
+
// dist/mcp/tools/get_call_chain.js
|
|
4032
|
+
var getCallChainTool = {
|
|
4033
|
+
name: "get_call_chain",
|
|
4034
|
+
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.
|
|
4035
|
+
|
|
4036
|
+
Use structured filters when you know the target:
|
|
4037
|
+
targetModel + targetOperation: "where does Booking get created?"
|
|
4038
|
+
targetActionKind: "where is auth enforced?"
|
|
4039
|
+
targetFunctionName: "how is function X reached?"
|
|
4040
|
+
No filter returns the full call tree up to maxDepth.
|
|
4041
|
+
|
|
4042
|
+
Run scan_project first \u2014 this tool reads from the generated action_bindings.json.`,
|
|
4043
|
+
inputSchema: {
|
|
4044
|
+
type: "object",
|
|
4045
|
+
properties: {
|
|
4046
|
+
projectRoot: { type: "string" },
|
|
4047
|
+
entrypointPath: { type: "string", description: "Relative path to the entrypoint file" },
|
|
4048
|
+
maxDepth: { type: "number", description: "Max traversal depth. Default 6, max 12." },
|
|
4049
|
+
targetActionKind: { type: "string", description: "Stop at this semantic action kind." },
|
|
4050
|
+
targetModel: { type: "string", description: "Stop at functions touching this model." },
|
|
4051
|
+
targetOperation: { type: "string", description: "Narrow targetModel to this operation." },
|
|
4052
|
+
targetFunctionName: { type: "string", description: "Stop at a specific function name." },
|
|
4053
|
+
includeTests: { type: "boolean", description: "Include test files in traversal. Default false." }
|
|
4054
|
+
},
|
|
4055
|
+
required: ["projectRoot", "entrypointPath"]
|
|
4056
|
+
}
|
|
4057
|
+
};
|
|
4058
|
+
async function handleGetCallChain(args) {
|
|
4059
|
+
const projectRoot = args.projectRoot;
|
|
4060
|
+
const entrypointPath = args.entrypointPath;
|
|
4061
|
+
if (!projectRoot || !entrypointPath)
|
|
4062
|
+
throw new Error("projectRoot and entrypointPath are required");
|
|
4063
|
+
const getCallChainArgs = {
|
|
4064
|
+
entrypointPath,
|
|
4065
|
+
maxDepth: typeof args.maxDepth === "number" ? args.maxDepth : void 0,
|
|
4066
|
+
targetActionKind: typeof args.targetActionKind === "string" ? args.targetActionKind : void 0,
|
|
4067
|
+
targetModel: typeof args.targetModel === "string" ? args.targetModel : void 0,
|
|
4068
|
+
targetOperation: typeof args.targetOperation === "string" ? args.targetOperation : void 0,
|
|
4069
|
+
targetFunctionName: typeof args.targetFunctionName === "string" ? args.targetFunctionName : void 0,
|
|
4070
|
+
includeTests: typeof args.includeTests === "boolean" ? args.includeTests : void 0
|
|
4071
|
+
};
|
|
4072
|
+
try {
|
|
4073
|
+
const result = await traverseCallChain(projectRoot, getCallChainArgs);
|
|
4074
|
+
return result;
|
|
4075
|
+
} catch (error) {
|
|
4076
|
+
throw new Error(`get_call_chain failed: ${error instanceof Error ? error.message : String(error)}`);
|
|
4077
|
+
}
|
|
4078
|
+
}
|
|
4079
|
+
|
|
3302
4080
|
// dist/mcp/server.js
|
|
3303
4081
|
var ALL_TOOLS = [
|
|
3304
4082
|
scanProjectTool,
|
|
3305
4083
|
getProjectMapTool,
|
|
3306
4084
|
setProjectBriefTool,
|
|
3307
4085
|
getFileContextTool,
|
|
4086
|
+
getCallChainTool,
|
|
3308
4087
|
writeDecisionCardTool,
|
|
3309
4088
|
getStrategicOverviewTool,
|
|
3310
4089
|
inspectPillarTool,
|
|
@@ -3316,6 +4095,7 @@ var TOOL_HANDLERS = {
|
|
|
3316
4095
|
get_project_map: handleGetProjectMap,
|
|
3317
4096
|
set_project_brief: handleSetProjectBrief,
|
|
3318
4097
|
get_file_context: handleGetFileContext,
|
|
4098
|
+
get_call_chain: handleGetCallChain,
|
|
3319
4099
|
write_decision_card: handleWriteDecisionCard,
|
|
3320
4100
|
get_strategic_overview: handleGetStrategicOverview,
|
|
3321
4101
|
inspect_pillar: handleInspectPillar,
|
package/dist/mcp/server.js
CHANGED
|
@@ -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