vibe-splain 2.3.0 → 2.4.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
@@ -119,7 +119,80 @@ 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
+ function deriveRiskTypes(f) {
130
+ const kinds = new Set(f.smells.map((s) => s.kind));
131
+ const types = [];
132
+ if (f.gravitySignals.cyclomatic > 15)
133
+ types.push("state_machine");
134
+ if (kinds.has("god-file"))
135
+ types.push("god_object");
136
+ if (f.gravitySignals.fanIn > 15)
137
+ types.push("deep_coupling");
138
+ if (kinds.has("swallowed-catch"))
139
+ types.push("error_sink");
140
+ if (f.gravitySignals.fanIn > 10 && f.gravitySignals.publicSurface > 8)
141
+ types.push("mutation_hotspot");
142
+ if (kinds.has("todo") && kinds.has("suppression"))
143
+ types.push("tech_debt");
144
+ return types.length > 0 ? types : ["complexity_hotspot"];
145
+ }
146
+ function deriveConfidence(f) {
147
+ if (f.gravitySignals.fanIn >= LOAD_BEARING_FAN_IN_THRESHOLD && f.gravity >= 40)
148
+ return "high";
149
+ if (f.gravitySignals.fanIn >= 5 || f.gravity >= 25)
150
+ return "medium";
151
+ return "low";
152
+ }
153
+ function findRuntimeEntrypoints(relPath, files, entrypoints) {
154
+ const found = [];
155
+ const visited = /* @__PURE__ */ new Set();
156
+ const queue = [relPath];
157
+ while (queue.length > 0) {
158
+ const curr = queue.shift();
159
+ if (visited.has(curr))
160
+ continue;
161
+ visited.add(curr);
162
+ if (entrypoints.has(curr) && curr !== relPath)
163
+ found.push(curr);
164
+ if (found.length >= 5)
165
+ break;
166
+ const f = files[curr];
167
+ if (f) {
168
+ for (const importer of f.importedBy)
169
+ if (!visited.has(importer))
170
+ queue.push(importer);
171
+ }
172
+ }
173
+ return found;
174
+ }
175
+ async function writeDeltaTargets(projectRoot, store, entrypoints = /* @__PURE__ */ new Set()) {
176
+ const domain = (f) => f.pillarHint && !f.pillarHint.startsWith("community-") ? f.pillarHint : null;
177
+ const targets = Object.values(store.files).filter((f) => f.isRealSource).sort((a, b) => b.gravity - a.gravity).map((f) => ({
178
+ path: f.relativePath,
179
+ gravity: f.gravity,
180
+ isLoadBearing: f.gravitySignals.fanIn >= LOAD_BEARING_FAN_IN_THRESHOLD,
181
+ blastRadius: f.importedBy,
182
+ pillarHint: domain(f),
183
+ domain: domain(f),
184
+ riskTypes: deriveRiskTypes(f),
185
+ severity: f.smells.length > 0 ? Math.max(...f.smells.map((s) => s.severity)) : 0,
186
+ confidence: deriveConfidence(f),
187
+ runtimeEntrypoints: findRuntimeEntrypoints(f.relativePath, store.files, entrypoints)
188
+ }));
189
+ const dir = join3(projectRoot, ".vibe-splainer");
190
+ await mkdir2(dir, { recursive: true });
191
+ const dest = join3(dir, "delta_targets.json");
192
+ const tmp = dest + ".tmp";
193
+ await writeFile3(tmp, JSON.stringify(targets, null, 2), "utf8");
194
+ const { rename } = await import("fs/promises");
195
+ await rename(tmp, dest);
123
196
  }
124
197
 
125
198
  // ../brain/dist/scanner.js
@@ -234,7 +307,14 @@ var DEMOTE_SEGMENTS = /* @__PURE__ */ new Set([
234
307
  "fixtures",
235
308
  "fixture",
236
309
  "__generated__",
237
- "__mocks__"
310
+ "__mocks__",
311
+ "playwright",
312
+ "e2e",
313
+ "__tests__",
314
+ "cypress",
315
+ "storybook",
316
+ "stories",
317
+ ".storybook"
238
318
  ]);
239
319
  var VENDOR_SEGMENTS = /* @__PURE__ */ new Set([
240
320
  "node_modules",
@@ -1072,6 +1152,19 @@ async function detectStackAndEntrypoints(projectRoot, files) {
1072
1152
  if (base === "main.rs" || base === "lib.rs")
1073
1153
  entrypoints.add(r);
1074
1154
  }
1155
+ if (stack.has("Next.js")) {
1156
+ const appRouterNames = /* @__PURE__ */ new Set(["page", "layout", "route", "loading", "error", "not-found", "template", "default"]);
1157
+ for (const abs of files) {
1158
+ const r = rel(abs);
1159
+ const stem = basename(r, extname(r));
1160
+ const underApp = /(?:^|[/\\])app[/\\]/.test(r);
1161
+ const underPages = /(?:^|[/\\])pages[/\\]/.test(r);
1162
+ if (underApp && appRouterNames.has(stem))
1163
+ entrypoints.add(r);
1164
+ if (underPages && !stem.startsWith("_"))
1165
+ entrypoints.add(r);
1166
+ }
1167
+ }
1075
1168
  return { stack: [...stack], entrypoints };
1076
1169
  }
1077
1170
  var SMELL_WEIGHT = {
@@ -1271,7 +1364,9 @@ async function scanProject(projectRoot) {
1271
1364
  brief: null
1272
1365
  };
1273
1366
  await writeGraph(projectRoot, graph);
1274
- await writeAnalysis(projectRoot, { files: persisted });
1367
+ const analysisStore = { files: persisted };
1368
+ await writeAnalysis(projectRoot, analysisStore);
1369
+ await writeDeltaTargets(projectRoot, analysisStore, entrypoints);
1275
1370
  const uiUrl = `file://${join4(projectRoot, ".vibe-splainer", "ui", "index.html")}`;
1276
1371
  return {
1277
1372
  projectRoot,
@@ -1482,26 +1577,11 @@ async function writeDossier(projectRoot, dossier) {
1482
1577
  p.decisions = p.decisions.filter((c) => !(c.severity === 1 && c.category === "Convention"));
1483
1578
  p.cardCount = p.decisions.length;
1484
1579
  }
1485
- const uniqueCards = /* @__PURE__ */ new Map();
1486
- for (const p of dossier.pillars) {
1487
- for (const c of p.decisions)
1488
- uniqueCards.set(c.id, c);
1489
- }
1490
- for (const c of dossier.wildDiscoveries)
1491
- uniqueCards.set(c.id, c);
1492
- const deltaTargets = Array.from(uniqueCards.values()).filter((c) => c.severity >= 4).map((c) => ({
1493
- target_path: c.primaryFile,
1494
- bottleneck_title: c.title,
1495
- structural_intent: c.thesis,
1496
- evidence_snippets: c.evidence
1497
- }));
1498
1580
  const dir = join5(projectRoot, ".vibe-splainer");
1499
1581
  await mkdir3(dir, { recursive: true });
1500
1582
  const dossierPath = join5(dir, "dossier.json");
1501
1583
  const tmp = dossierPath + ".tmp";
1502
1584
  await writeFile4(tmp, JSON.stringify(dossier, null, 2), "utf8");
1503
- const targetsPath = join5(dir, "delta_targets.json");
1504
- await writeFile4(targetsPath, JSON.stringify(deltaTargets, null, 2), "utf8");
1505
1585
  const { rename } = await import("fs/promises");
1506
1586
  await rename(tmp, dossierPath);
1507
1587
  await regenerateUI(projectRoot, dossier);
@@ -1544,6 +1624,7 @@ function validateMermaidNodeCount(diagram) {
1544
1624
  import chokidar from "chokidar";
1545
1625
  import { createHash } from "crypto";
1546
1626
  import { readFile as readFile6 } from "fs/promises";
1627
+ import { join as join6 } from "path";
1547
1628
  function startWatcher(projectRoot, watchedPaths) {
1548
1629
  const watcher = chokidar.watch(watchedPaths.length > 0 ? watchedPaths : projectRoot, {
1549
1630
  ignoreInitial: true,
@@ -1560,13 +1641,15 @@ function startWatcher(projectRoot, watchedPaths) {
1560
1641
  let mutated = false;
1561
1642
  for (const pillar of dossier.pillars) {
1562
1643
  for (const card of pillar.decisions) {
1563
- if (card.evidence.some((e) => e.file === filepath || filepath.endsWith(e.file))) {
1564
- if (card.lastScannedHash !== newHash) {
1565
- card.status = "stale";
1566
- if (!dossier.stalePaths.includes(filepath))
1567
- dossier.stalePaths.push(filepath);
1568
- mutated = true;
1569
- }
1644
+ if (!card.primaryFile)
1645
+ continue;
1646
+ const absMatch = filepath === join6(projectRoot, card.primaryFile) || filepath.endsWith("/" + card.primaryFile);
1647
+ if (absMatch && card.lastScannedHash !== newHash) {
1648
+ card.status = "stale";
1649
+ const rel = card.primaryFile;
1650
+ if (!dossier.stalePaths.includes(rel))
1651
+ dossier.stalePaths.push(rel);
1652
+ mutated = true;
1570
1653
  }
1571
1654
  }
1572
1655
  }
@@ -1716,7 +1799,7 @@ async function handleSetProjectBrief(args) {
1716
1799
 
1717
1800
  // dist/mcp/tools/get_file_context.js
1718
1801
  import { readFile as readFile7 } from "fs/promises";
1719
- import { join as join6, relative as relative2, isAbsolute } from "path";
1802
+ import { join as join7, relative as relative2, isAbsolute } from "path";
1720
1803
  var getFileContextTool = {
1721
1804
  name: "get_file_context",
1722
1805
  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.",
@@ -1736,7 +1819,7 @@ async function handleGetFileContext(args) {
1736
1819
  const full = args.full === true;
1737
1820
  if (!projectRoot || !filePath)
1738
1821
  throw new Error("projectRoot and filePath are required");
1739
- const fullPath = isAbsolute(filePath) ? filePath : join6(projectRoot, filePath);
1822
+ const fullPath = isAbsolute(filePath) ? filePath : join7(projectRoot, filePath);
1740
1823
  const relPath = relative2(projectRoot, fullPath);
1741
1824
  const evidence = await getFileAnalysis(fullPath);
1742
1825
  if (!evidence) {
@@ -1770,7 +1853,7 @@ async function handleGetFileContext(args) {
1770
1853
  import { v4 as uuidv4 } from "uuid";
1771
1854
  import { createHash as createHash2 } from "crypto";
1772
1855
  import { readFile as readFile8 } from "fs/promises";
1773
- import { join as join7 } from "path";
1856
+ import { join as join8 } from "path";
1774
1857
  var CATEGORIES = ["Bottleneck", "Hack", "Smart-Move", "Risk", "Convention", "Dead-Weight"];
1775
1858
  function normalizeSnippet(s) {
1776
1859
  let out = (s ?? "").replace(/\r\n/g, "\n");
@@ -1860,15 +1943,12 @@ async function handleWriteDecisionCard(args) {
1860
1943
  const persisted = store?.files[primaryFile];
1861
1944
  const gravity = persisted ? Math.round(persisted.gravity) : void 0;
1862
1945
  const heat = persisted ? Math.round(persisted.heat) : void 0;
1863
- let combinedContent = "";
1864
- for (const e of evidence) {
1865
- try {
1866
- combinedContent += await readFile8(join7(projectRoot, e.file), "utf8");
1867
- } catch {
1868
- combinedContent += e.snippet;
1869
- }
1946
+ let primaryContent = "";
1947
+ try {
1948
+ primaryContent = await readFile8(join8(projectRoot, primaryFile), "utf8");
1949
+ } catch {
1870
1950
  }
1871
- const hash = createHash2("sha256").update(combinedContent).digest("hex");
1951
+ const hash = createHash2("sha256").update(primaryContent).digest("hex");
1872
1952
  const card = {
1873
1953
  id: uuidv4(),
1874
1954
  pillar,
@@ -2003,7 +2083,7 @@ async function handleInspectPillar(args) {
2003
2083
  // dist/mcp/tools/get_wild_discoveries.js
2004
2084
  var getWildDiscoveriesTool = {
2005
2085
  name: "get_wild_discoveries",
2006
- 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.",
2086
+ 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.",
2007
2087
  inputSchema: {
2008
2088
  type: "object",
2009
2089
  properties: {
@@ -2234,7 +2314,7 @@ async function serveCommand() {
2234
2314
 
2235
2315
  // dist/index.js
2236
2316
  var program = new Command();
2237
- program.name("vibe-splain").description("Architectural dossier engine for vibe-coded projects").version("1.0.0");
2317
+ program.name("vibe-splain").description("Architectural dossier engine for vibe-coded projects").version("2.3.1");
2238
2318
  program.command("install").description("Patch coding agent MCP config files to register vibe-splain").action(installCommand);
2239
2319
  program.command("serve").description("Start the MCP server (called by the coding agent, not by you)").action(serveCommand);
2240
2320
  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: {
@@ -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.3.0",
4
- "description": "Architectural dossier engine for vibe-coded projects. Runs as an MCP server inside your coding agent.",
3
+ "version": "2.4.0",
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",
@@ -30,7 +30,7 @@
30
30
  "node": ">=18"
31
31
  },
32
32
  "bin": {
33
- "vibe-splain": "./dist/index.js"
33
+ "vibe-splain": "dist/index.js"
34
34
  },
35
35
  "scripts": {
36
36
  "build": "tsc && node build.mjs"