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
|
-
|
|
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
|
-
|
|
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.
|
|
1564
|
-
|
|
1565
|
-
|
|
1566
|
-
|
|
1567
|
-
|
|
1568
|
-
|
|
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
|
|
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 :
|
|
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
|
|
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
|
|
1864
|
-
|
|
1865
|
-
|
|
1866
|
-
|
|
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(
|
|
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
|
|
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("
|
|
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
|
|
4
|
+
description: 'Returns Decision Cards that are both high-heat (heat ≥ 60) AND/OR high-severity (severity ≥ 4) — 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
|
|
103
|
-
let
|
|
104
|
-
|
|
105
|
-
|
|
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
|
-
|
|
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.
|
|
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": "
|
|
33
|
+
"vibe-splain": "dist/index.js"
|
|
34
34
|
},
|
|
35
35
|
"scripts": {
|
|
36
36
|
"build": "tsc && node build.mjs"
|