vibe-splain 2.1.0 → 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 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
- let gravityRaw = centrality * 50 + Math.log2(fanIn + 1) * 8 + Math.log2(w.ast.cyclomatic + 1) * 4 + Math.log2(w.ast.publicSurface + 1) * 3;
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 pillarHint = real ? `community-${communities.get(w.rel)}` : null;
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, stack) {
1123
- const groups = /* @__PURE__ */ new Map();
1284
+ function buildPillars(real, communities, _stack) {
1285
+ const keywordGroups = /* @__PURE__ */ new Map();
1286
+ const unlabeled = [];
1124
1287
  for (const a of real) {
1125
- const c = communities.get(a.relativePath);
1126
- if (c === void 0)
1127
- continue;
1128
- if (!groups.has(c))
1129
- groups.set(c, []);
1130
- groups.get(c).push(a);
1131
- }
1132
- const sorted = [...groups.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, 6);
1133
- const pillars = sorted.map((g, idx) => {
1134
- const top = [...g.files].sort((a, b) => b.gravity - a.gravity);
1135
- const name = pillarName(top, idx);
1136
- return {
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: `Graph cluster of ${g.files.length} files centered on ${basename(top[0].relativePath)}.`,
1139
- memberFiles: top.map((f) => f.relativePath)
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 pillarName(files, idx) {
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 counts = /* @__PURE__ */ new Map();
1366
+ const segCounts = /* @__PURE__ */ new Map();
1160
1367
  for (const d of dirs) {
1161
- const seg = d.split(sep).pop();
1162
- counts.set(seg, (counts.get(seg) || 0) + 1);
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 = [...counts.entries()].sort((a, b) => b[1] - a[1])[0];
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
- return `Cluster ${idx + 1}`;
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 pillarName2 = args.pillarName;
1724
- if (!projectRoot || !pillarName2)
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 === pillarName2);
1941
+ const pillar = dossier.pillars.find((p) => p.name === pillarName);
1731
1942
  if (!pillar) {
1732
1943
  return {
1733
- error: `Pillar "${pillarName2}" not found. Available pillars: ${dossier.pillars.map((p) => p.name).join(", ")}`
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,