vibe-splain 2.2.0 → 2.3.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 CHANGED
@@ -119,7 +119,29 @@ async function readAnalysis(projectRoot) {
119
119
  async function writeAnalysis(projectRoot, store) {
120
120
  const dir = join3(projectRoot, ".vibe-splainer");
121
121
  await mkdir2(dir, { recursive: true });
122
- await writeFile3(join3(dir, "analysis.json"), JSON.stringify(store, null, 2), "utf8");
122
+ const dest = join3(dir, "analysis.json");
123
+ const tmp = dest + ".tmp";
124
+ await writeFile3(tmp, JSON.stringify(store, null, 2), "utf8");
125
+ const { rename } = await import("fs/promises");
126
+ await rename(tmp, dest);
127
+ }
128
+ var LOAD_BEARING_FAN_IN_THRESHOLD = 10;
129
+ async function writeDeltaTargets(projectRoot, store) {
130
+ const targets = Object.values(store.files).filter((f) => f.isRealSource).sort((a, b) => b.gravity - a.gravity).map((f) => ({
131
+ path: f.relativePath,
132
+ gravity: f.gravity,
133
+ isLoadBearing: f.gravitySignals.fanIn >= LOAD_BEARING_FAN_IN_THRESHOLD,
134
+ blastRadius: f.importedBy,
135
+ // community-{N} labels are internal graph IDs — not useful to Delta Engine
136
+ pillarHint: f.pillarHint && !f.pillarHint.startsWith("community-") ? f.pillarHint : null
137
+ }));
138
+ const dir = join3(projectRoot, ".vibe-splainer");
139
+ await mkdir2(dir, { recursive: true });
140
+ const dest = join3(dir, "delta_targets.json");
141
+ const tmp = dest + ".tmp";
142
+ await writeFile3(tmp, JSON.stringify(targets, null, 2), "utf8");
143
+ const { rename } = await import("fs/promises");
144
+ await rename(tmp, dest);
123
145
  }
124
146
 
125
147
  // ../brain/dist/scanner.js
@@ -981,9 +1003,9 @@ function detectCommunities(nodes, adjacency) {
981
1003
  if (!neighbors || neighbors.size === 0)
982
1004
  continue;
983
1005
  const counts = /* @__PURE__ */ new Map();
984
- for (const nb of neighbors) {
1006
+ for (const [nb, weight] of neighbors) {
985
1007
  const l = label.get(nb);
986
- counts.set(l, (counts.get(l) || 0) + 1);
1008
+ counts.set(l, (counts.get(l) || 0) + weight);
987
1009
  }
988
1010
  let best = label.get(node), bestCount = -1;
989
1011
  for (const [l, c] of counts) {
@@ -1175,7 +1197,7 @@ async function scanProject(projectRoot) {
1175
1197
  const undirected = /* @__PURE__ */ new Map();
1176
1198
  for (const node of realNodes) {
1177
1199
  outEdges.set(node, /* @__PURE__ */ new Set());
1178
- undirected.set(node, /* @__PURE__ */ new Set());
1200
+ undirected.set(node, /* @__PURE__ */ new Map());
1179
1201
  }
1180
1202
  for (const w of work) {
1181
1203
  if (!realSet.has(w.rel))
@@ -1184,8 +1206,11 @@ async function scanProject(projectRoot) {
1184
1206
  if (!realSet.has(target))
1185
1207
  continue;
1186
1208
  outEdges.get(w.rel).add(target);
1187
- undirected.get(w.rel).add(target);
1188
- undirected.get(target).add(w.rel);
1209
+ const wDir = w.rel.split(sep)[0];
1210
+ const tDir = target.split(sep)[0];
1211
+ const weight = wDir === tDir ? 1 : 0.5;
1212
+ undirected.get(w.rel).set(target, weight);
1213
+ undirected.get(target).set(w.rel, weight);
1189
1214
  }
1190
1215
  }
1191
1216
  const ranks = pageRank(realNodes, outEdges);
@@ -1268,7 +1293,9 @@ async function scanProject(projectRoot) {
1268
1293
  brief: null
1269
1294
  };
1270
1295
  await writeGraph(projectRoot, graph);
1271
- await writeAnalysis(projectRoot, { files: persisted });
1296
+ const analysisStore = { files: persisted };
1297
+ await writeAnalysis(projectRoot, analysisStore);
1298
+ await writeDeltaTargets(projectRoot, analysisStore);
1272
1299
  const uiUrl = `file://${join4(projectRoot, ".vibe-splainer", "ui", "index.html")}`;
1273
1300
  return {
1274
1301
  projectRoot,
@@ -1335,8 +1362,42 @@ function buildPillars(real, communities, _stack) {
1335
1362
  const gravB = real.filter((f) => b.memberFiles.includes(f.relativePath)).reduce((s, f) => s + f.gravity, 0);
1336
1363
  return gravB - gravA;
1337
1364
  });
1338
- const seen = /* @__PURE__ */ new Set();
1365
+ if (pillars.length === 0 && real.length > 0) {
1366
+ pillars.push({ name: "Core", description: "Primary application code.", memberFiles: real.slice(0, 20).map((f) => f.relativePath) });
1367
+ }
1368
+ const finalPillars = [];
1339
1369
  for (const p of pillars) {
1370
+ if (p.memberFiles.length > 15) {
1371
+ const groups = /* @__PURE__ */ new Map();
1372
+ for (const f of p.memberFiles) {
1373
+ let bucket = "Core";
1374
+ if (f.includes("app/") || f.includes("pages/") || f.includes("routes/"))
1375
+ bucket = "Routing";
1376
+ else if (f.includes("components/") || f.includes("ui/"))
1377
+ bucket = "Components";
1378
+ else if (f.includes("hooks/") || f.includes("lib/") || f.includes("utils/"))
1379
+ bucket = "Logic";
1380
+ const d = basename(dirname(f));
1381
+ const key = `${p.name} (${bucket} - ${d})`;
1382
+ if (!groups.has(key))
1383
+ groups.set(key, []);
1384
+ groups.get(key).push(f);
1385
+ }
1386
+ for (const [key, files] of groups) {
1387
+ if (files.length > 0) {
1388
+ finalPillars.push({
1389
+ name: key,
1390
+ description: `Subdivided from ${p.name}`,
1391
+ memberFiles: files
1392
+ });
1393
+ }
1394
+ }
1395
+ } else {
1396
+ finalPillars.push(p);
1397
+ }
1398
+ }
1399
+ const seen = /* @__PURE__ */ new Set();
1400
+ for (const p of finalPillars) {
1340
1401
  let n = p.name, i = 2;
1341
1402
  while (seen.has(n)) {
1342
1403
  n = `${p.name} ${i++}`;
@@ -1344,10 +1405,7 @@ function buildPillars(real, communities, _stack) {
1344
1405
  p.name = n;
1345
1406
  seen.add(n);
1346
1407
  }
1347
- if (pillars.length === 0 && real.length > 0) {
1348
- pillars.push({ name: "Core", description: "Primary application code.", memberFiles: real.slice(0, 20).map((f) => f.relativePath) });
1349
- }
1350
- return pillars;
1408
+ return finalPillars;
1351
1409
  }
1352
1410
  function pillarNameFromCluster(files) {
1353
1411
  const hintCounts = /* @__PURE__ */ new Map();
@@ -1444,6 +1502,10 @@ async function readDossier(projectRoot) {
1444
1502
  }
1445
1503
  async function writeDossier(projectRoot, dossier) {
1446
1504
  await dossierMutex.runExclusive(async () => {
1505
+ for (const p of dossier.pillars) {
1506
+ p.decisions = p.decisions.filter((c) => !(c.severity === 1 && c.category === "Convention"));
1507
+ p.cardCount = p.decisions.length;
1508
+ }
1447
1509
  const dir = join5(projectRoot, ".vibe-splainer");
1448
1510
  await mkdir3(dir, { recursive: true });
1449
1511
  const dossierPath = join5(dir, "dossier.json");
@@ -1491,6 +1553,7 @@ function validateMermaidNodeCount(diagram) {
1491
1553
  import chokidar from "chokidar";
1492
1554
  import { createHash } from "crypto";
1493
1555
  import { readFile as readFile6 } from "fs/promises";
1556
+ import { join as join6 } from "path";
1494
1557
  function startWatcher(projectRoot, watchedPaths) {
1495
1558
  const watcher = chokidar.watch(watchedPaths.length > 0 ? watchedPaths : projectRoot, {
1496
1559
  ignoreInitial: true,
@@ -1507,13 +1570,15 @@ function startWatcher(projectRoot, watchedPaths) {
1507
1570
  let mutated = false;
1508
1571
  for (const pillar of dossier.pillars) {
1509
1572
  for (const card of pillar.decisions) {
1510
- if (card.evidence.some((e) => e.file === filepath || filepath.endsWith(e.file))) {
1511
- if (card.lastScannedHash !== newHash) {
1512
- card.status = "stale";
1513
- if (!dossier.stalePaths.includes(filepath))
1514
- dossier.stalePaths.push(filepath);
1515
- mutated = true;
1516
- }
1573
+ if (!card.primaryFile)
1574
+ continue;
1575
+ const absMatch = filepath === join6(projectRoot, card.primaryFile) || filepath.endsWith("/" + card.primaryFile);
1576
+ if (absMatch && card.lastScannedHash !== newHash) {
1577
+ card.status = "stale";
1578
+ const rel = card.primaryFile;
1579
+ if (!dossier.stalePaths.includes(rel))
1580
+ dossier.stalePaths.push(rel);
1581
+ mutated = true;
1517
1582
  }
1518
1583
  }
1519
1584
  }
@@ -1663,7 +1728,7 @@ async function handleSetProjectBrief(args) {
1663
1728
 
1664
1729
  // dist/mcp/tools/get_file_context.js
1665
1730
  import { readFile as readFile7 } from "fs/promises";
1666
- import { join as join6, relative as relative2, isAbsolute } from "path";
1731
+ import { join as join7, relative as relative2, isAbsolute } from "path";
1667
1732
  var getFileContextTool = {
1668
1733
  name: "get_file_context",
1669
1734
  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.",
@@ -1683,7 +1748,7 @@ async function handleGetFileContext(args) {
1683
1748
  const full = args.full === true;
1684
1749
  if (!projectRoot || !filePath)
1685
1750
  throw new Error("projectRoot and filePath are required");
1686
- const fullPath = isAbsolute(filePath) ? filePath : join6(projectRoot, filePath);
1751
+ const fullPath = isAbsolute(filePath) ? filePath : join7(projectRoot, filePath);
1687
1752
  const relPath = relative2(projectRoot, fullPath);
1688
1753
  const evidence = await getFileAnalysis(fullPath);
1689
1754
  if (!evidence) {
@@ -1717,7 +1782,7 @@ async function handleGetFileContext(args) {
1717
1782
  import { v4 as uuidv4 } from "uuid";
1718
1783
  import { createHash as createHash2 } from "crypto";
1719
1784
  import { readFile as readFile8 } from "fs/promises";
1720
- import { join as join7 } from "path";
1785
+ import { join as join8 } from "path";
1721
1786
  var CATEGORIES = ["Bottleneck", "Hack", "Smart-Move", "Risk", "Convention", "Dead-Weight"];
1722
1787
  function normalizeSnippet(s) {
1723
1788
  let out = (s ?? "").replace(/\r\n/g, "\n");
@@ -1742,7 +1807,7 @@ var writeDecisionCardTool = {
1742
1807
  narrative: { type: "string", description: "3-5 sentences. WHY it exists and WHY it's built this way. Do NOT restate the file's header comments." },
1743
1808
  tradeoff: { type: "string", description: "What was given up, or why the obvious approach was rejected. Null only if genuinely none." },
1744
1809
  blastRadius: { type: "string", description: "What breaks if this changes. Ground it in the fan-in (importedBy) from get_file_context." },
1745
- confidence: { type: "string", enum: ["low", "medium", "high"] },
1810
+ confidence: { type: "string", enum: ["low", "medium", "high"], description: 'Do NOT default to "high". Reserve "high" ONLY for provable execution anti-patterns. Score subjective stylistic choices or abstractions as "low" or "medium".' },
1746
1811
  evidence: {
1747
1812
  type: "array",
1748
1813
  items: {
@@ -1807,15 +1872,12 @@ async function handleWriteDecisionCard(args) {
1807
1872
  const persisted = store?.files[primaryFile];
1808
1873
  const gravity = persisted ? Math.round(persisted.gravity) : void 0;
1809
1874
  const heat = persisted ? Math.round(persisted.heat) : void 0;
1810
- let combinedContent = "";
1811
- for (const e of evidence) {
1812
- try {
1813
- combinedContent += await readFile8(join7(projectRoot, e.file), "utf8");
1814
- } catch {
1815
- combinedContent += e.snippet;
1816
- }
1875
+ let primaryContent = "";
1876
+ try {
1877
+ primaryContent = await readFile8(join8(projectRoot, primaryFile), "utf8");
1878
+ } catch {
1817
1879
  }
1818
- const hash = createHash2("sha256").update(combinedContent).digest("hex");
1880
+ const hash = createHash2("sha256").update(primaryContent).digest("hex");
1819
1881
  const card = {
1820
1882
  id: uuidv4(),
1821
1883
  pillar,
@@ -1950,7 +2012,7 @@ async function handleInspectPillar(args) {
1950
2012
  // dist/mcp/tools/get_wild_discoveries.js
1951
2013
  var getWildDiscoveriesTool = {
1952
2014
  name: "get_wild_discoveries",
1953
- description: "Returns files with extremely high cognitive complexity (weight \u2265 25) that don't fit standard patterns. These are the most surprising and important parts of the codebase to understand.",
2015
+ description: "Returns Decision Cards that are both high-heat (heat \u2265 60) AND/OR high-severity (severity \u2265 4) \u2014 the files that are load-bearing AND smelly. These are the highest-leverage things to understand and fix first.",
1954
2016
  inputSchema: {
1955
2017
  type: "object",
1956
2018
  properties: {
@@ -2181,7 +2243,7 @@ async function serveCommand() {
2181
2243
 
2182
2244
  // dist/index.js
2183
2245
  var program = new Command();
2184
- program.name("vibe-splain").description("Architectural dossier engine for vibe-coded projects").version("1.0.0");
2246
+ program.name("vibe-splain").description("Architectural dossier engine for vibe-coded projects").version("2.3.1");
2185
2247
  program.command("install").description("Patch coding agent MCP config files to register vibe-splain").action(installCommand);
2186
2248
  program.command("serve").description("Start the MCP server (called by the coding agent, not by you)").action(serveCommand);
2187
2249
  program.parse();
@@ -1,7 +1,7 @@
1
1
  import { readDossier } from '@vibe-splain/brain';
2
2
  export const getWildDiscoveriesTool = {
3
3
  name: 'get_wild_discoveries',
4
- description: 'Returns files with extremely high cognitive complexity (weight25) that don\'t fit standard patterns. These are the most surprising and important parts of the codebase to understand.',
4
+ description: 'Returns Decision Cards that are both high-heat (heat 60) AND/OR high-severity (severity4) — the files that are load-bearing AND smelly. These are the highest-leverage things to understand and fix first.',
5
5
  inputSchema: {
6
6
  type: 'object',
7
7
  properties: {
@@ -47,6 +47,7 @@ export declare const writeDecisionCardTool: {
47
47
  confidence: {
48
48
  type: string;
49
49
  enum: string[];
50
+ description: string;
50
51
  };
51
52
  evidence: {
52
53
  type: string;
@@ -29,7 +29,7 @@ export const writeDecisionCardTool = {
29
29
  narrative: { type: 'string', description: "3-5 sentences. WHY it exists and WHY it's built this way. Do NOT restate the file's header comments." },
30
30
  tradeoff: { type: 'string', description: 'What was given up, or why the obvious approach was rejected. Null only if genuinely none.' },
31
31
  blastRadius: { type: 'string', description: 'What breaks if this changes. Ground it in the fan-in (importedBy) from get_file_context.' },
32
- confidence: { type: 'string', enum: ['low', 'medium', 'high'] },
32
+ confidence: { type: 'string', enum: ['low', 'medium', 'high'], description: 'Do NOT default to "high". Reserve "high" ONLY for provable execution anti-patterns. Score subjective stylistic choices or abstractions as "low" or "medium".' },
33
33
  evidence: {
34
34
  type: 'array',
35
35
  items: {
@@ -99,17 +99,13 @@ export async function handleWriteDecisionCard(args) {
99
99
  const persisted = store?.files[primaryFile];
100
100
  const gravity = persisted ? Math.round(persisted.gravity) : undefined;
101
101
  const heat = persisted ? Math.round(persisted.heat) : undefined;
102
- // Hash for staleness.
103
- let combinedContent = '';
104
- for (const e of evidence) {
105
- try {
106
- combinedContent += await readFile(join(projectRoot, e.file), 'utf8');
107
- }
108
- catch {
109
- combinedContent += e.snippet;
110
- }
102
+ // Hash the primaryFile so the watcher can detect staleness per-file.
103
+ let primaryContent = '';
104
+ try {
105
+ primaryContent = await readFile(join(projectRoot, primaryFile), 'utf8');
111
106
  }
112
- const hash = createHash('sha256').update(combinedContent).digest('hex');
107
+ catch { /* */ }
108
+ const hash = createHash('sha256').update(primaryContent).digest('hex');
113
109
  const card = {
114
110
  id: uuidv4(),
115
111
  pillar, title, thesis, category, severity, narrative,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "vibe-splain",
3
- "version": "2.2.0",
4
- "description": "Architectural dossier engine for vibe-coded projects. Runs as an MCP server inside your coding agent.",
3
+ "version": "2.3.1",
4
+ "description": "Architectural dossier engine for vibe-coded TypeScript/JavaScript projects. Runs as an MCP server inside your coding agent.",
5
5
  "type": "module",
6
6
  "license": "MIT",
7
7
  "author": "abp2204",