vibe-splain 2.1.1 → 2.2.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/index.js +240 -29
- package/dist/mcp/tools/get_file_context.js +1 -0
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -244,6 +244,163 @@ var VENDOR_SEGMENTS = /* @__PURE__ */ new Set([
|
|
|
244
244
|
"third_party",
|
|
245
245
|
"third-party"
|
|
246
246
|
]);
|
|
247
|
+
var PILLAR_KEYWORDS = {
|
|
248
|
+
"Auth": [
|
|
249
|
+
"passport",
|
|
250
|
+
"jsonwebtoken",
|
|
251
|
+
"bcrypt",
|
|
252
|
+
"bcryptjs",
|
|
253
|
+
"oauth",
|
|
254
|
+
"session",
|
|
255
|
+
"cookie-parser",
|
|
256
|
+
"next-auth",
|
|
257
|
+
"@auth/",
|
|
258
|
+
"lucia",
|
|
259
|
+
"clerk",
|
|
260
|
+
"@clerk/",
|
|
261
|
+
"supabase/auth",
|
|
262
|
+
"@supabase/auth-helpers",
|
|
263
|
+
"iron-session",
|
|
264
|
+
"jose",
|
|
265
|
+
"jwt",
|
|
266
|
+
"@auth/core",
|
|
267
|
+
"arctic"
|
|
268
|
+
],
|
|
269
|
+
"Database": [
|
|
270
|
+
"prisma",
|
|
271
|
+
"@prisma/",
|
|
272
|
+
"mongoose",
|
|
273
|
+
"sequelize",
|
|
274
|
+
"typeorm",
|
|
275
|
+
"knex",
|
|
276
|
+
"pg",
|
|
277
|
+
"mysql",
|
|
278
|
+
"mysql2",
|
|
279
|
+
"better-sqlite3",
|
|
280
|
+
"drizzle-orm",
|
|
281
|
+
"drizzle",
|
|
282
|
+
"kysely",
|
|
283
|
+
"@supabase/supabase-js",
|
|
284
|
+
"mongodb",
|
|
285
|
+
"redis",
|
|
286
|
+
"ioredis"
|
|
287
|
+
],
|
|
288
|
+
"Payments": [
|
|
289
|
+
"stripe",
|
|
290
|
+
"@stripe/",
|
|
291
|
+
"paypal",
|
|
292
|
+
"braintree",
|
|
293
|
+
"plaid",
|
|
294
|
+
"lemonsqueezy",
|
|
295
|
+
"@lemonsqueezy/",
|
|
296
|
+
"paddle",
|
|
297
|
+
"lemon-squeezy"
|
|
298
|
+
],
|
|
299
|
+
"Routing": [
|
|
300
|
+
"express",
|
|
301
|
+
"fastify",
|
|
302
|
+
"koa",
|
|
303
|
+
"koa-router",
|
|
304
|
+
"next/router",
|
|
305
|
+
"next/navigation",
|
|
306
|
+
"react-router",
|
|
307
|
+
"@remix-run/",
|
|
308
|
+
"hono",
|
|
309
|
+
"express-rate-limit",
|
|
310
|
+
"cors",
|
|
311
|
+
"helmet"
|
|
312
|
+
],
|
|
313
|
+
"Queue": [
|
|
314
|
+
"bull",
|
|
315
|
+
"bullmq",
|
|
316
|
+
"amqplib",
|
|
317
|
+
"kafkajs",
|
|
318
|
+
"kafka",
|
|
319
|
+
"upstash",
|
|
320
|
+
"@upstash/",
|
|
321
|
+
"bee-queue",
|
|
322
|
+
"agenda"
|
|
323
|
+
],
|
|
324
|
+
"Storage": [
|
|
325
|
+
"aws-sdk",
|
|
326
|
+
"@aws-sdk/",
|
|
327
|
+
"multer",
|
|
328
|
+
"cloudinary",
|
|
329
|
+
"@google-cloud/storage",
|
|
330
|
+
"minio",
|
|
331
|
+
"@vercel/blob",
|
|
332
|
+
"sharp",
|
|
333
|
+
"imagekit"
|
|
334
|
+
],
|
|
335
|
+
"Config": [
|
|
336
|
+
"dotenv",
|
|
337
|
+
"convict",
|
|
338
|
+
"env-var",
|
|
339
|
+
"@t3-oss/env",
|
|
340
|
+
"envalid"
|
|
341
|
+
],
|
|
342
|
+
"Email": [
|
|
343
|
+
"nodemailer",
|
|
344
|
+
"resend",
|
|
345
|
+
"@sendgrid/",
|
|
346
|
+
"postmark",
|
|
347
|
+
"@resend/",
|
|
348
|
+
"mailgun"
|
|
349
|
+
],
|
|
350
|
+
"Realtime": [
|
|
351
|
+
"socket.io",
|
|
352
|
+
"ws",
|
|
353
|
+
"pusher",
|
|
354
|
+
"ably",
|
|
355
|
+
"@supabase/realtime",
|
|
356
|
+
"socket.io-client"
|
|
357
|
+
]
|
|
358
|
+
};
|
|
359
|
+
var PILLAR_PATH_PATTERNS = {
|
|
360
|
+
"Auth": /(?:^|[\/\\])(?:auth|login|signup|register|session|oauth)(?:[\/\\]|$)/i,
|
|
361
|
+
"Database": /(?:^|[\/\\])(?:db|database|models?|schema|migrations?|seeds?)(?:[\/\\]|$)/i,
|
|
362
|
+
"Payments": /(?:^|[\/\\])(?:pay|payments?|billing|checkout|subscriptions?|stripe)(?:[\/\\]|$)/i,
|
|
363
|
+
"Routing": /(?:^|[\/\\])(?:routes?|router|middleware|api)(?:[\/\\]|$)/i,
|
|
364
|
+
"Queue": /(?:^|[\/\\])(?:queues?|workers?|jobs?|consumers?|producers?)(?:[\/\\]|$)/i,
|
|
365
|
+
"Storage": /(?:^|[\/\\])(?:storage|uploads?|s3|blobs?|media)(?:[\/\\]|$)/i,
|
|
366
|
+
"Config": /(?:^|[\/\\])(?:config|env|settings?)(?:[\/\\]|$)/i,
|
|
367
|
+
"Email": /(?:^|[\/\\])(?:emails?|mail|notifications?)(?:[\/\\]|$)/i
|
|
368
|
+
};
|
|
369
|
+
var MEANINGLESS_SEGMENTS = /* @__PURE__ */ new Set([
|
|
370
|
+
"src",
|
|
371
|
+
"lib",
|
|
372
|
+
"app",
|
|
373
|
+
"pages",
|
|
374
|
+
"components",
|
|
375
|
+
"modules",
|
|
376
|
+
"features",
|
|
377
|
+
"core",
|
|
378
|
+
"common",
|
|
379
|
+
"shared",
|
|
380
|
+
"internal",
|
|
381
|
+
"pkg",
|
|
382
|
+
"packages"
|
|
383
|
+
]);
|
|
384
|
+
function matchPillarByImports(importSpecs) {
|
|
385
|
+
const scores = /* @__PURE__ */ new Map();
|
|
386
|
+
for (const spec of importSpecs) {
|
|
387
|
+
for (const [pillar, keywords] of Object.entries(PILLAR_KEYWORDS)) {
|
|
388
|
+
if (keywords.some((kw) => spec === kw || spec.startsWith(kw + "/"))) {
|
|
389
|
+
scores.set(pillar, (scores.get(pillar) || 0) + 1);
|
|
390
|
+
}
|
|
391
|
+
}
|
|
392
|
+
}
|
|
393
|
+
if (scores.size === 0)
|
|
394
|
+
return null;
|
|
395
|
+
return [...scores.entries()].sort((a, b) => b[1] - a[1])[0][0];
|
|
396
|
+
}
|
|
397
|
+
function matchPillarByPath(relPath) {
|
|
398
|
+
for (const [pillar, pattern] of Object.entries(PILLAR_PATH_PATTERNS)) {
|
|
399
|
+
if (pattern.test(relPath))
|
|
400
|
+
return pillar;
|
|
401
|
+
}
|
|
402
|
+
return null;
|
|
403
|
+
}
|
|
247
404
|
async function collectFiles(dir, projectRoot, acc) {
|
|
248
405
|
let entries;
|
|
249
406
|
try {
|
|
@@ -1047,7 +1204,10 @@ async function scanProject(projectRoot) {
|
|
|
1047
1204
|
publicSurface: w.ast.publicSurface,
|
|
1048
1205
|
loc: w.ast.loc
|
|
1049
1206
|
};
|
|
1050
|
-
|
|
1207
|
+
const depthRatio = (w.ast.cyclomatic + w.ast.maxNesting * 2) / Math.max(1, w.ast.publicSurface);
|
|
1208
|
+
const depthFactor = Math.min(1, Math.log2(depthRatio + 1) / 3);
|
|
1209
|
+
const adjustedCentrality = centrality * (0.3 + 0.7 * depthFactor);
|
|
1210
|
+
let gravityRaw = adjustedCentrality * 50 + Math.log2(fanIn + 1) * 6 + Math.log2(w.ast.cyclomatic + 1) * 7 + Math.log2(w.ast.publicSurface + 1) * 2 + (w.ast.maxNesting >= 4 ? 5 : 0);
|
|
1051
1211
|
if (!real)
|
|
1052
1212
|
gravityRaw *= 0.2;
|
|
1053
1213
|
const gravity = Math.max(0, Math.min(100, gravityRaw));
|
|
@@ -1060,7 +1220,9 @@ async function scanProject(projectRoot) {
|
|
|
1060
1220
|
magicNumbers: w.ast.magicNumbers
|
|
1061
1221
|
};
|
|
1062
1222
|
const heat = real ? computeHeat(w.ast.smells) : 0;
|
|
1063
|
-
const
|
|
1223
|
+
const keywordPillar = matchPillarByImports(w.importSpecs);
|
|
1224
|
+
const pathPillar = matchPillarByPath(w.rel);
|
|
1225
|
+
const pillarHint = real ? keywordPillar || pathPillar || `community-${communities.get(w.rel)}` : null;
|
|
1064
1226
|
const fa = {
|
|
1065
1227
|
path: w.abs,
|
|
1066
1228
|
relativePath: w.rel,
|
|
@@ -1119,25 +1281,59 @@ async function scanProject(projectRoot) {
|
|
|
1119
1281
|
graph
|
|
1120
1282
|
};
|
|
1121
1283
|
}
|
|
1122
|
-
function buildPillars(real, communities,
|
|
1123
|
-
const
|
|
1284
|
+
function buildPillars(real, communities, _stack) {
|
|
1285
|
+
const keywordGroups = /* @__PURE__ */ new Map();
|
|
1286
|
+
const unlabeled = [];
|
|
1124
1287
|
for (const a of real) {
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
const pillars =
|
|
1134
|
-
|
|
1135
|
-
const
|
|
1136
|
-
|
|
1288
|
+
if (a.pillarHint && !a.pillarHint.startsWith("community-")) {
|
|
1289
|
+
if (!keywordGroups.has(a.pillarHint))
|
|
1290
|
+
keywordGroups.set(a.pillarHint, []);
|
|
1291
|
+
keywordGroups.get(a.pillarHint).push(a);
|
|
1292
|
+
} else {
|
|
1293
|
+
unlabeled.push(a);
|
|
1294
|
+
}
|
|
1295
|
+
}
|
|
1296
|
+
const pillars = [];
|
|
1297
|
+
for (const [name, files] of keywordGroups) {
|
|
1298
|
+
const sorted = [...files].sort((a, b) => b.gravity - a.gravity);
|
|
1299
|
+
pillars.push({
|
|
1137
1300
|
name,
|
|
1138
|
-
description:
|
|
1139
|
-
memberFiles:
|
|
1140
|
-
};
|
|
1301
|
+
description: `${name} subsystem: ${files.length} file${files.length > 1 ? "s" : ""} centered on ${basename(sorted[0].relativePath)}.`,
|
|
1302
|
+
memberFiles: sorted.map((f) => f.relativePath)
|
|
1303
|
+
});
|
|
1304
|
+
}
|
|
1305
|
+
if (unlabeled.length > 0) {
|
|
1306
|
+
const communityGroups = /* @__PURE__ */ new Map();
|
|
1307
|
+
for (const a of unlabeled) {
|
|
1308
|
+
const c = communities.get(a.relativePath);
|
|
1309
|
+
if (c === void 0)
|
|
1310
|
+
continue;
|
|
1311
|
+
if (!communityGroups.has(c))
|
|
1312
|
+
communityGroups.set(c, []);
|
|
1313
|
+
communityGroups.get(c).push(a);
|
|
1314
|
+
}
|
|
1315
|
+
const remainingSlots = Math.max(0, 6 - pillars.length);
|
|
1316
|
+
const sorted = [...communityGroups.entries()].map(([id, files]) => ({ id, files, weight: files.reduce((s, f) => s + f.gravity, 0) })).filter((g) => g.files.length >= 2).sort((a, b) => b.weight - a.weight).slice(0, remainingSlots);
|
|
1317
|
+
for (const g of sorted) {
|
|
1318
|
+
const top = [...g.files].sort((a, b) => b.gravity - a.gravity);
|
|
1319
|
+
const name = pillarNameFromCluster(top);
|
|
1320
|
+
const existing = pillars.find((p) => p.name === name);
|
|
1321
|
+
if (existing) {
|
|
1322
|
+
existing.memberFiles.push(...top.map((f) => f.relativePath));
|
|
1323
|
+
existing.description = `${name} subsystem: ${existing.memberFiles.length} files centered on ${basename(existing.memberFiles[0])}.`;
|
|
1324
|
+
} else {
|
|
1325
|
+
pillars.push({
|
|
1326
|
+
name,
|
|
1327
|
+
description: `${g.files.length} files centered on ${basename(top[0].relativePath)}.`,
|
|
1328
|
+
memberFiles: top.map((f) => f.relativePath)
|
|
1329
|
+
});
|
|
1330
|
+
}
|
|
1331
|
+
}
|
|
1332
|
+
}
|
|
1333
|
+
pillars.sort((a, b) => {
|
|
1334
|
+
const gravA = real.filter((f) => a.memberFiles.includes(f.relativePath)).reduce((s, f) => s + f.gravity, 0);
|
|
1335
|
+
const gravB = real.filter((f) => b.memberFiles.includes(f.relativePath)).reduce((s, f) => s + f.gravity, 0);
|
|
1336
|
+
return gravB - gravA;
|
|
1141
1337
|
});
|
|
1142
1338
|
const seen = /* @__PURE__ */ new Set();
|
|
1143
1339
|
for (const p of pillars) {
|
|
@@ -1153,19 +1349,33 @@ function buildPillars(real, communities, stack) {
|
|
|
1153
1349
|
}
|
|
1154
1350
|
return pillars;
|
|
1155
1351
|
}
|
|
1156
|
-
function
|
|
1352
|
+
function pillarNameFromCluster(files) {
|
|
1353
|
+
const hintCounts = /* @__PURE__ */ new Map();
|
|
1354
|
+
for (const f of files) {
|
|
1355
|
+
if (f.pillarHint && !f.pillarHint.startsWith("community-")) {
|
|
1356
|
+
hintCounts.set(f.pillarHint, (hintCounts.get(f.pillarHint) || 0) + 1);
|
|
1357
|
+
}
|
|
1358
|
+
}
|
|
1359
|
+
if (hintCounts.size > 0) {
|
|
1360
|
+
const best = [...hintCounts.entries()].sort((a, b) => b[1] - a[1])[0];
|
|
1361
|
+
if (best[1] >= files.length * 0.4)
|
|
1362
|
+
return best[0];
|
|
1363
|
+
}
|
|
1157
1364
|
const dirs = files.map((f) => dirname(f.relativePath)).filter((d) => d && d !== ".");
|
|
1158
1365
|
if (dirs.length) {
|
|
1159
|
-
const
|
|
1366
|
+
const segCounts = /* @__PURE__ */ new Map();
|
|
1160
1367
|
for (const d of dirs) {
|
|
1161
|
-
const
|
|
1162
|
-
|
|
1368
|
+
const segments = d.split(sep).filter((s) => !MEANINGLESS_SEGMENTS.has(s.toLowerCase()));
|
|
1369
|
+
const meaningful = segments.pop();
|
|
1370
|
+
if (meaningful)
|
|
1371
|
+
segCounts.set(meaningful, (segCounts.get(meaningful) || 0) + 1);
|
|
1163
1372
|
}
|
|
1164
|
-
const top = [...
|
|
1373
|
+
const top = [...segCounts.entries()].sort((a, b) => b[1] - a[1])[0];
|
|
1165
1374
|
if (top)
|
|
1166
1375
|
return titleCase(top[0]);
|
|
1167
1376
|
}
|
|
1168
|
-
|
|
1377
|
+
const topFile = basename(files[0].relativePath, extname(files[0].relativePath));
|
|
1378
|
+
return titleCase(topFile);
|
|
1169
1379
|
}
|
|
1170
1380
|
function titleCase(s) {
|
|
1171
1381
|
return s.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
|
|
@@ -1492,6 +1702,7 @@ async function handleGetFileContext(args) {
|
|
|
1492
1702
|
heatSignals: evidence.heatSignals,
|
|
1493
1703
|
importedBy: persisted?.importedBy ?? [],
|
|
1494
1704
|
imports: persisted?.imports ?? [],
|
|
1705
|
+
pillarHint: persisted?.pillarHint ?? null,
|
|
1495
1706
|
signature: evidence.signature,
|
|
1496
1707
|
hotSpans: evidence.hotSpans,
|
|
1497
1708
|
smellSpans: evidence.smellSpans
|
|
@@ -1720,17 +1931,17 @@ var inspectPillarTool = {
|
|
|
1720
1931
|
};
|
|
1721
1932
|
async function handleInspectPillar(args) {
|
|
1722
1933
|
const projectRoot = args.projectRoot;
|
|
1723
|
-
const
|
|
1724
|
-
if (!projectRoot || !
|
|
1934
|
+
const pillarName = args.pillarName;
|
|
1935
|
+
if (!projectRoot || !pillarName)
|
|
1725
1936
|
throw new Error("projectRoot and pillarName are required");
|
|
1726
1937
|
const dossier = await readDossier(projectRoot);
|
|
1727
1938
|
if (!dossier) {
|
|
1728
1939
|
return { error: "No dossier found. Run scan_project first." };
|
|
1729
1940
|
}
|
|
1730
|
-
const pillar = dossier.pillars.find((p) => p.name ===
|
|
1941
|
+
const pillar = dossier.pillars.find((p) => p.name === pillarName);
|
|
1731
1942
|
if (!pillar) {
|
|
1732
1943
|
return {
|
|
1733
|
-
error: `Pillar "${
|
|
1944
|
+
error: `Pillar "${pillarName}" not found. Available pillars: ${dossier.pillars.map((p) => p.name).join(", ")}`
|
|
1734
1945
|
};
|
|
1735
1946
|
}
|
|
1736
1947
|
return pillar;
|
|
@@ -39,6 +39,7 @@ export async function handleGetFileContext(args) {
|
|
|
39
39
|
heatSignals: evidence.heatSignals,
|
|
40
40
|
importedBy: persisted?.importedBy ?? [],
|
|
41
41
|
imports: persisted?.imports ?? [],
|
|
42
|
+
pillarHint: persisted?.pillarHint ?? null,
|
|
42
43
|
signature: evidence.signature,
|
|
43
44
|
hotSpans: evidence.hotSpans,
|
|
44
45
|
smellSpans: evidence.smellSpans,
|