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
|
-
|
|
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) +
|
|
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
|
|
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
|
-
|
|
1188
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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
|
-
|
|
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.
|
|
1511
|
-
|
|
1512
|
-
|
|
1513
|
-
|
|
1514
|
-
|
|
1515
|
-
|
|
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
|
|
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 :
|
|
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
|
|
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
|
|
1811
|
-
|
|
1812
|
-
|
|
1813
|
-
|
|
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(
|
|
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
|
|
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("
|
|
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
|
|
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: {
|
|
@@ -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
|
|
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.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",
|