vibe-splain 2.5.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/commands/install.js +3 -2
- package/dist/index.js +936 -45
- 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
|
@@ -19,8 +19,9 @@ function expandPath(p) {
|
|
|
19
19
|
return p;
|
|
20
20
|
}
|
|
21
21
|
var AGENT_CONFIGS = [
|
|
22
|
-
{ name: "Claude Code", path: "~/.claude/
|
|
23
|
-
{ name: "Claude
|
|
22
|
+
{ name: "Claude Code CLI", path: "~/.claude/settings.json", format: "claude" },
|
|
23
|
+
{ name: "Claude Desktop", path: "~/.claude/claude_desktop_config.json", format: "claude" },
|
|
24
|
+
{ name: "Claude Desktop (Windows)", path: "%APPDATA%/Claude/claude_desktop_config.json", format: "claude" },
|
|
24
25
|
{ name: "Gemini CLI", path: "~/.gemini/settings.json", format: "gemini" },
|
|
25
26
|
{ name: "Cursor", path: "~/.cursor/mcp.json", format: "cursor" },
|
|
26
27
|
{ name: "Windsurf", path: "~/.codeium/windsurf/mcp_config.json", format: "cursor" }
|
|
@@ -88,10 +89,10 @@ import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema
|
|
|
88
89
|
|
|
89
90
|
// ../brain/dist/scanner.js
|
|
90
91
|
import { extname as extname4 } from "path";
|
|
91
|
-
import { readFile as
|
|
92
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
92
93
|
|
|
93
94
|
// ../brain/dist/pipeline/orchestrator.js
|
|
94
|
-
import { join as
|
|
95
|
+
import { join as join9 } from "path";
|
|
95
96
|
|
|
96
97
|
// ../brain/dist/graph.js
|
|
97
98
|
import { join as join2 } from "path";
|
|
@@ -571,6 +572,122 @@ function extractImports(source, lang) {
|
|
|
571
572
|
specs.push(m[1] || m[2]);
|
|
572
573
|
return specs;
|
|
573
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
|
+
}
|
|
574
691
|
async function detectStackAndEntrypoints(projectRoot, files) {
|
|
575
692
|
const stack = /* @__PURE__ */ new Set();
|
|
576
693
|
const entrypoints = /* @__PURE__ */ new Set();
|
|
@@ -989,11 +1106,12 @@ function analyzeAst(source, lang, tree) {
|
|
|
989
1106
|
}
|
|
990
1107
|
scored.sort((a, b) => b.score - a.score);
|
|
991
1108
|
const hotSpans = scored.slice(0, 3).filter((s) => s.bodyLOC >= 4).map((s) => {
|
|
992
|
-
const
|
|
993
|
-
const snippet = stripLeadingComments(
|
|
1109
|
+
const rawExcerpt = source.split("\n").slice(s.node.startPosition.row, s.node.endPosition.row + 1).join("\n");
|
|
1110
|
+
const snippet = stripLeadingComments(rawExcerpt).slice(0, 2e3);
|
|
994
1111
|
return {
|
|
995
1112
|
startLine: s.node.startPosition.row + 1,
|
|
996
1113
|
endLine: s.node.endPosition.row + 1,
|
|
1114
|
+
rawExcerpt,
|
|
997
1115
|
snippet,
|
|
998
1116
|
reason: `high complexity: ${s.decisions} decision branches across ${s.bodyLOC} lines`
|
|
999
1117
|
};
|
|
@@ -1062,6 +1180,7 @@ async function runInventory(projectRoot) {
|
|
|
1062
1180
|
continue;
|
|
1063
1181
|
const ast = analyzeAst(source, lang, tree);
|
|
1064
1182
|
const importSpecs = extractImports(source, lang);
|
|
1183
|
+
const rawNamedImports = extractNamedImports(source, lang, tree);
|
|
1065
1184
|
const frameworkRole = inferFrameworkRole(rel);
|
|
1066
1185
|
const productDomain = inferProductDomain(rel, importSpecs);
|
|
1067
1186
|
work.push({
|
|
@@ -1070,7 +1189,9 @@ async function runInventory(projectRoot) {
|
|
|
1070
1189
|
lang,
|
|
1071
1190
|
source,
|
|
1072
1191
|
ast,
|
|
1192
|
+
tree,
|
|
1073
1193
|
importSpecs,
|
|
1194
|
+
rawNamedImports,
|
|
1074
1195
|
pathDemote: pathDemoteReason(rel),
|
|
1075
1196
|
frameworkRole,
|
|
1076
1197
|
productDomain
|
|
@@ -2049,9 +2170,516 @@ async function runClassification(projectRoot, inv, res) {
|
|
|
2049
2170
|
return { projectRoot, classified, stack: inv.stack, entrypoints, map, communities };
|
|
2050
2171
|
}
|
|
2051
2172
|
|
|
2052
|
-
// ../brain/dist/pipeline/
|
|
2173
|
+
// ../brain/dist/pipeline/binding.js
|
|
2053
2174
|
import { join as join7 } from "path";
|
|
2054
|
-
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",
|
|
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";
|
|
2055
2683
|
import { createHash } from "crypto";
|
|
2056
2684
|
function computeSeverity(sideEffectProfile, productDomain, gravity, heat, maxNesting, hasLongFunctions, swallowedCatches, runtimeEntrypoints) {
|
|
2057
2685
|
let score = 0;
|
|
@@ -2113,6 +2741,11 @@ function applyCorrections(file) {
|
|
|
2113
2741
|
}
|
|
2114
2742
|
if (file.canonicalSeverity === 5)
|
|
2115
2743
|
file.canonicalLoadBearing = true;
|
|
2744
|
+
if (file.riskTypes.includes("registry_bottleneck")) {
|
|
2745
|
+
if (file.canonicalSeverity < 4)
|
|
2746
|
+
file.canonicalSeverity = 4;
|
|
2747
|
+
file.canonicalLoadBearing = true;
|
|
2748
|
+
}
|
|
2116
2749
|
}
|
|
2117
2750
|
function inferObservableOutputs(frameworkRole, productDomain, sideEffectProfile) {
|
|
2118
2751
|
const outputs = [];
|
|
@@ -2141,6 +2774,9 @@ function inferObservableOutputs(frameworkRole, productDomain, sideEffectProfile)
|
|
|
2141
2774
|
outputs.push("sdk_event_name");
|
|
2142
2775
|
if (frameworkRole === "hook" || frameworkRole === "store")
|
|
2143
2776
|
outputs.push("ui_state_transition");
|
|
2777
|
+
if (productDomain === "data_table" && frameworkRole === "provider") {
|
|
2778
|
+
outputs.push("ui_state_transition", "filter_state", "selected_segment");
|
|
2779
|
+
}
|
|
2144
2780
|
return [...new Set(outputs)];
|
|
2145
2781
|
}
|
|
2146
2782
|
function inferPatchRisk(productDomain, riskTypes, sideEffectProfile, importedByCount, loadBearingScore) {
|
|
@@ -2157,9 +2793,21 @@ function inferPatchRisk(productDomain, riskTypes, sideEffectProfile, importedByC
|
|
|
2157
2793
|
reason: `${productDomain} writes to external state (${external.join(", ") || "database"}). Changes require integration testing.`
|
|
2158
2794
|
};
|
|
2159
2795
|
}
|
|
2796
|
+
if (riskTypes.includes("registry_bottleneck")) {
|
|
2797
|
+
return {
|
|
2798
|
+
level: "high",
|
|
2799
|
+
reason: "registry_bottleneck: central dispatch point \u2014 blast radius not measurable by fan-in alone."
|
|
2800
|
+
};
|
|
2801
|
+
}
|
|
2160
2802
|
if (loadBearingScore >= 5 || importedByCount >= 5) {
|
|
2161
2803
|
return { level: "medium", reason: `Imported by ${importedByCount} files. Interface changes will cascade.` };
|
|
2162
2804
|
}
|
|
2805
|
+
if (productDomain === "data_table" && riskTypes.includes("state_machine")) {
|
|
2806
|
+
return {
|
|
2807
|
+
level: "medium",
|
|
2808
|
+
reason: "data_table state machine: controls user-visible workflow state (filters, segments, pagination) \u2014 regression risk not captured by mutation scoring."
|
|
2809
|
+
};
|
|
2810
|
+
}
|
|
2163
2811
|
return { level: "low", reason: "Locally contained \u2014 limited blast radius." };
|
|
2164
2812
|
}
|
|
2165
2813
|
function inferSafePatchStrategy(riskTypes, sideEffectProfile) {
|
|
@@ -2249,9 +2897,17 @@ function deriveConfidence(fanIn, gravity) {
|
|
|
2249
2897
|
return "medium";
|
|
2250
2898
|
return "low";
|
|
2251
2899
|
}
|
|
2252
|
-
async function runScoring(projectRoot, cr) {
|
|
2253
|
-
const dir =
|
|
2900
|
+
async function runScoring(projectRoot, cr, binding) {
|
|
2901
|
+
const dir = join8(projectRoot, ".vibe-splainer");
|
|
2254
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
|
+
}
|
|
2255
2911
|
const persisted = {};
|
|
2256
2912
|
const severityBreakdowns = {};
|
|
2257
2913
|
for (const f of cr.classified) {
|
|
@@ -2285,7 +2941,7 @@ async function runScoring(projectRoot, cr) {
|
|
|
2285
2941
|
severityBreakdowns[f.rel] = `severity=${pf.canonicalSeverity} loadBearing=${pf.canonicalLoadBearing} effects=${pf.sideEffectProfile.join(",")} domain=${pf.productDomain}`;
|
|
2286
2942
|
}
|
|
2287
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] }]));
|
|
2288
|
-
await
|
|
2944
|
+
await writeFile8(join8(dir, "stage-09-severity.json"), JSON.stringify(stage09, null, 2), "utf8");
|
|
2289
2945
|
const store = { files: persisted };
|
|
2290
2946
|
const importedByMapForDelta = /* @__PURE__ */ new Map();
|
|
2291
2947
|
for (const [rel, pf] of Object.entries(persisted)) {
|
|
@@ -2306,9 +2962,89 @@ async function runScoring(projectRoot, cr) {
|
|
|
2306
2962
|
file: pf.relativePath,
|
|
2307
2963
|
startLine: span.startLine,
|
|
2308
2964
|
endLine: span.endLine,
|
|
2309
|
-
rawSourceExcerpt: span.
|
|
2310
|
-
evidenceHash: createHash("sha256").update(span.
|
|
2965
|
+
rawSourceExcerpt: span.rawExcerpt,
|
|
2966
|
+
evidenceHash: createHash("sha256").update(span.rawExcerpt).digest("hex").slice(0, 12)
|
|
2311
2967
|
}));
|
|
2968
|
+
const displayEvidence = pf.hotSpans.map((span) => ({
|
|
2969
|
+
file: pf.relativePath,
|
|
2970
|
+
startLine: span.startLine,
|
|
2971
|
+
endLine: span.endLine,
|
|
2972
|
+
excerpt: span.snippet,
|
|
2973
|
+
isTruncated: span.rawExcerpt.length > 2e3
|
|
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
|
+
}
|
|
2312
3048
|
return {
|
|
2313
3049
|
path: pf.relativePath,
|
|
2314
3050
|
frameworkRole: pf.frameworkRole,
|
|
@@ -2332,17 +3068,19 @@ async function runScoring(projectRoot, cr) {
|
|
|
2332
3068
|
doNotTouch: inferDoNotTouch(pf.sideEffectProfile, pf.productDomain),
|
|
2333
3069
|
testProbes: inferTestProbes(pf.writeIntents, observableOutputs),
|
|
2334
3070
|
rawEvidence,
|
|
3071
|
+
displayEvidence,
|
|
3072
|
+
criticalFunctions,
|
|
2335
3073
|
analysisAnnotation: `${pf.frameworkRole} in ${pf.productDomain} domain. fanIn=${pf.gravitySignals.fanIn} cyclomatic=${pf.gravitySignals.cyclomatic} loc=${pf.gravitySignals.loc}`,
|
|
2336
3074
|
hashes: { fileHash, evidenceHash: rawEvidence.map((e) => e.evidenceHash).join("-") }
|
|
2337
3075
|
};
|
|
2338
3076
|
});
|
|
2339
|
-
const dest =
|
|
3077
|
+
const dest = join8(dir, "delta_targets.json");
|
|
2340
3078
|
const tmp = dest + ".tmp";
|
|
2341
|
-
await
|
|
3079
|
+
await writeFile8(tmp, JSON.stringify(deltaTargets, null, 2), "utf8");
|
|
2342
3080
|
const { rename } = await import("fs/promises");
|
|
2343
3081
|
await rename(tmp, dest);
|
|
2344
|
-
const validationReport = buildValidationReport(store, deltaTargets);
|
|
2345
|
-
await
|
|
3082
|
+
const validationReport = await buildValidationReport(store, deltaTargets, projectRoot);
|
|
3083
|
+
await writeFile8(join8(dir, "validation_report.json"), JSON.stringify(validationReport, null, 2), "utf8");
|
|
2346
3084
|
for (const e of validationReport.errors) {
|
|
2347
3085
|
console.error(`[vibe-splain] VALIDATION ERROR [${e.rule}] ${e.file}: ${e.detail}`);
|
|
2348
3086
|
}
|
|
@@ -2351,7 +3089,7 @@ async function runScoring(projectRoot, cr) {
|
|
|
2351
3089
|
}
|
|
2352
3090
|
return { store, deltaTargets, validationReport };
|
|
2353
3091
|
}
|
|
2354
|
-
function buildValidationReport(store, deltaTargets) {
|
|
3092
|
+
async function buildValidationReport(store, deltaTargets, projectRoot) {
|
|
2355
3093
|
const errors = [];
|
|
2356
3094
|
const warnings = [];
|
|
2357
3095
|
let passCount = 0;
|
|
@@ -2421,8 +3159,109 @@ function buildValidationReport(store, deltaTargets) {
|
|
|
2421
3159
|
detail: `Entrypoints found but domain surface mismatch for ${pf.productDomain}. Found: ${foundPaths}`
|
|
2422
3160
|
});
|
|
2423
3161
|
}
|
|
3162
|
+
if (pf.riskTypes.includes("registry_bottleneck")) {
|
|
3163
|
+
if (pf.canonicalSeverity < 4)
|
|
3164
|
+
errors.push({
|
|
3165
|
+
file: pf.relativePath,
|
|
3166
|
+
rule: "registry_bottleneck_severity",
|
|
3167
|
+
detail: "registry_bottleneck file must have severity >= 4",
|
|
3168
|
+
expected: ">=4",
|
|
3169
|
+
actual: String(pf.canonicalSeverity)
|
|
3170
|
+
});
|
|
3171
|
+
if (!pf.canonicalLoadBearing)
|
|
3172
|
+
errors.push({
|
|
3173
|
+
file: pf.relativePath,
|
|
3174
|
+
rule: "registry_bottleneck_load_bearing",
|
|
3175
|
+
detail: "registry_bottleneck file must be load-bearing",
|
|
3176
|
+
expected: "true",
|
|
3177
|
+
actual: "false"
|
|
3178
|
+
});
|
|
3179
|
+
if (delta && delta.patchRisk.level !== "high" && delta.patchRisk.level !== "critical")
|
|
3180
|
+
errors.push({
|
|
3181
|
+
file: pf.relativePath,
|
|
3182
|
+
rule: "registry_bottleneck_patch_risk",
|
|
3183
|
+
detail: "registry_bottleneck file must have patch risk high or critical",
|
|
3184
|
+
expected: "high|critical",
|
|
3185
|
+
actual: delta?.patchRisk.level ?? "unknown"
|
|
3186
|
+
});
|
|
3187
|
+
}
|
|
3188
|
+
if (pf.productDomain === "data_table" && pf.riskTypes.includes("state_machine") && delta?.patchRisk.level === "low") {
|
|
3189
|
+
warnings.push({
|
|
3190
|
+
file: pf.relativePath,
|
|
3191
|
+
rule: "data_table_state_machine_risk",
|
|
3192
|
+
detail: "data_table state machine should have at least medium patch risk"
|
|
3193
|
+
});
|
|
3194
|
+
}
|
|
2424
3195
|
passCount++;
|
|
2425
3196
|
}
|
|
3197
|
+
const PAYMENT_PROVIDER_PATH_TERMS = ["stripe", "paypal", "btcpay", "btcpayserver", "alby", "hitpay", "payment"];
|
|
3198
|
+
const PAYMENT_CONTENT_TERMS = [
|
|
3199
|
+
"constructEvent",
|
|
3200
|
+
"checkoutSession",
|
|
3201
|
+
"paymentIntent",
|
|
3202
|
+
"stripe-signature",
|
|
3203
|
+
"webhook-signature",
|
|
3204
|
+
"payment_mutation",
|
|
3205
|
+
"paymentStatus",
|
|
3206
|
+
"invoicePaid",
|
|
3207
|
+
"chargeSucceeded"
|
|
3208
|
+
];
|
|
3209
|
+
for (const [rel, pf] of Object.entries(store.files)) {
|
|
3210
|
+
if (!pf.isRealSource)
|
|
3211
|
+
continue;
|
|
3212
|
+
const pathLower = rel.toLowerCase();
|
|
3213
|
+
if (!pathLower.includes("webhook"))
|
|
3214
|
+
continue;
|
|
3215
|
+
const primaryTrigger = PAYMENT_PROVIDER_PATH_TERMS.some((t) => pathLower.includes(t));
|
|
3216
|
+
let secondaryTrigger = false;
|
|
3217
|
+
if (!primaryTrigger && pf.productDomain !== "payments_webhooks") {
|
|
3218
|
+
try {
|
|
3219
|
+
const src = await readFile7(join8(projectRoot, rel), "utf8");
|
|
3220
|
+
secondaryTrigger = PAYMENT_CONTENT_TERMS.some((t) => src.includes(t));
|
|
3221
|
+
} catch {
|
|
3222
|
+
}
|
|
3223
|
+
}
|
|
3224
|
+
if (!primaryTrigger && !secondaryTrigger)
|
|
3225
|
+
continue;
|
|
3226
|
+
const delta = deltaByPath.get(rel);
|
|
3227
|
+
const triggerLabel = primaryTrigger ? "path" : "content";
|
|
3228
|
+
const webhookChecks = [
|
|
3229
|
+
[
|
|
3230
|
+
pf.productDomain !== "payments_webhooks",
|
|
3231
|
+
"webhook_domain",
|
|
3232
|
+
`Payment webhook (${triggerLabel} trigger) not classified as payments_webhooks`
|
|
3233
|
+
],
|
|
3234
|
+
[
|
|
3235
|
+
!pf.sideEffectProfile.includes("webhook_ingress"),
|
|
3236
|
+
"webhook_ingress_missing",
|
|
3237
|
+
`Payment webhook (${triggerLabel} trigger) missing webhook_ingress side effect`
|
|
3238
|
+
],
|
|
3239
|
+
[
|
|
3240
|
+
!pf.sideEffectProfile.includes("payment_mutation"),
|
|
3241
|
+
"webhook_payment_mutation_missing",
|
|
3242
|
+
`Payment webhook (${triggerLabel} trigger) missing payment_mutation side effect`
|
|
3243
|
+
],
|
|
3244
|
+
[
|
|
3245
|
+
!pf.writeIntents.includes("handle_payment_webhook"),
|
|
3246
|
+
"webhook_write_intent_missing",
|
|
3247
|
+
`Payment webhook (${triggerLabel} trigger) missing handle_payment_webhook write intent`
|
|
3248
|
+
],
|
|
3249
|
+
[
|
|
3250
|
+
!!delta && delta.patchRisk.level !== "high" && delta.patchRisk.level !== "critical",
|
|
3251
|
+
"webhook_patch_risk",
|
|
3252
|
+
`Payment webhook (${triggerLabel} trigger) patchRisk must be high or critical`
|
|
3253
|
+
],
|
|
3254
|
+
[
|
|
3255
|
+
!pf.canonicalLoadBearing,
|
|
3256
|
+
"webhook_load_bearing",
|
|
3257
|
+
`Payment webhook (${triggerLabel} trigger) must be load-bearing`
|
|
3258
|
+
]
|
|
3259
|
+
];
|
|
3260
|
+
for (const [condition, rule, detail] of webhookChecks) {
|
|
3261
|
+
if (condition)
|
|
3262
|
+
errors.push({ file: rel, rule, detail });
|
|
3263
|
+
}
|
|
3264
|
+
}
|
|
2426
3265
|
return {
|
|
2427
3266
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
2428
3267
|
passed: errors.length === 0,
|
|
@@ -2436,8 +3275,9 @@ function buildValidationReport(store, deltaTargets) {
|
|
|
2436
3275
|
async function runPipeline(projectRoot) {
|
|
2437
3276
|
const inv = await runInventory(projectRoot);
|
|
2438
3277
|
const res = await runResolution(projectRoot, inv);
|
|
3278
|
+
const binding = await runActionBinding(projectRoot, inv, res);
|
|
2439
3279
|
const cr = await runClassification(projectRoot, inv, res);
|
|
2440
|
-
const scoring = await runScoring(projectRoot, cr);
|
|
3280
|
+
const scoring = await runScoring(projectRoot, cr, binding);
|
|
2441
3281
|
await writeGraph(projectRoot, res.graph);
|
|
2442
3282
|
await writeAnalysis(projectRoot, scoring.store);
|
|
2443
3283
|
const files = cr.classified.filter((f) => f.isRealSource).sort((a, b) => b.gravity - a.gravity).map((f) => ({
|
|
@@ -2472,7 +3312,7 @@ async function runPipeline(projectRoot) {
|
|
|
2472
3312
|
productDomain: f.productDomain,
|
|
2473
3313
|
sideEffectProfile: f.sideEffectProfile
|
|
2474
3314
|
}));
|
|
2475
|
-
const uiUrl = `file://${
|
|
3315
|
+
const uiUrl = `file://${join9(projectRoot, ".vibe-splainer", "ui", "index.html")}`;
|
|
2476
3316
|
return {
|
|
2477
3317
|
projectRoot,
|
|
2478
3318
|
totalFilesScanned: cr.classified.length,
|
|
@@ -2505,7 +3345,7 @@ async function getFileAnalysis(absPath) {
|
|
|
2505
3345
|
return null;
|
|
2506
3346
|
let source;
|
|
2507
3347
|
try {
|
|
2508
|
-
source = await
|
|
3348
|
+
source = await readFile8(absPath, "utf8");
|
|
2509
3349
|
} catch {
|
|
2510
3350
|
return null;
|
|
2511
3351
|
}
|
|
@@ -2544,16 +3384,16 @@ async function getFileAnalysis(absPath) {
|
|
|
2544
3384
|
|
|
2545
3385
|
// ../brain/dist/dossier.js
|
|
2546
3386
|
import { Mutex } from "async-mutex";
|
|
2547
|
-
import { join as
|
|
3387
|
+
import { join as join10, dirname as dirname3 } from "path";
|
|
2548
3388
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
2549
|
-
import { readFile as
|
|
3389
|
+
import { readFile as readFile9, writeFile as writeFile9, mkdir as mkdir7 } from "fs/promises";
|
|
2550
3390
|
import { existsSync as existsSync4, cpSync } from "fs";
|
|
2551
3391
|
var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
|
|
2552
3392
|
var dossierMutex = new Mutex();
|
|
2553
3393
|
async function readDossier(projectRoot) {
|
|
2554
|
-
const dossierPath =
|
|
3394
|
+
const dossierPath = join10(projectRoot, ".vibe-splainer", "dossier.json");
|
|
2555
3395
|
try {
|
|
2556
|
-
const raw = await
|
|
3396
|
+
const raw = await readFile9(dossierPath, "utf8");
|
|
2557
3397
|
return JSON.parse(raw);
|
|
2558
3398
|
} catch {
|
|
2559
3399
|
return null;
|
|
@@ -2565,33 +3405,33 @@ async function writeDossier(projectRoot, dossier) {
|
|
|
2565
3405
|
p.decisions = p.decisions.filter((c) => !(c.severity === 1 && c.category === "Convention"));
|
|
2566
3406
|
p.cardCount = p.decisions.length;
|
|
2567
3407
|
}
|
|
2568
|
-
const dir =
|
|
3408
|
+
const dir = join10(projectRoot, ".vibe-splainer");
|
|
2569
3409
|
await mkdir7(dir, { recursive: true });
|
|
2570
|
-
const dossierPath =
|
|
3410
|
+
const dossierPath = join10(dir, "dossier.json");
|
|
2571
3411
|
const tmp = dossierPath + ".tmp";
|
|
2572
|
-
await
|
|
3412
|
+
await writeFile9(tmp, JSON.stringify(dossier, null, 2), "utf8");
|
|
2573
3413
|
const { rename } = await import("fs/promises");
|
|
2574
3414
|
await rename(tmp, dossierPath);
|
|
2575
3415
|
await regenerateUI(projectRoot, dossier);
|
|
2576
3416
|
});
|
|
2577
3417
|
}
|
|
2578
3418
|
async function regenerateUI(projectRoot, dossier) {
|
|
2579
|
-
const uiDir =
|
|
3419
|
+
const uiDir = join10(projectRoot, ".vibe-splainer", "ui");
|
|
2580
3420
|
await mkdir7(uiDir, { recursive: true });
|
|
2581
|
-
let templateDir =
|
|
3421
|
+
let templateDir = join10(__dirname2, "ui");
|
|
2582
3422
|
if (!existsSync4(templateDir)) {
|
|
2583
|
-
templateDir =
|
|
3423
|
+
templateDir = join10(__dirname2, "../../cli/dist/ui");
|
|
2584
3424
|
}
|
|
2585
3425
|
if (!existsSync4(templateDir)) {
|
|
2586
3426
|
console.error("[vibe-splain] UI template not found at", templateDir, "- skipping UI regeneration");
|
|
2587
3427
|
return;
|
|
2588
3428
|
}
|
|
2589
3429
|
cpSync(templateDir, uiDir, { recursive: true });
|
|
2590
|
-
let html = await
|
|
3430
|
+
let html = await readFile9(join10(templateDir, "index.html"), "utf8");
|
|
2591
3431
|
const injection = `<script>window.__VIBE_DOSSIER__ = ${JSON.stringify(dossier)};</script>`;
|
|
2592
3432
|
html = html.replace("<!-- VIBE_DOSSIER_INJECTION_POINT -->", injection);
|
|
2593
|
-
await
|
|
2594
|
-
console.error("[vibe-splain] UI regenerated at",
|
|
3433
|
+
await writeFile9(join10(uiDir, "index.html"), html, "utf8");
|
|
3434
|
+
console.error("[vibe-splain] UI regenerated at", join10(uiDir, "index.html"));
|
|
2595
3435
|
}
|
|
2596
3436
|
function validateMermaidNodeCount(diagram) {
|
|
2597
3437
|
if (!diagram)
|
|
@@ -2611,8 +3451,8 @@ function validateMermaidNodeCount(diagram) {
|
|
|
2611
3451
|
// ../brain/dist/watcher.js
|
|
2612
3452
|
import chokidar from "chokidar";
|
|
2613
3453
|
import { createHash as createHash2 } from "crypto";
|
|
2614
|
-
import { readFile as
|
|
2615
|
-
import { join as
|
|
3454
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
3455
|
+
import { join as join11 } from "path";
|
|
2616
3456
|
function startWatcher(projectRoot, watchedPaths) {
|
|
2617
3457
|
const watcher = chokidar.watch(watchedPaths.length > 0 ? watchedPaths : projectRoot, {
|
|
2618
3458
|
ignoreInitial: true,
|
|
@@ -2624,14 +3464,14 @@ function startWatcher(projectRoot, watchedPaths) {
|
|
|
2624
3464
|
const dossier = await readDossier(projectRoot);
|
|
2625
3465
|
if (!dossier)
|
|
2626
3466
|
return;
|
|
2627
|
-
const content = await
|
|
3467
|
+
const content = await readFile10(filepath, "utf8");
|
|
2628
3468
|
const newHash = createHash2("sha256").update(content).digest("hex");
|
|
2629
3469
|
let mutated = false;
|
|
2630
3470
|
for (const pillar of dossier.pillars) {
|
|
2631
3471
|
for (const card of pillar.decisions) {
|
|
2632
3472
|
if (!card.primaryFile)
|
|
2633
3473
|
continue;
|
|
2634
|
-
const absMatch = filepath ===
|
|
3474
|
+
const absMatch = filepath === join11(projectRoot, card.primaryFile) || filepath.endsWith("/" + card.primaryFile);
|
|
2635
3475
|
if (absMatch && card.lastScannedHash !== newHash) {
|
|
2636
3476
|
card.status = "stale";
|
|
2637
3477
|
const rel = card.primaryFile;
|
|
@@ -2801,8 +3641,8 @@ async function handleSetProjectBrief(args) {
|
|
|
2801
3641
|
}
|
|
2802
3642
|
|
|
2803
3643
|
// dist/mcp/tools/get_file_context.js
|
|
2804
|
-
import { readFile as
|
|
2805
|
-
import { join as
|
|
3644
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
3645
|
+
import { join as join12, relative as relative3, isAbsolute } from "path";
|
|
2806
3646
|
var getFileContextTool = {
|
|
2807
3647
|
name: "get_file_context",
|
|
2808
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.",
|
|
@@ -2822,7 +3662,7 @@ async function handleGetFileContext(args) {
|
|
|
2822
3662
|
const full = args.full === true;
|
|
2823
3663
|
if (!projectRoot || !filePath)
|
|
2824
3664
|
throw new Error("projectRoot and filePath are required");
|
|
2825
|
-
const fullPath = isAbsolute(filePath) ? filePath :
|
|
3665
|
+
const fullPath = isAbsolute(filePath) ? filePath : join12(projectRoot, filePath);
|
|
2826
3666
|
const relPath = relative3(projectRoot, fullPath);
|
|
2827
3667
|
const evidence = await getFileAnalysis(fullPath);
|
|
2828
3668
|
if (!evidence) {
|
|
@@ -2847,7 +3687,7 @@ async function handleGetFileContext(args) {
|
|
|
2847
3687
|
smellSpans: evidence.smellSpans
|
|
2848
3688
|
};
|
|
2849
3689
|
if (full) {
|
|
2850
|
-
result.source = await
|
|
3690
|
+
result.source = await readFile11(fullPath, "utf8");
|
|
2851
3691
|
}
|
|
2852
3692
|
return result;
|
|
2853
3693
|
}
|
|
@@ -2855,8 +3695,8 @@ async function handleGetFileContext(args) {
|
|
|
2855
3695
|
// dist/mcp/tools/write_decision_card.js
|
|
2856
3696
|
import { v4 as uuidv4 } from "uuid";
|
|
2857
3697
|
import { createHash as createHash3 } from "crypto";
|
|
2858
|
-
import { readFile as
|
|
2859
|
-
import { join as
|
|
3698
|
+
import { readFile as readFile12 } from "fs/promises";
|
|
3699
|
+
import { join as join13 } from "path";
|
|
2860
3700
|
var CATEGORIES = ["Bottleneck", "Hack", "Smart-Move", "Risk", "Convention", "Dead-Weight"];
|
|
2861
3701
|
function normalizeSnippet(s) {
|
|
2862
3702
|
let out = (s ?? "").replace(/\r\n/g, "\n");
|
|
@@ -2948,7 +3788,7 @@ async function handleWriteDecisionCard(args) {
|
|
|
2948
3788
|
const heat = persisted ? Math.round(persisted.heat) : void 0;
|
|
2949
3789
|
let primaryContent = "";
|
|
2950
3790
|
try {
|
|
2951
|
-
primaryContent = await
|
|
3791
|
+
primaryContent = await readFile12(join13(projectRoot, primaryFile), "utf8");
|
|
2952
3792
|
} catch {
|
|
2953
3793
|
}
|
|
2954
3794
|
const hash = createHash3("sha256").update(primaryContent).digest("hex");
|
|
@@ -3168,12 +4008,62 @@ async function handleMarkStale(args) {
|
|
|
3168
4008
|
};
|
|
3169
4009
|
}
|
|
3170
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
|
+
|
|
3171
4060
|
// dist/mcp/server.js
|
|
3172
4061
|
var ALL_TOOLS = [
|
|
3173
4062
|
scanProjectTool,
|
|
3174
4063
|
getProjectMapTool,
|
|
3175
4064
|
setProjectBriefTool,
|
|
3176
4065
|
getFileContextTool,
|
|
4066
|
+
getCallChainTool,
|
|
3177
4067
|
writeDecisionCardTool,
|
|
3178
4068
|
getStrategicOverviewTool,
|
|
3179
4069
|
inspectPillarTool,
|
|
@@ -3185,6 +4075,7 @@ var TOOL_HANDLERS = {
|
|
|
3185
4075
|
get_project_map: handleGetProjectMap,
|
|
3186
4076
|
set_project_brief: handleSetProjectBrief,
|
|
3187
4077
|
get_file_context: handleGetFileContext,
|
|
4078
|
+
get_call_chain: handleGetCallChain,
|
|
3188
4079
|
write_decision_card: handleWriteDecisionCard,
|
|
3189
4080
|
get_strategic_overview: handleGetStrategicOverview,
|
|
3190
4081
|
inspect_pillar: handleInspectPillar,
|