vibe-splain 2.3.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
@@ -1271,7 +1293,9 @@ async function scanProject(projectRoot) {
1271
1293
  brief: null
1272
1294
  };
1273
1295
  await writeGraph(projectRoot, graph);
1274
- await writeAnalysis(projectRoot, { files: persisted });
1296
+ const analysisStore = { files: persisted };
1297
+ await writeAnalysis(projectRoot, analysisStore);
1298
+ await writeDeltaTargets(projectRoot, analysisStore);
1275
1299
  const uiUrl = `file://${join4(projectRoot, ".vibe-splainer", "ui", "index.html")}`;
1276
1300
  return {
1277
1301
  projectRoot,
@@ -1482,26 +1506,11 @@ async function writeDossier(projectRoot, dossier) {
1482
1506
  p.decisions = p.decisions.filter((c) => !(c.severity === 1 && c.category === "Convention"));
1483
1507
  p.cardCount = p.decisions.length;
1484
1508
  }
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
1509
  const dir = join5(projectRoot, ".vibe-splainer");
1499
1510
  await mkdir3(dir, { recursive: true });
1500
1511
  const dossierPath = join5(dir, "dossier.json");
1501
1512
  const tmp = dossierPath + ".tmp";
1502
1513
  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
1514
  const { rename } = await import("fs/promises");
1506
1515
  await rename(tmp, dossierPath);
1507
1516
  await regenerateUI(projectRoot, dossier);
@@ -1544,6 +1553,7 @@ function validateMermaidNodeCount(diagram) {
1544
1553
  import chokidar from "chokidar";
1545
1554
  import { createHash } from "crypto";
1546
1555
  import { readFile as readFile6 } from "fs/promises";
1556
+ import { join as join6 } from "path";
1547
1557
  function startWatcher(projectRoot, watchedPaths) {
1548
1558
  const watcher = chokidar.watch(watchedPaths.length > 0 ? watchedPaths : projectRoot, {
1549
1559
  ignoreInitial: true,
@@ -1560,13 +1570,15 @@ function startWatcher(projectRoot, watchedPaths) {
1560
1570
  let mutated = false;
1561
1571
  for (const pillar of dossier.pillars) {
1562
1572
  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
- }
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;
1570
1582
  }
1571
1583
  }
1572
1584
  }
@@ -1716,7 +1728,7 @@ async function handleSetProjectBrief(args) {
1716
1728
 
1717
1729
  // dist/mcp/tools/get_file_context.js
1718
1730
  import { readFile as readFile7 } from "fs/promises";
1719
- import { join as join6, relative as relative2, isAbsolute } from "path";
1731
+ import { join as join7, relative as relative2, isAbsolute } from "path";
1720
1732
  var getFileContextTool = {
1721
1733
  name: "get_file_context",
1722
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.",
@@ -1736,7 +1748,7 @@ async function handleGetFileContext(args) {
1736
1748
  const full = args.full === true;
1737
1749
  if (!projectRoot || !filePath)
1738
1750
  throw new Error("projectRoot and filePath are required");
1739
- const fullPath = isAbsolute(filePath) ? filePath : join6(projectRoot, filePath);
1751
+ const fullPath = isAbsolute(filePath) ? filePath : join7(projectRoot, filePath);
1740
1752
  const relPath = relative2(projectRoot, fullPath);
1741
1753
  const evidence = await getFileAnalysis(fullPath);
1742
1754
  if (!evidence) {
@@ -1770,7 +1782,7 @@ async function handleGetFileContext(args) {
1770
1782
  import { v4 as uuidv4 } from "uuid";
1771
1783
  import { createHash as createHash2 } from "crypto";
1772
1784
  import { readFile as readFile8 } from "fs/promises";
1773
- import { join as join7 } from "path";
1785
+ import { join as join8 } from "path";
1774
1786
  var CATEGORIES = ["Bottleneck", "Hack", "Smart-Move", "Risk", "Convention", "Dead-Weight"];
1775
1787
  function normalizeSnippet(s) {
1776
1788
  let out = (s ?? "").replace(/\r\n/g, "\n");
@@ -1860,15 +1872,12 @@ async function handleWriteDecisionCard(args) {
1860
1872
  const persisted = store?.files[primaryFile];
1861
1873
  const gravity = persisted ? Math.round(persisted.gravity) : void 0;
1862
1874
  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
- }
1875
+ let primaryContent = "";
1876
+ try {
1877
+ primaryContent = await readFile8(join8(projectRoot, primaryFile), "utf8");
1878
+ } catch {
1870
1879
  }
1871
- const hash = createHash2("sha256").update(combinedContent).digest("hex");
1880
+ const hash = createHash2("sha256").update(primaryContent).digest("hex");
1872
1881
  const card = {
1873
1882
  id: uuidv4(),
1874
1883
  pillar,
@@ -2003,7 +2012,7 @@ async function handleInspectPillar(args) {
2003
2012
  // dist/mcp/tools/get_wild_discoveries.js
2004
2013
  var getWildDiscoveriesTool = {
2005
2014
  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.",
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.",
2007
2016
  inputSchema: {
2008
2017
  type: "object",
2009
2018
  properties: {
@@ -2234,7 +2243,7 @@ async function serveCommand() {
2234
2243
 
2235
2244
  // dist/index.js
2236
2245
  var program = new Command();
2237
- 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");
2238
2247
  program.command("install").description("Patch coding agent MCP config files to register vibe-splain").action(installCommand);
2239
2248
  program.command("serve").description("Start the MCP server (called by the coding agent, not by you)").action(serveCommand);
2240
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: {
@@ -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.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",