vibe-splain 2.7.2 → 3.0.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/README.md +237 -0
- package/dist/commands/export.d.ts +1 -0
- package/dist/commands/export.js +19 -0
- package/dist/commands/serve.d.ts +1 -1
- package/dist/commands/serve.js +2 -2
- package/dist/export/ArtifactBundleWriter.d.ts +22 -0
- package/dist/export/ArtifactBundleWriter.js +56 -0
- package/dist/export/ExportOrchestrator.d.ts +12 -0
- package/dist/export/ExportOrchestrator.js +72 -0
- package/dist/export/Watcher.d.ts +1 -0
- package/dist/export/Watcher.js +47 -0
- package/dist/export/renderers/AgentMarkdownRenderer.d.ts +8 -0
- package/dist/export/renderers/AgentMarkdownRenderer.js +90 -0
- package/dist/export/renderers/DeltaRenderer.d.ts +6 -0
- package/dist/export/renderers/DeltaRenderer.js +22 -0
- package/dist/export/renderers/GraphRenderer.d.ts +9 -0
- package/dist/export/renderers/GraphRenderer.js +18 -0
- package/dist/export/renderers/HtmlRenderer.d.ts +6 -0
- package/dist/export/renderers/HtmlRenderer.js +54 -0
- package/dist/export/renderers/JsonRenderer.d.ts +6 -0
- package/dist/export/renderers/JsonRenderer.js +12 -0
- package/dist/export/renderers/RawAnalysisRenderer.d.ts +6 -0
- package/dist/export/renderers/RawAnalysisRenderer.js +12 -0
- package/dist/export/renderers/Renderer.d.ts +5 -0
- package/dist/export/renderers/Renderer.js +2 -0
- package/dist/export/renderers/ValidationRenderer.d.ts +6 -0
- package/dist/export/renderers/ValidationRenderer.js +14 -0
- package/dist/index.js +722 -574
- package/dist/mcp/server.d.ts +1 -1
- package/dist/mcp/server.js +3 -3
- package/dist/mcp/tools/mark_stale.d.ts +1 -1
- package/dist/mcp/tools/mark_stale.js +9 -3
- package/dist/mcp/tools/scan_project.d.ts +1 -1
- package/dist/mcp/tools/scan_project.js +24 -4
- package/dist/mcp/tools/set_project_brief.d.ts +1 -1
- package/dist/mcp/tools/set_project_brief.js +9 -3
- package/dist/mcp/tools/write_decision_card.d.ts +1 -1
- package/dist/mcp/tools/write_decision_card.js +15 -4
- package/dist/ui/index.html +2 -2
- package/package.json +1 -1
package/dist/index.js
CHANGED
|
@@ -89,49 +89,17 @@ import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema
|
|
|
89
89
|
|
|
90
90
|
// ../brain/dist/scanner.js
|
|
91
91
|
import { extname as extname4 } from "path";
|
|
92
|
-
import { readFile as
|
|
92
|
+
import { readFile as readFile6 } from "fs/promises";
|
|
93
93
|
|
|
94
94
|
// ../brain/dist/pipeline/orchestrator.js
|
|
95
|
-
import { join as
|
|
96
|
-
|
|
97
|
-
// ../brain/dist/graph.js
|
|
98
|
-
import { join as join2 } from "path";
|
|
99
|
-
import { readFile as readFile2, writeFile as writeFile2, mkdir } from "fs/promises";
|
|
100
|
-
async function writeGraph(projectRoot, graph) {
|
|
101
|
-
const dir = join2(projectRoot, ".vibe-splainer");
|
|
102
|
-
await mkdir(dir, { recursive: true });
|
|
103
|
-
const graphPath = join2(dir, "graph.json");
|
|
104
|
-
await writeFile2(graphPath, JSON.stringify(graph, null, 2), "utf8");
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
// ../brain/dist/analysis.js
|
|
108
|
-
import { join as join3 } from "path";
|
|
109
|
-
import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir2 } from "fs/promises";
|
|
110
|
-
async function readAnalysis(projectRoot) {
|
|
111
|
-
const p = join3(projectRoot, ".vibe-splainer", "analysis.json");
|
|
112
|
-
try {
|
|
113
|
-
const raw = await readFile3(p, "utf8");
|
|
114
|
-
return JSON.parse(raw);
|
|
115
|
-
} catch {
|
|
116
|
-
return null;
|
|
117
|
-
}
|
|
118
|
-
}
|
|
119
|
-
async function writeAnalysis(projectRoot, store) {
|
|
120
|
-
const dir = join3(projectRoot, ".vibe-splainer");
|
|
121
|
-
await mkdir2(dir, { recursive: true });
|
|
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
|
-
}
|
|
95
|
+
import { join as join7 } from "path";
|
|
128
96
|
|
|
129
97
|
// ../brain/dist/pipeline/inventory.js
|
|
130
98
|
import Parser from "web-tree-sitter";
|
|
131
|
-
import { join as
|
|
99
|
+
import { join as join2, dirname, relative, extname, basename, sep } from "path";
|
|
132
100
|
import { fileURLToPath } from "url";
|
|
133
101
|
import { createRequire } from "module";
|
|
134
|
-
import { readFile as
|
|
102
|
+
import { readFile as readFile2, readdir, writeFile as writeFile2, mkdir } from "fs/promises";
|
|
135
103
|
import { existsSync as existsSync2 } from "fs";
|
|
136
104
|
var __dirname = dirname(fileURLToPath(import.meta.url));
|
|
137
105
|
var require2 = createRequire(import.meta.url);
|
|
@@ -162,12 +130,12 @@ var SUPPORTED_EXTENSIONS = new Set(Object.keys(EXT_LANG));
|
|
|
162
130
|
function resolveWasm(file) {
|
|
163
131
|
try {
|
|
164
132
|
const wasmsDir = dirname(require2.resolve("tree-sitter-wasms/package.json"));
|
|
165
|
-
const p =
|
|
133
|
+
const p = join2(wasmsDir, "out", file);
|
|
166
134
|
if (existsSync2(p))
|
|
167
135
|
return p;
|
|
168
136
|
} catch {
|
|
169
137
|
}
|
|
170
|
-
const local =
|
|
138
|
+
const local = join2(__dirname, "../../wasm", file);
|
|
171
139
|
return existsSync2(local) ? local : null;
|
|
172
140
|
}
|
|
173
141
|
async function getLanguage(lang) {
|
|
@@ -275,7 +243,7 @@ async function collectFiles(dir, projectRoot, acc) {
|
|
|
275
243
|
}
|
|
276
244
|
if (EXCLUDE_DIRS.has(entry.name))
|
|
277
245
|
continue;
|
|
278
|
-
const fullPath =
|
|
246
|
+
const fullPath = join2(dir, entry.name);
|
|
279
247
|
if (entry.isDirectory()) {
|
|
280
248
|
await collectFiles(fullPath, projectRoot, acc);
|
|
281
249
|
} else if (entry.isFile()) {
|
|
@@ -692,10 +660,10 @@ async function detectStackAndEntrypoints(projectRoot, files) {
|
|
|
692
660
|
const stack = /* @__PURE__ */ new Set();
|
|
693
661
|
const entrypoints = /* @__PURE__ */ new Set();
|
|
694
662
|
const rel = (abs) => relative(projectRoot, abs);
|
|
695
|
-
const pkgPath =
|
|
663
|
+
const pkgPath = join2(projectRoot, "package.json");
|
|
696
664
|
if (existsSync2(pkgPath)) {
|
|
697
665
|
try {
|
|
698
|
-
const pkg = JSON.parse(await
|
|
666
|
+
const pkg = JSON.parse(await readFile2(pkgPath, "utf8"));
|
|
699
667
|
stack.add("Node.js");
|
|
700
668
|
const deps = { ...pkg.dependencies || {}, ...pkg.devDependencies || {} };
|
|
701
669
|
for (const known of ["react", "next", "vue", "svelte", "express", "fastify", "typescript", "vite"]) {
|
|
@@ -705,7 +673,7 @@ async function detectStackAndEntrypoints(projectRoot, files) {
|
|
|
705
673
|
const addEntry = (p) => {
|
|
706
674
|
if (!p)
|
|
707
675
|
return;
|
|
708
|
-
const abs =
|
|
676
|
+
const abs = join2(projectRoot, p);
|
|
709
677
|
const r = relative(projectRoot, abs);
|
|
710
678
|
if (files.includes(abs))
|
|
711
679
|
entrypoints.add(r);
|
|
@@ -719,16 +687,16 @@ async function detectStackAndEntrypoints(projectRoot, files) {
|
|
|
719
687
|
} catch {
|
|
720
688
|
}
|
|
721
689
|
}
|
|
722
|
-
const pyproject =
|
|
723
|
-
const setupPy =
|
|
724
|
-
const requirements =
|
|
690
|
+
const pyproject = join2(projectRoot, "pyproject.toml");
|
|
691
|
+
const setupPy = join2(projectRoot, "setup.py");
|
|
692
|
+
const requirements = join2(projectRoot, "requirements.txt");
|
|
725
693
|
if (existsSync2(pyproject) || existsSync2(setupPy) || existsSync2(requirements)) {
|
|
726
694
|
stack.add("Python");
|
|
727
695
|
let reqText = "";
|
|
728
696
|
for (const f of [pyproject, requirements]) {
|
|
729
697
|
if (existsSync2(f)) {
|
|
730
698
|
try {
|
|
731
|
-
reqText += await
|
|
699
|
+
reqText += await readFile2(f, "utf8");
|
|
732
700
|
} catch {
|
|
733
701
|
}
|
|
734
702
|
}
|
|
@@ -738,11 +706,11 @@ async function detectStackAndEntrypoints(projectRoot, files) {
|
|
|
738
706
|
stack.add(known);
|
|
739
707
|
}
|
|
740
708
|
}
|
|
741
|
-
if (existsSync2(
|
|
709
|
+
if (existsSync2(join2(projectRoot, "go.mod")))
|
|
742
710
|
stack.add("Go");
|
|
743
|
-
if (existsSync2(
|
|
711
|
+
if (existsSync2(join2(projectRoot, "Cargo.toml")))
|
|
744
712
|
stack.add("Rust");
|
|
745
|
-
if (existsSync2(
|
|
713
|
+
if (existsSync2(join2(projectRoot, "pom.xml")) || existsSync2(join2(projectRoot, "build.gradle")))
|
|
746
714
|
stack.add("Java");
|
|
747
715
|
for (const abs of files) {
|
|
748
716
|
const r = rel(abs);
|
|
@@ -1168,7 +1136,7 @@ async function runInventory(projectRoot) {
|
|
|
1168
1136
|
continue;
|
|
1169
1137
|
let source;
|
|
1170
1138
|
try {
|
|
1171
|
-
source = await
|
|
1139
|
+
source = await readFile2(file, "utf8");
|
|
1172
1140
|
} catch {
|
|
1173
1141
|
continue;
|
|
1174
1142
|
}
|
|
@@ -1197,8 +1165,8 @@ async function runInventory(projectRoot) {
|
|
|
1197
1165
|
productDomain
|
|
1198
1166
|
});
|
|
1199
1167
|
}
|
|
1200
|
-
const dir =
|
|
1201
|
-
await
|
|
1168
|
+
const dir = join2(projectRoot, ".vibe-splainer");
|
|
1169
|
+
await mkdir(dir, { recursive: true });
|
|
1202
1170
|
const stage01 = {
|
|
1203
1171
|
files: work.map((w) => ({
|
|
1204
1172
|
absPath: w.abs,
|
|
@@ -1209,17 +1177,17 @@ async function runInventory(projectRoot) {
|
|
|
1209
1177
|
totalCount: work.length,
|
|
1210
1178
|
realSourceCount: work.filter((w) => !w.pathDemote).length
|
|
1211
1179
|
};
|
|
1212
|
-
await
|
|
1180
|
+
await writeFile2(join2(dir, "stage-01-inventory.json"), JSON.stringify(stage01, null, 2), "utf8");
|
|
1213
1181
|
const stage02 = Object.fromEntries(work.map((w) => [w.rel, w.frameworkRole]));
|
|
1214
|
-
await
|
|
1182
|
+
await writeFile2(join2(dir, "stage-02-framework-roles.json"), JSON.stringify(stage02, null, 2), "utf8");
|
|
1215
1183
|
const stage03 = Object.fromEntries(work.map((w) => [w.rel, w.productDomain]));
|
|
1216
|
-
await
|
|
1184
|
+
await writeFile2(join2(dir, "stage-03-domains.json"), JSON.stringify(stage03, null, 2), "utf8");
|
|
1217
1185
|
return { projectRoot, work, stack, entrypoints, fileSet, basenameIndex };
|
|
1218
1186
|
}
|
|
1219
1187
|
|
|
1220
1188
|
// ../brain/dist/pipeline/resolution.js
|
|
1221
|
-
import { join as
|
|
1222
|
-
import { readFile as
|
|
1189
|
+
import { join as join3, dirname as dirname2, relative as relative2, extname as extname2, sep as sep2 } from "path";
|
|
1190
|
+
import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir2 } from "fs/promises";
|
|
1223
1191
|
import { existsSync as existsSync3 } from "fs";
|
|
1224
1192
|
function parseJsonLenient(text) {
|
|
1225
1193
|
const stripped = text.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
|
|
@@ -1229,12 +1197,37 @@ function parseJsonLenient(text) {
|
|
|
1229
1197
|
return null;
|
|
1230
1198
|
}
|
|
1231
1199
|
}
|
|
1200
|
+
async function discoverAllTsConfigs(dir, projectRoot, maxDepth = 4) {
|
|
1201
|
+
const result = {};
|
|
1202
|
+
if (maxDepth < 0)
|
|
1203
|
+
return result;
|
|
1204
|
+
const { readdir: readdir2 } = await import("fs/promises");
|
|
1205
|
+
let entries = [];
|
|
1206
|
+
try {
|
|
1207
|
+
entries = await readdir2(dir, { withFileTypes: true });
|
|
1208
|
+
} catch {
|
|
1209
|
+
return result;
|
|
1210
|
+
}
|
|
1211
|
+
for (const entry of entries) {
|
|
1212
|
+
const fullPath = join3(dir, entry.name);
|
|
1213
|
+
if (entry.isDirectory()) {
|
|
1214
|
+
if (entry.name === "node_modules" || entry.name === ".git")
|
|
1215
|
+
continue;
|
|
1216
|
+
const sub = await discoverAllTsConfigs(fullPath, projectRoot, maxDepth - 1);
|
|
1217
|
+
Object.assign(result, sub);
|
|
1218
|
+
} else if (entry.name === "tsconfig.json") {
|
|
1219
|
+
const paths = await extractTsConfigPaths(fullPath, projectRoot);
|
|
1220
|
+
Object.assign(result, paths);
|
|
1221
|
+
}
|
|
1222
|
+
}
|
|
1223
|
+
return result;
|
|
1224
|
+
}
|
|
1232
1225
|
async function extractTsConfigPaths(tsconfigPath, projectRoot, depth = 0) {
|
|
1233
1226
|
if (depth > 3 || !existsSync3(tsconfigPath))
|
|
1234
1227
|
return {};
|
|
1235
1228
|
let raw;
|
|
1236
1229
|
try {
|
|
1237
|
-
raw = await
|
|
1230
|
+
raw = await readFile3(tsconfigPath, "utf8");
|
|
1238
1231
|
} catch {
|
|
1239
1232
|
return {};
|
|
1240
1233
|
}
|
|
@@ -1243,18 +1236,31 @@ async function extractTsConfigPaths(tsconfigPath, projectRoot, depth = 0) {
|
|
|
1243
1236
|
return {};
|
|
1244
1237
|
const result = {};
|
|
1245
1238
|
if (typeof parsed.extends === "string") {
|
|
1246
|
-
|
|
1239
|
+
let baseFile = parsed.extends;
|
|
1240
|
+
if (baseFile.startsWith(".")) {
|
|
1241
|
+
baseFile = join3(dirname2(tsconfigPath), baseFile);
|
|
1242
|
+
} else {
|
|
1243
|
+
baseFile = join3(projectRoot, "node_modules", baseFile);
|
|
1244
|
+
if (!baseFile.endsWith(".json"))
|
|
1245
|
+
baseFile += ".json";
|
|
1246
|
+
}
|
|
1247
1247
|
const base = await extractTsConfigPaths(baseFile, projectRoot, depth + 1);
|
|
1248
1248
|
Object.assign(result, base);
|
|
1249
1249
|
}
|
|
1250
1250
|
const opts = parsed.compilerOptions || {};
|
|
1251
|
-
const baseUrl = typeof opts.baseUrl === "string" ?
|
|
1251
|
+
const baseUrl = typeof opts.baseUrl === "string" ? join3(dirname2(tsconfigPath), opts.baseUrl) : dirname2(tsconfigPath);
|
|
1252
|
+
if (typeof opts.baseUrl === "string") {
|
|
1253
|
+
const relBase = relative2(projectRoot, baseUrl);
|
|
1254
|
+
if (relBase && relBase !== ".") {
|
|
1255
|
+
result[""] = relBase;
|
|
1256
|
+
}
|
|
1257
|
+
}
|
|
1252
1258
|
const paths = opts.paths || {};
|
|
1253
1259
|
for (const [alias, targets] of Object.entries(paths)) {
|
|
1254
1260
|
if (!Array.isArray(targets) || targets.length === 0)
|
|
1255
1261
|
continue;
|
|
1256
1262
|
const first = targets[0].replace(/\/\*$/, "");
|
|
1257
|
-
const resolved = relative2(projectRoot,
|
|
1263
|
+
const resolved = relative2(projectRoot, join3(baseUrl, first));
|
|
1258
1264
|
const key = alias.replace(/\/\*$/, "");
|
|
1259
1265
|
result[key] = resolved;
|
|
1260
1266
|
}
|
|
@@ -1262,12 +1268,12 @@ async function extractTsConfigPaths(tsconfigPath, projectRoot, depth = 0) {
|
|
|
1262
1268
|
}
|
|
1263
1269
|
async function discoverWorkspacePackages(projectRoot) {
|
|
1264
1270
|
const packages = {};
|
|
1265
|
-
const pkgPath =
|
|
1271
|
+
const pkgPath = join3(projectRoot, "package.json");
|
|
1266
1272
|
if (!existsSync3(pkgPath))
|
|
1267
1273
|
return packages;
|
|
1268
1274
|
let rootPkg;
|
|
1269
1275
|
try {
|
|
1270
|
-
rootPkg = JSON.parse(await
|
|
1276
|
+
rootPkg = JSON.parse(await readFile3(pkgPath, "utf8"));
|
|
1271
1277
|
} catch {
|
|
1272
1278
|
return packages;
|
|
1273
1279
|
}
|
|
@@ -1275,7 +1281,7 @@ async function discoverWorkspacePackages(projectRoot) {
|
|
|
1275
1281
|
const globs = Array.isArray(workspaces) ? workspaces : Array.isArray(workspaces?.packages) ? workspaces.packages : [];
|
|
1276
1282
|
for (const glob of globs) {
|
|
1277
1283
|
const prefix = glob.replace(/\/\*$/, "");
|
|
1278
|
-
const absPrefix =
|
|
1284
|
+
const absPrefix = join3(projectRoot, prefix);
|
|
1279
1285
|
if (!existsSync3(absPrefix))
|
|
1280
1286
|
continue;
|
|
1281
1287
|
const { readdir: readdir2 } = await import("fs/promises");
|
|
@@ -1287,13 +1293,13 @@ async function discoverWorkspacePackages(projectRoot) {
|
|
|
1287
1293
|
continue;
|
|
1288
1294
|
}
|
|
1289
1295
|
for (const entry of entries) {
|
|
1290
|
-
const wsPkgPath =
|
|
1296
|
+
const wsPkgPath = join3(absPrefix, entry, "package.json");
|
|
1291
1297
|
if (!existsSync3(wsPkgPath))
|
|
1292
1298
|
continue;
|
|
1293
1299
|
try {
|
|
1294
|
-
const wsPkg = JSON.parse(await
|
|
1300
|
+
const wsPkg = JSON.parse(await readFile3(wsPkgPath, "utf8"));
|
|
1295
1301
|
if (typeof wsPkg.name === "string") {
|
|
1296
|
-
packages[wsPkg.name] = relative2(projectRoot,
|
|
1302
|
+
packages[wsPkg.name] = relative2(projectRoot, join3(absPrefix, entry));
|
|
1297
1303
|
}
|
|
1298
1304
|
} catch {
|
|
1299
1305
|
continue;
|
|
@@ -1302,27 +1308,6 @@ async function discoverWorkspacePackages(projectRoot) {
|
|
|
1302
1308
|
}
|
|
1303
1309
|
return packages;
|
|
1304
1310
|
}
|
|
1305
|
-
async function discoverAppTsConfigPaths(projectRoot) {
|
|
1306
|
-
const result = {};
|
|
1307
|
-
const scanDirs = ["apps", "packages"];
|
|
1308
|
-
for (const scanDir of scanDirs) {
|
|
1309
|
-
const absDir = join5(projectRoot, scanDir);
|
|
1310
|
-
if (!existsSync3(absDir))
|
|
1311
|
-
continue;
|
|
1312
|
-
const { readdir: readdir2 } = await import("fs/promises");
|
|
1313
|
-
try {
|
|
1314
|
-
const entries = await readdir2(absDir, { withFileTypes: true });
|
|
1315
|
-
for (const entry of entries.filter((e) => e.isDirectory())) {
|
|
1316
|
-
const tsconfig = join5(absDir, entry.name, "tsconfig.json");
|
|
1317
|
-
const paths = await extractTsConfigPaths(tsconfig, projectRoot);
|
|
1318
|
-
Object.assign(result, paths);
|
|
1319
|
-
}
|
|
1320
|
-
} catch {
|
|
1321
|
-
continue;
|
|
1322
|
-
}
|
|
1323
|
-
}
|
|
1324
|
-
return result;
|
|
1325
|
-
}
|
|
1326
1311
|
var CONVENTIONAL_ALIASES = [
|
|
1327
1312
|
{ prefix: "~/", replacement: "" },
|
|
1328
1313
|
{ prefix: "@components/", replacement: "components/" },
|
|
@@ -1337,10 +1322,9 @@ var CONVENTIONAL_ALIASES = [
|
|
|
1337
1322
|
{ prefix: "@calcom/emails/", replacement: "../packages/emails/" }
|
|
1338
1323
|
];
|
|
1339
1324
|
async function buildAliasMap(projectRoot) {
|
|
1340
|
-
const
|
|
1325
|
+
const allPaths = await discoverAllTsConfigs(projectRoot, projectRoot);
|
|
1341
1326
|
const workspacePackages = await discoverWorkspacePackages(projectRoot);
|
|
1342
|
-
const
|
|
1343
|
-
const resolvedAliases = { ...appPaths, ...rootPaths };
|
|
1327
|
+
const resolvedAliases = { ...allPaths };
|
|
1344
1328
|
for (const [pkgName, pkgDir] of Object.entries(workspacePackages)) {
|
|
1345
1329
|
if (!(pkgName in resolvedAliases)) {
|
|
1346
1330
|
resolvedAliases[pkgName] = pkgDir;
|
|
@@ -1355,7 +1339,7 @@ function tryJsCandidates(base, projectRoot, fileSet) {
|
|
|
1355
1339
|
for (const ext of JS_EXTS)
|
|
1356
1340
|
candidates.push(base + ext);
|
|
1357
1341
|
for (const ext of JS_EXTS)
|
|
1358
|
-
candidates.push(
|
|
1342
|
+
candidates.push(join3(base, "index" + ext));
|
|
1359
1343
|
for (const c of candidates) {
|
|
1360
1344
|
const rel = relative2(projectRoot, c);
|
|
1361
1345
|
if (fileSet.has(rel))
|
|
@@ -1371,11 +1355,11 @@ function resolvePython(spec, fromAbs, projectRoot, fileSet) {
|
|
|
1371
1355
|
for (let i = 1; i < dots; i++)
|
|
1372
1356
|
dir = dirname2(dir);
|
|
1373
1357
|
const rest = spec.slice(dots).replace(/\./g, sep2);
|
|
1374
|
-
modulePath = rest ?
|
|
1358
|
+
modulePath = rest ? join3(dir, rest) : dir;
|
|
1375
1359
|
} else {
|
|
1376
|
-
modulePath =
|
|
1360
|
+
modulePath = join3(projectRoot, spec.replace(/\./g, sep2));
|
|
1377
1361
|
}
|
|
1378
|
-
for (const c of [modulePath + ".py",
|
|
1362
|
+
for (const c of [modulePath + ".py", join3(modulePath, "__init__.py")]) {
|
|
1379
1363
|
if (fileSet.has(relative2(projectRoot, c)))
|
|
1380
1364
|
return relative2(projectRoot, c);
|
|
1381
1365
|
}
|
|
@@ -1403,31 +1387,42 @@ function resolveImportWithAliasMap(spec, fromAbs, lang, projectRoot, fileSet, ba
|
|
|
1403
1387
|
}
|
|
1404
1388
|
if (lang === "typescript" || lang === "tsx" || lang === "javascript") {
|
|
1405
1389
|
if (spec.startsWith(".")) {
|
|
1406
|
-
const base =
|
|
1390
|
+
const base = join3(dirname2(fromAbs), spec);
|
|
1407
1391
|
return { resolved: tryJsCandidates(base, projectRoot, fileSet), isAlias: false };
|
|
1408
1392
|
}
|
|
1409
1393
|
for (const [prefix, replacement] of Object.entries(aliasMap.resolvedAliases)) {
|
|
1394
|
+
if (prefix === "")
|
|
1395
|
+
continue;
|
|
1410
1396
|
if (spec === prefix || spec.startsWith(prefix + "/")) {
|
|
1411
1397
|
const rest = spec.slice(prefix.length).replace(/^\//, "");
|
|
1412
|
-
const base =
|
|
1398
|
+
const base = join3(projectRoot, replacement, rest);
|
|
1413
1399
|
const resolved = tryJsCandidates(base, projectRoot, fileSet);
|
|
1414
|
-
|
|
1400
|
+
if (resolved)
|
|
1401
|
+
return { resolved, isAlias: true };
|
|
1415
1402
|
}
|
|
1416
1403
|
}
|
|
1404
|
+
if (aliasMap.resolvedAliases[""] !== void 0) {
|
|
1405
|
+
const base = join3(projectRoot, aliasMap.resolvedAliases[""], spec);
|
|
1406
|
+
const resolved = tryJsCandidates(base, projectRoot, fileSet);
|
|
1407
|
+
if (resolved)
|
|
1408
|
+
return { resolved, isAlias: true };
|
|
1409
|
+
}
|
|
1417
1410
|
for (const [pkgName, pkgDir] of Object.entries(aliasMap.workspacePackages)) {
|
|
1418
1411
|
if (spec === pkgName || spec.startsWith(pkgName + "/")) {
|
|
1419
1412
|
const rest = spec.slice(pkgName.length).replace(/^\//, "");
|
|
1420
|
-
const base =
|
|
1413
|
+
const base = join3(projectRoot, pkgDir, rest);
|
|
1421
1414
|
const resolved = tryJsCandidates(base, projectRoot, fileSet);
|
|
1422
|
-
|
|
1415
|
+
if (resolved)
|
|
1416
|
+
return { resolved, isAlias: true };
|
|
1423
1417
|
}
|
|
1424
1418
|
}
|
|
1425
1419
|
for (const { prefix, replacement } of CONVENTIONAL_ALIASES) {
|
|
1426
1420
|
if (spec.startsWith(prefix)) {
|
|
1427
1421
|
const rest = replacement + spec.slice(prefix.length);
|
|
1428
|
-
const base =
|
|
1422
|
+
const base = join3(projectRoot, rest);
|
|
1429
1423
|
const resolved = tryJsCandidates(base, projectRoot, fileSet);
|
|
1430
|
-
|
|
1424
|
+
if (resolved)
|
|
1425
|
+
return { resolved, isAlias: true };
|
|
1431
1426
|
}
|
|
1432
1427
|
}
|
|
1433
1428
|
return { resolved: null, isAlias: false };
|
|
@@ -1477,8 +1472,8 @@ async function runResolution(projectRoot, inv) {
|
|
|
1477
1472
|
fanOut.set(w.rel, distinctModules.size);
|
|
1478
1473
|
}
|
|
1479
1474
|
const unresolvedImports = [...unresolvedSet];
|
|
1480
|
-
const dir =
|
|
1481
|
-
await
|
|
1475
|
+
const dir = join3(projectRoot, ".vibe-splainer");
|
|
1476
|
+
await mkdir2(dir, { recursive: true });
|
|
1482
1477
|
const stage04 = {
|
|
1483
1478
|
resolvedAliases: aliasMap.resolvedAliases,
|
|
1484
1479
|
workspacePackages: aliasMap.workspacePackages,
|
|
@@ -1486,7 +1481,7 @@ async function runResolution(projectRoot, inv) {
|
|
|
1486
1481
|
resolutionFailuresByFile,
|
|
1487
1482
|
resolutionFailureReasons
|
|
1488
1483
|
};
|
|
1489
|
-
await
|
|
1484
|
+
await writeFile3(join3(dir, "stage-04-aliases.json"), JSON.stringify(stage04, null, 2), "utf8");
|
|
1490
1485
|
return {
|
|
1491
1486
|
aliasMap,
|
|
1492
1487
|
importedBy,
|
|
@@ -1501,8 +1496,8 @@ async function runResolution(projectRoot, inv) {
|
|
|
1501
1496
|
}
|
|
1502
1497
|
|
|
1503
1498
|
// ../brain/dist/pipeline/classification.js
|
|
1504
|
-
import { join as
|
|
1505
|
-
import { writeFile as
|
|
1499
|
+
import { join as join4, basename as basename2, extname as extname3, sep as sep3 } from "path";
|
|
1500
|
+
import { writeFile as writeFile4, mkdir as mkdir3 } from "fs/promises";
|
|
1506
1501
|
function inferSideEffectProfile(source, importSpecs, productDomain, frameworkRole) {
|
|
1507
1502
|
const effects = /* @__PURE__ */ new Set();
|
|
1508
1503
|
if (/router\.(push|replace|back)\(|redirect\(|notFound\(|permanentRedirect\(/.test(source)) {
|
|
@@ -2111,6 +2106,8 @@ async function runClassification(projectRoot, inv, res) {
|
|
|
2111
2106
|
const entrypointTraceStatus = deriveEntrypointTraceStatus(w.productDomain, runtimeEntrypoints, importsUnresolvedArr);
|
|
2112
2107
|
const smellMaxSeverity = w.ast.smells.length > 0 ? Math.max(...w.ast.smells.map((s) => s.severity)) : 0;
|
|
2113
2108
|
const loadBearingScore = computeLoadBearingScore(gravity, heat, fanIn, effects, w.productDomain, smellMaxSeverity, runtimeEntrypoints);
|
|
2109
|
+
const isLoadBearing = fanIn >= 10;
|
|
2110
|
+
const isOperationallyCritical = loadBearingScore >= 5;
|
|
2114
2111
|
classified.push({
|
|
2115
2112
|
rel: w.rel,
|
|
2116
2113
|
abs: w.abs,
|
|
@@ -2135,25 +2132,28 @@ async function runClassification(projectRoot, inv, res) {
|
|
|
2135
2132
|
entrypointTraceStatus,
|
|
2136
2133
|
blockedImports: importsUnresolvedArr,
|
|
2137
2134
|
loadBearingScore,
|
|
2135
|
+
isOperationallyCritical,
|
|
2136
|
+
isLoadBearing,
|
|
2138
2137
|
hotSpans: w.ast.hotSpans,
|
|
2139
2138
|
source: w.source
|
|
2140
2139
|
});
|
|
2141
2140
|
}
|
|
2142
|
-
const dir =
|
|
2143
|
-
await
|
|
2141
|
+
const dir = join4(projectRoot, ".vibe-splainer");
|
|
2142
|
+
await mkdir3(dir, { recursive: true });
|
|
2144
2143
|
const stage05 = Object.fromEntries(classified.map((f) => [f.rel, f.sideEffectProfile]));
|
|
2145
|
-
await
|
|
2144
|
+
await writeFile4(join4(dir, "stage-05-side-effects.json"), JSON.stringify(stage05, null, 2), "utf8");
|
|
2146
2145
|
const stage06 = Object.fromEntries(classified.map((f) => [f.rel, f.writeIntents]));
|
|
2147
|
-
await
|
|
2146
|
+
await writeFile4(join4(dir, "stage-06-write-intents.json"), JSON.stringify(stage06, null, 2), "utf8");
|
|
2148
2147
|
const stage07 = Object.fromEntries(classified.map((f) => [f.rel, f.riskTypes]));
|
|
2149
|
-
await
|
|
2148
|
+
await writeFile4(join4(dir, "stage-07-risk-types.json"), JSON.stringify(stage07, null, 2), "utf8");
|
|
2150
2149
|
const stage08 = Object.fromEntries(classified.map((f) => [f.rel, {
|
|
2151
|
-
isLoadBearing: f.
|
|
2150
|
+
isLoadBearing: f.isLoadBearing,
|
|
2151
|
+
isOperationallyCritical: f.isOperationallyCritical,
|
|
2152
2152
|
loadBearingScore: f.loadBearingScore,
|
|
2153
2153
|
runtimeEntrypoints: f.runtimeEntrypoints.length,
|
|
2154
2154
|
entrypointTraceStatus: f.entrypointTraceStatus
|
|
2155
2155
|
}]));
|
|
2156
|
-
await
|
|
2156
|
+
await writeFile4(join4(dir, "stage-08-load-bearing.json"), JSON.stringify(stage08, null, 2), "utf8");
|
|
2157
2157
|
const realClassified = classified.filter((f) => f.isRealSource).sort((a, b) => b.gravity - a.gravity);
|
|
2158
2158
|
const wildCandidates = realClassified.filter((f) => f.heat >= 60 || f.smells.some((s) => s.severity >= 4));
|
|
2159
2159
|
const pillars = buildPillars(classified, communities);
|
|
@@ -2171,8 +2171,8 @@ async function runClassification(projectRoot, inv, res) {
|
|
|
2171
2171
|
}
|
|
2172
2172
|
|
|
2173
2173
|
// ../brain/dist/pipeline/binding.js
|
|
2174
|
-
import { join as
|
|
2175
|
-
import { writeFile as
|
|
2174
|
+
import { join as join5 } from "path";
|
|
2175
|
+
import { writeFile as writeFile5, readFile as readFile4 } from "fs/promises";
|
|
2176
2176
|
var FUNCTION_TYPES2 = /* @__PURE__ */ new Set([
|
|
2177
2177
|
"function_declaration",
|
|
2178
2178
|
"function_expression",
|
|
@@ -2550,7 +2550,7 @@ async function runActionBinding(projectRoot, inv, res) {
|
|
|
2550
2550
|
}
|
|
2551
2551
|
}
|
|
2552
2552
|
}
|
|
2553
|
-
await
|
|
2553
|
+
await writeFile5(join5(projectRoot, ".vibe-splainer", "action_bindings.json"), JSON.stringify(artifact, null, 2), "utf8");
|
|
2554
2554
|
const summary = {
|
|
2555
2555
|
filesProcessed,
|
|
2556
2556
|
functionsExtracted,
|
|
@@ -2560,14 +2560,14 @@ async function runActionBinding(projectRoot, inv, res) {
|
|
|
2560
2560
|
entrypointsFound,
|
|
2561
2561
|
namedImportsExtracted
|
|
2562
2562
|
};
|
|
2563
|
-
await
|
|
2563
|
+
await writeFile5(join5(projectRoot, ".vibe-splainer", "stage-09-action-bindings-summary.json"), JSON.stringify(summary, null, 2), "utf8");
|
|
2564
2564
|
return { artifact };
|
|
2565
2565
|
}
|
|
2566
2566
|
async function traverseCallChain(projectRoot, args) {
|
|
2567
|
-
const artifactPath =
|
|
2567
|
+
const artifactPath = join5(projectRoot, ".vibe-splainer", "action_bindings.json");
|
|
2568
2568
|
let artifact;
|
|
2569
2569
|
try {
|
|
2570
|
-
const raw = await
|
|
2570
|
+
const raw = await readFile4(artifactPath, "utf8");
|
|
2571
2571
|
artifact = JSON.parse(raw);
|
|
2572
2572
|
} catch {
|
|
2573
2573
|
throw new Error("action_bindings.json not found. Run scan_project first.");
|
|
@@ -2690,9 +2690,8 @@ async function traverseCallChain(projectRoot, args) {
|
|
|
2690
2690
|
}
|
|
2691
2691
|
|
|
2692
2692
|
// ../brain/dist/pipeline/scoring.js
|
|
2693
|
-
import { join as
|
|
2694
|
-
import {
|
|
2695
|
-
import { createHash } from "crypto";
|
|
2693
|
+
import { join as join6 } from "path";
|
|
2694
|
+
import { mkdir as mkdir4, readFile as readFile5 } from "fs/promises";
|
|
2696
2695
|
function computeSeverity(sideEffectProfile, productDomain, gravity, heat, maxNesting, hasLongFunctions, swallowedCatches, runtimeEntrypoints) {
|
|
2697
2696
|
let score = 0;
|
|
2698
2697
|
if (sideEffectProfile.includes("database_write"))
|
|
@@ -2759,149 +2758,6 @@ function applyCorrections(file) {
|
|
|
2759
2758
|
file.canonicalLoadBearing = true;
|
|
2760
2759
|
}
|
|
2761
2760
|
}
|
|
2762
|
-
function inferObservableOutputs(frameworkRole, productDomain, sideEffectProfile) {
|
|
2763
|
-
const outputs = [];
|
|
2764
|
-
const ENTRYPOINT_ROLES2 = /* @__PURE__ */ new Set(["app_route_page", "app_route_handler", "pages_route", "pages_api_route", "trpc_api_route"]);
|
|
2765
|
-
if (sideEffectProfile.includes("redirect"))
|
|
2766
|
-
outputs.push("redirect_url");
|
|
2767
|
-
if (ENTRYPOINT_ROLES2.has(frameworkRole))
|
|
2768
|
-
outputs.push("http_status");
|
|
2769
|
-
if (frameworkRole === "app_route_handler" || frameworkRole === "pages_api_route") {
|
|
2770
|
-
outputs.push("json_response_shape");
|
|
2771
|
-
}
|
|
2772
|
-
if (productDomain === "booking_creation" || productDomain === "booking_management")
|
|
2773
|
-
outputs.push("booking_uid");
|
|
2774
|
-
if (productDomain === "payments" || productDomain === "payments_webhooks")
|
|
2775
|
-
outputs.push("payment_status");
|
|
2776
|
-
if (productDomain === "auth_oauth")
|
|
2777
|
-
outputs.push("auth_token");
|
|
2778
|
-
if (sideEffectProfile.includes("webhook_delivery") || sideEffectProfile.includes("webhook_ingress")) {
|
|
2779
|
-
outputs.push("webhook_payload");
|
|
2780
|
-
}
|
|
2781
|
-
if (sideEffectProfile.includes("calendar_mutation"))
|
|
2782
|
-
outputs.push("calendar_event_id");
|
|
2783
|
-
if (sideEffectProfile.includes("email_send"))
|
|
2784
|
-
outputs.push("email_payload");
|
|
2785
|
-
if (sideEffectProfile.includes("analytics_event"))
|
|
2786
|
-
outputs.push("sdk_event_name");
|
|
2787
|
-
if (frameworkRole === "hook" || frameworkRole === "store")
|
|
2788
|
-
outputs.push("ui_state_transition");
|
|
2789
|
-
if (productDomain === "data_table" && frameworkRole === "provider") {
|
|
2790
|
-
outputs.push("ui_state_transition", "filter_state", "selected_segment");
|
|
2791
|
-
}
|
|
2792
|
-
return [...new Set(outputs)];
|
|
2793
|
-
}
|
|
2794
|
-
function inferPatchRisk(productDomain, riskTypes, sideEffectProfile, importedByCount, loadBearingScore) {
|
|
2795
|
-
if (loadBearingScore >= 12 || productDomain === "booking_creation" && riskTypes.includes("mutation_orchestration")) {
|
|
2796
|
-
return {
|
|
2797
|
-
level: "critical",
|
|
2798
|
-
reason: `${productDomain} domain with ${riskTypes.join(", ")} \u2014 any patch risks breaking live booking, payment, or auth flows.`
|
|
2799
|
-
};
|
|
2800
|
-
}
|
|
2801
|
-
if (loadBearingScore >= 8 || sideEffectProfile.includes("payment_mutation") || sideEffectProfile.includes("auth_token_mutation")) {
|
|
2802
|
-
const external = sideEffectProfile.filter((s) => ["payment_mutation", "auth_token_mutation", "database_write", "webhook_delivery"].includes(s));
|
|
2803
|
-
return {
|
|
2804
|
-
level: "high",
|
|
2805
|
-
reason: `${productDomain} writes to external state (${external.join(", ") || "database"}). Changes require integration testing.`
|
|
2806
|
-
};
|
|
2807
|
-
}
|
|
2808
|
-
if (riskTypes.includes("registry_bottleneck")) {
|
|
2809
|
-
return {
|
|
2810
|
-
level: "high",
|
|
2811
|
-
reason: "registry_bottleneck: central dispatch point \u2014 blast radius not measurable by fan-in alone."
|
|
2812
|
-
};
|
|
2813
|
-
}
|
|
2814
|
-
if (loadBearingScore >= 5 || importedByCount >= 5) {
|
|
2815
|
-
return { level: "medium", reason: `Imported by ${importedByCount} files. Interface changes will cascade.` };
|
|
2816
|
-
}
|
|
2817
|
-
if (productDomain === "data_table" && riskTypes.includes("state_machine")) {
|
|
2818
|
-
return {
|
|
2819
|
-
level: "medium",
|
|
2820
|
-
reason: "data_table state machine: controls user-visible workflow state (filters, segments, pagination) \u2014 regression risk not captured by mutation scoring."
|
|
2821
|
-
};
|
|
2822
|
-
}
|
|
2823
|
-
return { level: "low", reason: "Locally contained \u2014 limited blast radius." };
|
|
2824
|
-
}
|
|
2825
|
-
function inferSafePatchStrategy(riskTypes, sideEffectProfile) {
|
|
2826
|
-
if (riskTypes.includes("mutation_orchestration")) {
|
|
2827
|
-
return "Do not rewrite inline. Extract pure decision logic into a tested reducer or state machine first. Preserve all side-effect call sites (redirect URLs, SDK event names, response shapes) as invariants.";
|
|
2828
|
-
}
|
|
2829
|
-
if (riskTypes.includes("registry_bottleneck")) {
|
|
2830
|
-
return "Add new entries without removing existing keys. Treat the registry map as append-only until all consumers are verified.";
|
|
2831
|
-
}
|
|
2832
|
-
if (riskTypes.includes("registry_consumer")) {
|
|
2833
|
-
return "Verify the registry contract (Components.tsx) before patching. Changes to field types must be reflected in both the registry and all rendering paths.";
|
|
2834
|
-
}
|
|
2835
|
-
if (riskTypes.includes("route_handler_write_path")) {
|
|
2836
|
-
return "Add integration tests covering success and failure paths before modifying. Verify HTTP status codes and response shapes are preserved.";
|
|
2837
|
-
}
|
|
2838
|
-
if (riskTypes.includes("god_component") || riskTypes.includes("god_hook")) {
|
|
2839
|
-
return "Extract sub-concerns into separate modules first. Only refactor the extraction points after tests confirm equivalence.";
|
|
2840
|
-
}
|
|
2841
|
-
if (sideEffectProfile.includes("database_write")) {
|
|
2842
|
-
return "Wrap changes in a transaction or use a feature flag. Run against a staging database before production.";
|
|
2843
|
-
}
|
|
2844
|
-
return "Review importedBy before patching. Run affected integration tests.";
|
|
2845
|
-
}
|
|
2846
|
-
function inferDoNotTouch(sideEffectProfile, productDomain) {
|
|
2847
|
-
const items = [];
|
|
2848
|
-
if (sideEffectProfile.includes("payment_mutation"))
|
|
2849
|
-
items.push("payment flow branch");
|
|
2850
|
-
if (sideEffectProfile.includes("auth_token_mutation"))
|
|
2851
|
-
items.push("token issuance / refresh branch");
|
|
2852
|
-
if (sideEffectProfile.includes("webhook_delivery") || sideEffectProfile.includes("webhook_ingress")) {
|
|
2853
|
-
items.push("webhook payload shape");
|
|
2854
|
-
}
|
|
2855
|
-
if (sideEffectProfile.includes("redirect"))
|
|
2856
|
-
items.push("redirect URL strings");
|
|
2857
|
-
if (sideEffectProfile.includes("analytics_event"))
|
|
2858
|
-
items.push("SDK event names");
|
|
2859
|
-
if (sideEffectProfile.includes("booking_mutation")) {
|
|
2860
|
-
items.push("booking success response shape", "recurring booking branch");
|
|
2861
|
-
}
|
|
2862
|
-
if (productDomain === "auth_oauth")
|
|
2863
|
-
items.push("OAuth callback URLs", "token scopes");
|
|
2864
|
-
return items;
|
|
2865
|
-
}
|
|
2866
|
-
function inferTestProbes(writeIntents, observableOutputs) {
|
|
2867
|
-
const probes = [];
|
|
2868
|
-
if (writeIntents.includes("create_booking")) {
|
|
2869
|
-
probes.push({
|
|
2870
|
-
name: "standard booking success",
|
|
2871
|
-
scenario: "create a standard booking and assert success redirect and booking uid",
|
|
2872
|
-
expectedObservable: ["booking_uid", "redirect_url", "sdk_event_name"].filter((o) => observableOutputs.includes(o))
|
|
2873
|
-
});
|
|
2874
|
-
}
|
|
2875
|
-
if (writeIntents.includes("reschedule_booking")) {
|
|
2876
|
-
probes.push({
|
|
2877
|
-
name: "reschedule booking",
|
|
2878
|
-
scenario: "reschedule an existing booking and assert reschedule event path",
|
|
2879
|
-
expectedObservable: ["booking_uid", "redirect_url"].filter((o) => observableOutputs.includes(o))
|
|
2880
|
-
});
|
|
2881
|
-
}
|
|
2882
|
-
if (writeIntents.includes("create_recurring_booking")) {
|
|
2883
|
-
probes.push({
|
|
2884
|
-
name: "recurring booking",
|
|
2885
|
-
scenario: "create recurring booking and assert recurring success behavior",
|
|
2886
|
-
expectedObservable: ["booking_uid", "redirect_url"].filter((o) => observableOutputs.includes(o))
|
|
2887
|
-
});
|
|
2888
|
-
}
|
|
2889
|
-
if (writeIntents.includes("handle_payment_webhook")) {
|
|
2890
|
-
probes.push({
|
|
2891
|
-
name: "payment webhook ingestion",
|
|
2892
|
-
scenario: "send a valid payment webhook and assert booking/payment state updated",
|
|
2893
|
-
expectedObservable: ["payment_status", "booking_uid", "http_status"].filter((o) => observableOutputs.includes(o))
|
|
2894
|
-
});
|
|
2895
|
-
}
|
|
2896
|
-
if (writeIntents.includes("issue_auth_token")) {
|
|
2897
|
-
probes.push({
|
|
2898
|
-
name: "token issuance",
|
|
2899
|
-
scenario: "complete OAuth flow and assert access token issued with correct scopes",
|
|
2900
|
-
expectedObservable: ["auth_token", "http_status"].filter((o) => observableOutputs.includes(o))
|
|
2901
|
-
});
|
|
2902
|
-
}
|
|
2903
|
-
return probes;
|
|
2904
|
-
}
|
|
2905
2761
|
function deriveConfidence(fanIn, gravity) {
|
|
2906
2762
|
if (fanIn >= 10 && gravity >= 40)
|
|
2907
2763
|
return "high";
|
|
@@ -2910,12 +2766,12 @@ function deriveConfidence(fanIn, gravity) {
|
|
|
2910
2766
|
return "low";
|
|
2911
2767
|
}
|
|
2912
2768
|
async function runScoring(projectRoot, cr, binding) {
|
|
2913
|
-
const dir =
|
|
2914
|
-
await
|
|
2769
|
+
const dir = join6(projectRoot, ".vibe-splainer");
|
|
2770
|
+
await mkdir4(dir, { recursive: true });
|
|
2915
2771
|
let bindingArtifact = binding?.artifact;
|
|
2916
2772
|
if (!bindingArtifact) {
|
|
2917
2773
|
try {
|
|
2918
|
-
const raw = await
|
|
2774
|
+
const raw = await readFile5(join6(projectRoot, ".vibe-splainer", "action_bindings.json"), "utf8");
|
|
2919
2775
|
bindingArtifact = JSON.parse(raw);
|
|
2920
2776
|
} catch {
|
|
2921
2777
|
}
|
|
@@ -2924,7 +2780,7 @@ async function runScoring(projectRoot, cr, binding) {
|
|
|
2924
2780
|
const severityBreakdowns = {};
|
|
2925
2781
|
for (const f of cr.classified) {
|
|
2926
2782
|
const severity = computeSeverity(f.sideEffectProfile, f.productDomain, f.gravity, f.heat, f.heatSignals.maxNesting, f.heatSignals.longFunctions > 0, f.heatSignals.swallowedCatches, f.runtimeEntrypoints);
|
|
2927
|
-
const
|
|
2783
|
+
const confidence = deriveConfidence(f.gravitySignals.fanIn, f.gravity);
|
|
2928
2784
|
const pf = {
|
|
2929
2785
|
relativePath: f.rel,
|
|
2930
2786
|
language: f.lang,
|
|
@@ -2946,161 +2802,25 @@ async function runScoring(projectRoot, cr, binding) {
|
|
|
2946
2802
|
riskTypes: f.riskTypes,
|
|
2947
2803
|
writeIntents: f.writeIntents,
|
|
2948
2804
|
canonicalSeverity: severity,
|
|
2949
|
-
canonicalLoadBearing: isLoadBearing
|
|
2805
|
+
canonicalLoadBearing: f.isLoadBearing,
|
|
2806
|
+
// STRICT: fanIn >= 10
|
|
2807
|
+
isOperationallyCritical: f.isOperationallyCritical,
|
|
2808
|
+
confidence
|
|
2950
2809
|
};
|
|
2951
2810
|
applyCorrections(pf);
|
|
2952
2811
|
persisted[f.rel] = pf;
|
|
2953
2812
|
severityBreakdowns[f.rel] = `severity=${pf.canonicalSeverity} loadBearing=${pf.canonicalLoadBearing} effects=${pf.sideEffectProfile.join(",")} domain=${pf.productDomain}`;
|
|
2954
2813
|
}
|
|
2955
|
-
const stage09 = Object.fromEntries(Object.entries(persisted).filter(([, pf]) => pf.isRealSource).map(([rel, pf]) => [rel, { canonicalSeverity: pf.canonicalSeverity, canonicalLoadBearing: pf.canonicalLoadBearing, scoreBreakdown: severityBreakdowns[rel] }]));
|
|
2956
|
-
await writeFile8(join8(dir, "stage-09-severity.json"), JSON.stringify(stage09, null, 2), "utf8");
|
|
2957
2814
|
const store = { files: persisted };
|
|
2958
|
-
const
|
|
2959
|
-
|
|
2960
|
-
|
|
2961
|
-
|
|
2962
|
-
|
|
2963
|
-
|
|
2964
|
-
|
|
2965
|
-
|
|
2966
|
-
|
|
2967
|
-
const loadBearingScore = computeLoadBearingScore(pf.gravity, pf.heat, pf.importedBy.length, pf.sideEffectProfile, pf.productDomain, smellMaxSeverity, runtimeEntrypoints);
|
|
2968
|
-
const observableOutputs = inferObservableOutputs(pf.frameworkRole, pf.productDomain, pf.sideEffectProfile);
|
|
2969
|
-
const patchRisk = inferPatchRisk(pf.productDomain, pf.riskTypes, pf.sideEffectProfile, pf.importedBy.length, loadBearingScore);
|
|
2970
|
-
const confidence = deriveConfidence(pf.gravitySignals.fanIn, pf.gravity);
|
|
2971
|
-
const fileHashInput = pf.hotSpans.map((h) => h.snippet).join("");
|
|
2972
|
-
const fileHash = createHash("sha256").update(fileHashInput || pf.relativePath).digest("hex").slice(0, 12);
|
|
2973
|
-
const rawEvidence = pf.hotSpans.map((span) => ({
|
|
2974
|
-
file: pf.relativePath,
|
|
2975
|
-
startLine: span.startLine,
|
|
2976
|
-
endLine: span.endLine,
|
|
2977
|
-
rawSourceExcerpt: span.rawExcerpt,
|
|
2978
|
-
evidenceHash: createHash("sha256").update(span.rawExcerpt).digest("hex").slice(0, 12)
|
|
2979
|
-
}));
|
|
2980
|
-
const displayEvidence = pf.hotSpans.map((span) => ({
|
|
2981
|
-
file: pf.relativePath,
|
|
2982
|
-
startLine: span.startLine,
|
|
2983
|
-
endLine: span.endLine,
|
|
2984
|
-
excerpt: span.snippet,
|
|
2985
|
-
isTruncated: span.rawExcerpt.length > 2e3
|
|
2986
|
-
}));
|
|
2987
|
-
let criticalFunctions = void 0;
|
|
2988
|
-
if (bindingArtifact) {
|
|
2989
|
-
const fileBinding = bindingArtifact.files[pf.relativePath];
|
|
2990
|
-
if (fileBinding) {
|
|
2991
|
-
const scoredFunctions = fileBinding.functions.map((fn) => {
|
|
2992
|
-
let fnScore = 0;
|
|
2993
|
-
const reasons = [];
|
|
2994
|
-
if (fn.semanticActions.length > 0) {
|
|
2995
|
-
fnScore += 3;
|
|
2996
|
-
reasons.push("Contains semantic actions");
|
|
2997
|
-
}
|
|
2998
|
-
if (fn.isEntrypoint) {
|
|
2999
|
-
fnScore += 2;
|
|
3000
|
-
reasons.push("Is a framework entrypoint");
|
|
3001
|
-
}
|
|
3002
|
-
const resolvedOutbound = fn.calls.filter((c) => c.resolvedTargetFunctionId).length;
|
|
3003
|
-
if (resolvedOutbound > 0) {
|
|
3004
|
-
const callPts = Math.min(3, resolvedOutbound);
|
|
3005
|
-
fnScore += callPts;
|
|
3006
|
-
reasons.push(`Has ${resolvedOutbound} resolved outbound calls`);
|
|
3007
|
-
} else if (fn.calls.length > 0) {
|
|
3008
|
-
fnScore += 1;
|
|
3009
|
-
reasons.push(`Has ${fn.calls.length} outbound calls`);
|
|
3010
|
-
}
|
|
3011
|
-
const writesModel = fn.semanticActions.some((a) => a.actionKind === "database_write" && a.targetModel);
|
|
3012
|
-
if (writesModel) {
|
|
3013
|
-
fnScore += 2;
|
|
3014
|
-
reasons.push("Writes to a database model");
|
|
3015
|
-
}
|
|
3016
|
-
const authOrValid = fn.semanticActions.some((a) => a.actionKind === "auth_check" || a.actionKind === "validation");
|
|
3017
|
-
if (authOrValid) {
|
|
3018
|
-
fnScore += 1;
|
|
3019
|
-
reasons.push("Performs auth/validation");
|
|
3020
|
-
}
|
|
3021
|
-
const hasEvidenceOverlap = rawEvidence.some((e) => fn.startLine <= e.endLine && fn.endLine >= e.startLine);
|
|
3022
|
-
if (hasEvidenceOverlap) {
|
|
3023
|
-
fnScore += 2;
|
|
3024
|
-
reasons.push("Overlaps with raw evidence span");
|
|
3025
|
-
}
|
|
3026
|
-
return { fn, fnScore, reasons };
|
|
3027
|
-
});
|
|
3028
|
-
scoredFunctions.sort((a, b) => b.fnScore - a.fnScore);
|
|
3029
|
-
const topFns = scoredFunctions.filter((x) => x.reasons.length > 0).slice(0, 5);
|
|
3030
|
-
if (topFns.length > 0) {
|
|
3031
|
-
criticalFunctions = topFns.map(({ fn, reasons }) => {
|
|
3032
|
-
const evidence = fn.semanticActions.slice(0, 5).sort((a, b) => a.sourceLine - b.sourceLine).map((a) => ({
|
|
3033
|
-
sourceLine: a.sourceLine,
|
|
3034
|
-
text: a.evidenceText,
|
|
3035
|
-
actionKind: a.actionKind,
|
|
3036
|
-
targetModel: a.targetModel,
|
|
3037
|
-
targetOperation: a.targetOperation,
|
|
3038
|
-
confidence: a.confidence
|
|
3039
|
-
}));
|
|
3040
|
-
const confidences = fn.semanticActions.map((a) => a.confidence);
|
|
3041
|
-
let confidence2 = "high";
|
|
3042
|
-
if (confidences.includes("low"))
|
|
3043
|
-
confidence2 = "low";
|
|
3044
|
-
else if (confidences.includes("medium"))
|
|
3045
|
-
confidence2 = "medium";
|
|
3046
|
-
return {
|
|
3047
|
-
functionId: fn.functionId,
|
|
3048
|
-
displayName: fn.displayName,
|
|
3049
|
-
functionKind: fn.functionKind,
|
|
3050
|
-
startLine: fn.startLine,
|
|
3051
|
-
endLine: fn.endLine,
|
|
3052
|
-
isEntrypoint: fn.isEntrypoint,
|
|
3053
|
-
isExported: fn.isExported,
|
|
3054
|
-
actionKinds: [...new Set(fn.semanticActions.map((a) => a.actionKind))],
|
|
3055
|
-
targetModels: [...new Set(fn.semanticActions.map((a) => a.targetModel).filter(Boolean))],
|
|
3056
|
-
targetOperations: [...new Set(fn.semanticActions.map((a) => a.targetOperation).filter(Boolean))],
|
|
3057
|
-
outboundCallCount: fn.calls.length,
|
|
3058
|
-
resolvedOutboundCallCount: fn.calls.filter((c) => c.resolvedTargetFunctionId).length,
|
|
3059
|
-
semanticActionCount: fn.semanticActions.length,
|
|
3060
|
-
evidence,
|
|
3061
|
-
confidence: confidence2,
|
|
3062
|
-
reasons
|
|
3063
|
-
};
|
|
3064
|
-
});
|
|
3065
|
-
}
|
|
3066
|
-
}
|
|
3067
|
-
}
|
|
3068
|
-
return {
|
|
3069
|
-
path: pf.relativePath,
|
|
3070
|
-
frameworkRole: pf.frameworkRole,
|
|
3071
|
-
productDomain: pf.productDomain,
|
|
3072
|
-
gravity: Math.round(pf.gravity),
|
|
3073
|
-
heat: Math.round(pf.heat),
|
|
3074
|
-
severity: pf.canonicalSeverity,
|
|
3075
|
-
confidence,
|
|
3076
|
-
isLoadBearing: pf.canonicalLoadBearing || loadBearingScore >= 5,
|
|
3077
|
-
loadBearingScore,
|
|
3078
|
-
riskTypes: pf.riskTypes,
|
|
3079
|
-
sideEffectProfile: pf.sideEffectProfile,
|
|
3080
|
-
blastRadius: pf.importedBy,
|
|
3081
|
-
runtimeEntrypoints,
|
|
3082
|
-
entrypointTraceStatus,
|
|
3083
|
-
blockedImports: pf.importsUnresolved,
|
|
3084
|
-
observableOutputs,
|
|
3085
|
-
writeIntents: pf.writeIntents,
|
|
3086
|
-
patchRisk,
|
|
3087
|
-
safePatchStrategy: inferSafePatchStrategy(pf.riskTypes, pf.sideEffectProfile),
|
|
3088
|
-
doNotTouch: inferDoNotTouch(pf.sideEffectProfile, pf.productDomain),
|
|
3089
|
-
testProbes: inferTestProbes(pf.writeIntents, observableOutputs),
|
|
3090
|
-
rawEvidence,
|
|
3091
|
-
displayEvidence,
|
|
3092
|
-
criticalFunctions,
|
|
3093
|
-
analysisAnnotation: `${pf.frameworkRole} in ${pf.productDomain} domain. fanIn=${pf.gravitySignals.fanIn} cyclomatic=${pf.gravitySignals.cyclomatic} loc=${pf.gravitySignals.loc}`,
|
|
3094
|
-
hashes: { fileHash, evidenceHash: rawEvidence.map((e) => e.evidenceHash).join("-") }
|
|
3095
|
-
};
|
|
3096
|
-
});
|
|
3097
|
-
const dest = join8(dir, "delta_targets.json");
|
|
3098
|
-
const tmp = dest + ".tmp";
|
|
3099
|
-
await writeFile8(tmp, JSON.stringify(deltaTargets, null, 2), "utf8");
|
|
3100
|
-
const { rename } = await import("fs/promises");
|
|
3101
|
-
await rename(tmp, dest);
|
|
3102
|
-
const validationReport = await buildValidationReport(store, deltaTargets, projectRoot);
|
|
3103
|
-
await writeFile8(join8(dir, "validation_report.json"), JSON.stringify(validationReport, null, 2), "utf8");
|
|
2815
|
+
const deltaTargets = Object.values(persisted).filter((pf) => pf.isRealSource).sort((a, b) => b.gravity - a.gravity).map((pf) => ({
|
|
2816
|
+
path: pf.relativePath,
|
|
2817
|
+
gravity: Math.round(pf.gravity),
|
|
2818
|
+
isLoadBearing: pf.canonicalLoadBearing,
|
|
2819
|
+
// STRICT: fanIn >= 10
|
|
2820
|
+
blastRadius: pf.importedBy,
|
|
2821
|
+
pillarHint: pf.pillarHint
|
|
2822
|
+
}));
|
|
2823
|
+
const validationReport = await buildValidationReport(store, deltaTargets, projectRoot, cr);
|
|
3104
2824
|
for (const e of validationReport.errors) {
|
|
3105
2825
|
console.error(`[vibe-splain] VALIDATION ERROR [${e.rule}] ${e.file}: ${e.detail}`);
|
|
3106
2826
|
}
|
|
@@ -3109,24 +2829,27 @@ async function runScoring(projectRoot, cr, binding) {
|
|
|
3109
2829
|
}
|
|
3110
2830
|
return { store, deltaTargets, validationReport };
|
|
3111
2831
|
}
|
|
3112
|
-
async function buildValidationReport(store, deltaTargets, projectRoot) {
|
|
2832
|
+
async function buildValidationReport(store, deltaTargets, projectRoot, cr) {
|
|
3113
2833
|
const errors = [];
|
|
3114
2834
|
const warnings = [];
|
|
3115
2835
|
let passCount = 0;
|
|
3116
|
-
|
|
2836
|
+
let tracedCount = 0;
|
|
2837
|
+
let realCount = 0;
|
|
3117
2838
|
for (const [, pf] of Object.entries(store.files)) {
|
|
3118
2839
|
if (!pf.isRealSource)
|
|
3119
2840
|
continue;
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
2841
|
+
realCount++;
|
|
2842
|
+
const classified = cr.classified.find((f) => f.rel === pf.relativePath);
|
|
2843
|
+
if (classified && classified.entrypointTraceStatus === "complete")
|
|
2844
|
+
tracedCount++;
|
|
2845
|
+
if (pf.canonicalSeverity === 5 && !pf.canonicalLoadBearing && pf.gravitySignals.fanIn < 10) {
|
|
2846
|
+
if (!pf.isOperationallyCritical) {
|
|
2847
|
+
errors.push({
|
|
2848
|
+
file: pf.relativePath,
|
|
2849
|
+
rule: "severity_5_no_criticality",
|
|
2850
|
+
detail: "severity=5 but not load-bearing and not operationally critical"
|
|
2851
|
+
});
|
|
2852
|
+
}
|
|
3130
2853
|
}
|
|
3131
2854
|
if (pf.writeIntents.includes("handle_payment_webhook") && pf.sideEffectProfile.includes("none_detected")) {
|
|
3132
2855
|
errors.push({
|
|
@@ -3138,7 +2861,7 @@ async function buildValidationReport(store, deltaTargets, projectRoot) {
|
|
|
3138
2861
|
});
|
|
3139
2862
|
continue;
|
|
3140
2863
|
}
|
|
3141
|
-
if (pf.productDomain === "booking_creation" &&
|
|
2864
|
+
if (pf.productDomain === "booking_creation" && classified?.entrypointTraceStatus === "no_runtime_entrypoint_found" && pf.importsUnresolved.length === 0) {
|
|
3142
2865
|
errors.push({
|
|
3143
2866
|
file: pf.relativePath,
|
|
3144
2867
|
rule: "booking_creation_no_entrypoint_no_blockers",
|
|
@@ -3146,72 +2869,29 @@ async function buildValidationReport(store, deltaTargets, projectRoot) {
|
|
|
3146
2869
|
});
|
|
3147
2870
|
continue;
|
|
3148
2871
|
}
|
|
3149
|
-
if (
|
|
3150
|
-
errors.push({
|
|
3151
|
-
file: pf.relativePath,
|
|
3152
|
-
rule: "severity_mismatch_delta",
|
|
3153
|
-
detail: "DeltaTarget severity does not match canonicalSeverity",
|
|
3154
|
-
expected: String(pf.canonicalSeverity),
|
|
3155
|
-
actual: String(delta.severity)
|
|
3156
|
-
});
|
|
3157
|
-
continue;
|
|
3158
|
-
}
|
|
3159
|
-
if (pf.canonicalSeverity >= 4 && (delta?.rawEvidence.length ?? 0) === 0 && pf.hotSpans.length === 0) {
|
|
2872
|
+
if (pf.canonicalSeverity >= 4 && pf.hotSpans.length === 0) {
|
|
3160
2873
|
errors.push({
|
|
3161
2874
|
file: pf.relativePath,
|
|
3162
2875
|
rule: "high_severity_no_evidence",
|
|
3163
|
-
detail: `severity=${pf.canonicalSeverity} but
|
|
2876
|
+
detail: `severity=${pf.canonicalSeverity} but no evidence hotSpans found`
|
|
3164
2877
|
});
|
|
3165
2878
|
continue;
|
|
3166
2879
|
}
|
|
3167
|
-
if (pf.canonicalSeverity >= 4 && (
|
|
2880
|
+
if (pf.canonicalSeverity >= 4 && (classified?.runtimeEntrypoints.length ?? 0) === 0) {
|
|
3168
2881
|
warnings.push({
|
|
3169
2882
|
file: pf.relativePath,
|
|
3170
2883
|
rule: "high_severity_no_entrypoints",
|
|
3171
2884
|
detail: `severity=${pf.canonicalSeverity} but no runtime entrypoints found \u2014 check alias resolution`
|
|
3172
2885
|
});
|
|
3173
2886
|
}
|
|
3174
|
-
if (
|
|
3175
|
-
const foundPaths =
|
|
2887
|
+
if (classified?.entrypointTraceStatus === "partial_wrong_surface") {
|
|
2888
|
+
const foundPaths = classified.runtimeEntrypoints.map((e) => e.path).join(", ");
|
|
3176
2889
|
warnings.push({
|
|
3177
2890
|
file: pf.relativePath,
|
|
3178
2891
|
rule: "partial_wrong_surface",
|
|
3179
2892
|
detail: `Entrypoints found but domain surface mismatch for ${pf.productDomain}. Found: ${foundPaths}`
|
|
3180
2893
|
});
|
|
3181
2894
|
}
|
|
3182
|
-
if (pf.riskTypes.includes("registry_bottleneck")) {
|
|
3183
|
-
if (pf.canonicalSeverity < 4)
|
|
3184
|
-
errors.push({
|
|
3185
|
-
file: pf.relativePath,
|
|
3186
|
-
rule: "registry_bottleneck_severity",
|
|
3187
|
-
detail: "registry_bottleneck file must have severity >= 4",
|
|
3188
|
-
expected: ">=4",
|
|
3189
|
-
actual: String(pf.canonicalSeverity)
|
|
3190
|
-
});
|
|
3191
|
-
if (!pf.canonicalLoadBearing)
|
|
3192
|
-
errors.push({
|
|
3193
|
-
file: pf.relativePath,
|
|
3194
|
-
rule: "registry_bottleneck_load_bearing",
|
|
3195
|
-
detail: "registry_bottleneck file must be load-bearing",
|
|
3196
|
-
expected: "true",
|
|
3197
|
-
actual: "false"
|
|
3198
|
-
});
|
|
3199
|
-
if (delta && delta.patchRisk.level !== "high" && delta.patchRisk.level !== "critical")
|
|
3200
|
-
errors.push({
|
|
3201
|
-
file: pf.relativePath,
|
|
3202
|
-
rule: "registry_bottleneck_patch_risk",
|
|
3203
|
-
detail: "registry_bottleneck file must have patch risk high or critical",
|
|
3204
|
-
expected: "high|critical",
|
|
3205
|
-
actual: delta?.patchRisk.level ?? "unknown"
|
|
3206
|
-
});
|
|
3207
|
-
}
|
|
3208
|
-
if (pf.productDomain === "data_table" && pf.riskTypes.includes("state_machine") && delta?.patchRisk.level === "low") {
|
|
3209
|
-
warnings.push({
|
|
3210
|
-
file: pf.relativePath,
|
|
3211
|
-
rule: "data_table_state_machine_risk",
|
|
3212
|
-
detail: "data_table state machine should have at least medium patch risk"
|
|
3213
|
-
});
|
|
3214
|
-
}
|
|
3215
2895
|
passCount++;
|
|
3216
2896
|
}
|
|
3217
2897
|
const PAYMENT_PROVIDER_PATH_TERMS = ["stripe", "paypal", "btcpay", "btcpayserver", "alby", "hitpay", "payment"];
|
|
@@ -3236,45 +2916,38 @@ async function buildValidationReport(store, deltaTargets, projectRoot) {
|
|
|
3236
2916
|
let secondaryTrigger = false;
|
|
3237
2917
|
if (!primaryTrigger && pf.productDomain !== "payments_webhooks") {
|
|
3238
2918
|
try {
|
|
3239
|
-
const src = await
|
|
2919
|
+
const src = await readFile5(join6(projectRoot, rel), "utf8");
|
|
3240
2920
|
secondaryTrigger = PAYMENT_CONTENT_TERMS.some((t) => src.includes(t));
|
|
3241
2921
|
} catch {
|
|
3242
2922
|
}
|
|
3243
2923
|
}
|
|
3244
2924
|
if (!primaryTrigger && !secondaryTrigger)
|
|
3245
2925
|
continue;
|
|
3246
|
-
const delta = deltaByPath.get(rel);
|
|
3247
|
-
const triggerLabel = primaryTrigger ? "path" : "content";
|
|
3248
2926
|
const webhookChecks = [
|
|
3249
2927
|
[
|
|
3250
2928
|
pf.productDomain !== "payments_webhooks",
|
|
3251
2929
|
"webhook_domain",
|
|
3252
|
-
`Payment webhook
|
|
2930
|
+
`Payment webhook not classified as payments_webhooks`
|
|
3253
2931
|
],
|
|
3254
2932
|
[
|
|
3255
2933
|
!pf.sideEffectProfile.includes("webhook_ingress"),
|
|
3256
2934
|
"webhook_ingress_missing",
|
|
3257
|
-
`Payment webhook
|
|
2935
|
+
`Payment webhook missing webhook_ingress side effect`
|
|
3258
2936
|
],
|
|
3259
2937
|
[
|
|
3260
2938
|
!pf.sideEffectProfile.includes("payment_mutation"),
|
|
3261
2939
|
"webhook_payment_mutation_missing",
|
|
3262
|
-
`Payment webhook
|
|
2940
|
+
`Payment webhook missing payment_mutation side effect`
|
|
3263
2941
|
],
|
|
3264
2942
|
[
|
|
3265
2943
|
!pf.writeIntents.includes("handle_payment_webhook"),
|
|
3266
2944
|
"webhook_write_intent_missing",
|
|
3267
|
-
`Payment webhook
|
|
2945
|
+
`Payment webhook missing handle_payment_webhook write intent`
|
|
3268
2946
|
],
|
|
3269
2947
|
[
|
|
3270
|
-
|
|
3271
|
-
"
|
|
3272
|
-
`Payment webhook
|
|
3273
|
-
],
|
|
3274
|
-
[
|
|
3275
|
-
!pf.canonicalLoadBearing,
|
|
3276
|
-
"webhook_load_bearing",
|
|
3277
|
-
`Payment webhook (${triggerLabel} trigger) must be load-bearing`
|
|
2948
|
+
!pf.isOperationallyCritical,
|
|
2949
|
+
"webhook_criticality",
|
|
2950
|
+
`Payment webhook must be operationally critical`
|
|
3278
2951
|
]
|
|
3279
2952
|
];
|
|
3280
2953
|
for (const [condition, rule, detail] of webhookChecks) {
|
|
@@ -3282,12 +2955,13 @@ async function buildValidationReport(store, deltaTargets, projectRoot) {
|
|
|
3282
2955
|
errors.push({ file: rel, rule, detail });
|
|
3283
2956
|
}
|
|
3284
2957
|
}
|
|
2958
|
+
const coverage = realCount > 0 ? Math.round(tracedCount / realCount * 100) : 0;
|
|
3285
2959
|
return {
|
|
3286
2960
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3287
2961
|
passed: errors.length === 0,
|
|
3288
2962
|
errors,
|
|
3289
2963
|
warnings,
|
|
3290
|
-
summary: { errorCount: errors.length, warningCount: warnings.length, passCount }
|
|
2964
|
+
summary: { errorCount: errors.length, warningCount: warnings.length, passCount, entrypointTraceCoverage: coverage }
|
|
3291
2965
|
};
|
|
3292
2966
|
}
|
|
3293
2967
|
|
|
@@ -3298,8 +2972,6 @@ async function runPipeline(projectRoot) {
|
|
|
3298
2972
|
const binding = await runActionBinding(projectRoot, inv, res);
|
|
3299
2973
|
const cr = await runClassification(projectRoot, inv, res);
|
|
3300
2974
|
const scoring = await runScoring(projectRoot, cr, binding);
|
|
3301
|
-
await writeGraph(projectRoot, res.graph);
|
|
3302
|
-
await writeAnalysis(projectRoot, scoring.store);
|
|
3303
2975
|
const files = cr.classified.filter((f) => f.isRealSource).sort((a, b) => b.gravity - a.gravity).map((f) => ({
|
|
3304
2976
|
path: f.abs,
|
|
3305
2977
|
relativePath: f.rel,
|
|
@@ -3332,7 +3004,7 @@ async function runPipeline(projectRoot) {
|
|
|
3332
3004
|
productDomain: f.productDomain,
|
|
3333
3005
|
sideEffectProfile: f.sideEffectProfile
|
|
3334
3006
|
}));
|
|
3335
|
-
const uiUrl = `file://${
|
|
3007
|
+
const uiUrl = `file://${join7(projectRoot, ".vibe-splainer", "ui", "index.html")}`;
|
|
3336
3008
|
return {
|
|
3337
3009
|
projectRoot,
|
|
3338
3010
|
totalFilesScanned: cr.classified.length,
|
|
@@ -3342,6 +3014,7 @@ async function runPipeline(projectRoot) {
|
|
|
3342
3014
|
wildCandidates,
|
|
3343
3015
|
uiUrl,
|
|
3344
3016
|
graph: res.graph,
|
|
3017
|
+
store: scoring.store,
|
|
3345
3018
|
validation: {
|
|
3346
3019
|
passed: scoring.validationReport.passed,
|
|
3347
3020
|
errors: scoring.validationReport.summary.errorCount,
|
|
@@ -3365,7 +3038,7 @@ async function getFileAnalysis(absPath) {
|
|
|
3365
3038
|
return null;
|
|
3366
3039
|
let source;
|
|
3367
3040
|
try {
|
|
3368
|
-
source = await
|
|
3041
|
+
source = await readFile6(absPath, "utf8");
|
|
3369
3042
|
} catch {
|
|
3370
3043
|
return null;
|
|
3371
3044
|
}
|
|
@@ -3402,56 +3075,30 @@ async function getFileAnalysis(absPath) {
|
|
|
3402
3075
|
};
|
|
3403
3076
|
}
|
|
3404
3077
|
|
|
3405
|
-
// ../brain/dist/
|
|
3406
|
-
import {
|
|
3407
|
-
import {
|
|
3408
|
-
|
|
3409
|
-
|
|
3410
|
-
import { existsSync as existsSync4, cpSync } from "fs";
|
|
3411
|
-
var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
|
|
3412
|
-
var dossierMutex = new Mutex();
|
|
3413
|
-
async function readDossier(projectRoot) {
|
|
3414
|
-
const dossierPath = join10(projectRoot, ".vibe-splainer", "dossier.json");
|
|
3078
|
+
// ../brain/dist/analysis.js
|
|
3079
|
+
import { join as join8 } from "path";
|
|
3080
|
+
import { readFile as readFile7, writeFile as writeFile6, mkdir as mkdir5 } from "fs/promises";
|
|
3081
|
+
async function readAnalysis(projectRoot) {
|
|
3082
|
+
const p = join8(projectRoot, ".vibe-splainer", "analysis.json");
|
|
3415
3083
|
try {
|
|
3416
|
-
const raw = await
|
|
3084
|
+
const raw = await readFile7(p, "utf8");
|
|
3417
3085
|
return JSON.parse(raw);
|
|
3418
3086
|
} catch {
|
|
3419
3087
|
return null;
|
|
3420
3088
|
}
|
|
3421
3089
|
}
|
|
3422
|
-
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
await
|
|
3430
|
-
|
|
3431
|
-
|
|
3432
|
-
|
|
3433
|
-
const { rename } = await import("fs/promises");
|
|
3434
|
-
await rename(tmp, dossierPath);
|
|
3435
|
-
await regenerateUI(projectRoot, dossier);
|
|
3436
|
-
});
|
|
3437
|
-
}
|
|
3438
|
-
async function regenerateUI(projectRoot, dossier) {
|
|
3439
|
-
const uiDir = join10(projectRoot, ".vibe-splainer", "ui");
|
|
3440
|
-
await mkdir7(uiDir, { recursive: true });
|
|
3441
|
-
let templateDir = join10(__dirname2, "ui");
|
|
3442
|
-
if (!existsSync4(templateDir)) {
|
|
3443
|
-
templateDir = join10(__dirname2, "../../cli/dist/ui");
|
|
3444
|
-
}
|
|
3445
|
-
if (!existsSync4(templateDir)) {
|
|
3446
|
-
console.error("[vibe-splain] UI template not found at", templateDir, "- skipping UI regeneration");
|
|
3447
|
-
return;
|
|
3090
|
+
|
|
3091
|
+
// ../brain/dist/dossier.js
|
|
3092
|
+
import { join as join9 } from "path";
|
|
3093
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
3094
|
+
async function readDossier(projectRoot) {
|
|
3095
|
+
const dossierPath = join9(projectRoot, ".vibe-splainer", "dossier.json");
|
|
3096
|
+
try {
|
|
3097
|
+
const raw = await readFile8(dossierPath, "utf8");
|
|
3098
|
+
return JSON.parse(raw);
|
|
3099
|
+
} catch {
|
|
3100
|
+
return null;
|
|
3448
3101
|
}
|
|
3449
|
-
cpSync(templateDir, uiDir, { recursive: true });
|
|
3450
|
-
let html = await readFile9(join10(templateDir, "index.html"), "utf8");
|
|
3451
|
-
const injection = `<script>window.__VIBE_DOSSIER__ = ${JSON.stringify(dossier)};</script>`;
|
|
3452
|
-
html = html.replace("<!-- VIBE_DOSSIER_INJECTION_POINT -->", injection);
|
|
3453
|
-
await writeFile9(join10(uiDir, "index.html"), html, "utf8");
|
|
3454
|
-
console.error("[vibe-splain] UI regenerated at", join10(uiDir, "index.html"));
|
|
3455
3102
|
}
|
|
3456
3103
|
function validateMermaidNodeCount(diagram) {
|
|
3457
3104
|
if (!diagram)
|
|
@@ -3468,11 +3115,452 @@ function validateMermaidNodeCount(diagram) {
|
|
|
3468
3115
|
return nodes.size <= 7;
|
|
3469
3116
|
}
|
|
3470
3117
|
|
|
3471
|
-
// ../brain/dist/
|
|
3118
|
+
// ../brain/dist/policy/RecommendationEngine.js
|
|
3119
|
+
function getEffortValue(effort) {
|
|
3120
|
+
if (effort === "low")
|
|
3121
|
+
return 1;
|
|
3122
|
+
if (effort === "medium")
|
|
3123
|
+
return 2;
|
|
3124
|
+
return 3;
|
|
3125
|
+
}
|
|
3126
|
+
function getImpactValue(impact) {
|
|
3127
|
+
if (impact === "low")
|
|
3128
|
+
return 1;
|
|
3129
|
+
if (impact === "medium")
|
|
3130
|
+
return 2;
|
|
3131
|
+
return 3;
|
|
3132
|
+
}
|
|
3133
|
+
var RecommendationEngine = class {
|
|
3134
|
+
static generateRecommendations(file) {
|
|
3135
|
+
const recommendations = [];
|
|
3136
|
+
if (file.riskTypes.includes("mutation_orchestration")) {
|
|
3137
|
+
recommendations.push({
|
|
3138
|
+
strategy: "Extract Decision Logic",
|
|
3139
|
+
description: "Do not rewrite inline. Extract pure decision logic into a tested reducer or state machine first. Preserve all side-effect call sites (redirect URLs, SDK event names, response shapes) as invariants.",
|
|
3140
|
+
effort: "high",
|
|
3141
|
+
impact: "high"
|
|
3142
|
+
});
|
|
3143
|
+
}
|
|
3144
|
+
if (file.riskTypes.includes("registry_bottleneck")) {
|
|
3145
|
+
recommendations.push({
|
|
3146
|
+
strategy: "Append-Only Registry Updates",
|
|
3147
|
+
description: "Add new entries without removing existing keys. Treat the registry map as append-only until all consumers are verified.",
|
|
3148
|
+
effort: "low",
|
|
3149
|
+
impact: "medium"
|
|
3150
|
+
});
|
|
3151
|
+
}
|
|
3152
|
+
if (file.riskTypes.includes("registry_consumer")) {
|
|
3153
|
+
recommendations.push({
|
|
3154
|
+
strategy: "Verify Registry Contract",
|
|
3155
|
+
description: "Verify the registry contract before patching. Changes to field types must be reflected in both the registry and all rendering paths.",
|
|
3156
|
+
effort: "medium",
|
|
3157
|
+
impact: "low"
|
|
3158
|
+
});
|
|
3159
|
+
}
|
|
3160
|
+
if (file.riskTypes.includes("route_handler_write_path")) {
|
|
3161
|
+
recommendations.push({
|
|
3162
|
+
strategy: "Integration Testing First",
|
|
3163
|
+
description: "Add integration tests covering success and failure paths before modifying. Verify HTTP status codes and response shapes are preserved.",
|
|
3164
|
+
effort: "medium",
|
|
3165
|
+
impact: "high"
|
|
3166
|
+
});
|
|
3167
|
+
}
|
|
3168
|
+
if (file.riskTypes.includes("god_component") || file.riskTypes.includes("god_hook")) {
|
|
3169
|
+
recommendations.push({
|
|
3170
|
+
strategy: "Extract Sub-Concerns",
|
|
3171
|
+
description: "Extract sub-concerns into separate modules first. Only refactor the extraction points after tests confirm equivalence.",
|
|
3172
|
+
effort: "high",
|
|
3173
|
+
impact: "high"
|
|
3174
|
+
});
|
|
3175
|
+
}
|
|
3176
|
+
if (file.sideEffectProfile.includes("database_write")) {
|
|
3177
|
+
recommendations.push({
|
|
3178
|
+
strategy: "Feature Flags & Staging",
|
|
3179
|
+
description: "Wrap changes in a transaction or use a feature flag. Run against a staging database before production.",
|
|
3180
|
+
effort: "medium",
|
|
3181
|
+
impact: "high"
|
|
3182
|
+
});
|
|
3183
|
+
}
|
|
3184
|
+
if (recommendations.length === 0 && file.importedBy.length >= 5) {
|
|
3185
|
+
recommendations.push({
|
|
3186
|
+
strategy: "Review Blast Radius",
|
|
3187
|
+
description: "Review importedBy before patching. Run affected integration tests.",
|
|
3188
|
+
effort: "medium",
|
|
3189
|
+
impact: "medium"
|
|
3190
|
+
});
|
|
3191
|
+
}
|
|
3192
|
+
recommendations.sort((a, b) => {
|
|
3193
|
+
const ratioA = getImpactValue(a.impact) / getEffortValue(a.effort);
|
|
3194
|
+
const ratioB = getImpactValue(b.impact) / getEffortValue(b.effort);
|
|
3195
|
+
return ratioB - ratioA;
|
|
3196
|
+
});
|
|
3197
|
+
return recommendations;
|
|
3198
|
+
}
|
|
3199
|
+
};
|
|
3200
|
+
|
|
3201
|
+
// dist/export/ArtifactBundleWriter.js
|
|
3202
|
+
import { join as join10 } from "path";
|
|
3203
|
+
import { writeFile as writeFile7, mkdir as mkdir6, rm, rename } from "fs/promises";
|
|
3204
|
+
import { createHash } from "crypto";
|
|
3205
|
+
var ArtifactBundleWriter = class {
|
|
3206
|
+
projectRoot;
|
|
3207
|
+
constructor(projectRoot) {
|
|
3208
|
+
this.projectRoot = projectRoot;
|
|
3209
|
+
}
|
|
3210
|
+
async writeBundle(artifacts) {
|
|
3211
|
+
const outputDir = join10(this.projectRoot, ".vibe-splainer");
|
|
3212
|
+
const stagingDir = join10(this.projectRoot, ".vibe-splainer.tmp");
|
|
3213
|
+
try {
|
|
3214
|
+
await rm(stagingDir, { recursive: true, force: true });
|
|
3215
|
+
const { existsSync: existsSync5 } = await import("fs");
|
|
3216
|
+
const { cp } = await import("fs/promises");
|
|
3217
|
+
if (existsSync5(outputDir)) {
|
|
3218
|
+
await cp(outputDir, stagingDir, { recursive: true });
|
|
3219
|
+
} else {
|
|
3220
|
+
await mkdir6(stagingDir, { recursive: true });
|
|
3221
|
+
}
|
|
3222
|
+
const manifestArtifacts = [];
|
|
3223
|
+
for (const artifact of artifacts) {
|
|
3224
|
+
const destPath = join10(stagingDir, artifact.path);
|
|
3225
|
+
await mkdir6(join10(destPath, ".."), { recursive: true });
|
|
3226
|
+
await writeFile7(destPath, artifact.content);
|
|
3227
|
+
const contentStr = artifact.content;
|
|
3228
|
+
const buffer = typeof contentStr === "string" ? Buffer.from(contentStr, "utf-8") : contentStr;
|
|
3229
|
+
manifestArtifacts.push({
|
|
3230
|
+
type: artifact.type,
|
|
3231
|
+
path: artifact.path,
|
|
3232
|
+
checksum: "sha256:" + createHash("sha256").update(buffer).digest("hex"),
|
|
3233
|
+
sizeBytes: buffer.length
|
|
3234
|
+
});
|
|
3235
|
+
}
|
|
3236
|
+
const manifest = {
|
|
3237
|
+
schemaVersion: "1.0.0",
|
|
3238
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3239
|
+
projectRoot: this.projectRoot,
|
|
3240
|
+
artifacts: manifestArtifacts
|
|
3241
|
+
};
|
|
3242
|
+
await writeFile7(join10(stagingDir, "artifact_manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
|
|
3243
|
+
await rm(outputDir, { recursive: true, force: true });
|
|
3244
|
+
await rename(stagingDir, outputDir);
|
|
3245
|
+
} catch (err) {
|
|
3246
|
+
await rm(stagingDir, { recursive: true, force: true });
|
|
3247
|
+
throw err;
|
|
3248
|
+
}
|
|
3249
|
+
}
|
|
3250
|
+
};
|
|
3251
|
+
|
|
3252
|
+
// dist/export/renderers/JsonRenderer.js
|
|
3253
|
+
var JsonRenderer = class {
|
|
3254
|
+
render(viewModel, _store) {
|
|
3255
|
+
return [
|
|
3256
|
+
{
|
|
3257
|
+
type: "dossier",
|
|
3258
|
+
path: "dossier.json",
|
|
3259
|
+
content: JSON.stringify(viewModel, null, 2)
|
|
3260
|
+
}
|
|
3261
|
+
];
|
|
3262
|
+
}
|
|
3263
|
+
};
|
|
3264
|
+
|
|
3265
|
+
// dist/export/renderers/HtmlRenderer.js
|
|
3266
|
+
import { join as join11, dirname as dirname3, relative as relative3 } from "path";
|
|
3267
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3268
|
+
import { existsSync as existsSync4, readFileSync, readdirSync, statSync } from "fs";
|
|
3269
|
+
var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
|
|
3270
|
+
function getAllFiles(dirPath, arrayOfFiles = []) {
|
|
3271
|
+
const files = readdirSync(dirPath);
|
|
3272
|
+
files.forEach(function(file) {
|
|
3273
|
+
const fullPath = join11(dirPath, file);
|
|
3274
|
+
if (statSync(fullPath).isDirectory()) {
|
|
3275
|
+
arrayOfFiles = getAllFiles(fullPath, arrayOfFiles);
|
|
3276
|
+
} else {
|
|
3277
|
+
arrayOfFiles.push(fullPath);
|
|
3278
|
+
}
|
|
3279
|
+
});
|
|
3280
|
+
return arrayOfFiles;
|
|
3281
|
+
}
|
|
3282
|
+
var HtmlRenderer = class {
|
|
3283
|
+
render(viewModel, _store) {
|
|
3284
|
+
let templateDir = join11(__dirname2, "..", "..", "ui");
|
|
3285
|
+
if (!existsSync4(templateDir)) {
|
|
3286
|
+
templateDir = join11(__dirname2, "..", "..", "..", "..", "dist", "ui");
|
|
3287
|
+
}
|
|
3288
|
+
if (!existsSync4(templateDir)) {
|
|
3289
|
+
console.error("[vibe-splain] UI template not found at", templateDir, "- skipping UI regeneration");
|
|
3290
|
+
return [];
|
|
3291
|
+
}
|
|
3292
|
+
const artifacts = [];
|
|
3293
|
+
const allFiles = getAllFiles(templateDir);
|
|
3294
|
+
for (const file of allFiles) {
|
|
3295
|
+
const relPath = relative3(templateDir, file);
|
|
3296
|
+
if (relPath === "index.html") {
|
|
3297
|
+
const templateHtml = readFileSync(file, "utf8");
|
|
3298
|
+
const injection = `<script>window.__VIBE_DOSSIER__ = ${JSON.stringify(viewModel)};</script>`;
|
|
3299
|
+
const bakedHtml = templateHtml.replace("<!-- VIBE_DOSSIER_INJECTION_POINT -->", injection);
|
|
3300
|
+
artifacts.push({
|
|
3301
|
+
type: "html",
|
|
3302
|
+
path: join11("ui", relPath),
|
|
3303
|
+
content: bakedHtml
|
|
3304
|
+
});
|
|
3305
|
+
} else {
|
|
3306
|
+
artifacts.push({
|
|
3307
|
+
type: "asset",
|
|
3308
|
+
path: join11("ui", relPath),
|
|
3309
|
+
content: readFileSync(file)
|
|
3310
|
+
});
|
|
3311
|
+
}
|
|
3312
|
+
}
|
|
3313
|
+
return artifacts;
|
|
3314
|
+
}
|
|
3315
|
+
};
|
|
3316
|
+
|
|
3317
|
+
// dist/export/renderers/DeltaRenderer.js
|
|
3318
|
+
var DeltaRenderer = class {
|
|
3319
|
+
render(_viewModel, store) {
|
|
3320
|
+
const deltaTargets = Object.values(store.files).filter((pf) => pf.isRealSource).sort((a, b) => b.gravity - a.gravity).map((pf) => ({
|
|
3321
|
+
path: pf.relativePath,
|
|
3322
|
+
gravity: Math.round(pf.gravity),
|
|
3323
|
+
isLoadBearing: pf.canonicalLoadBearing,
|
|
3324
|
+
blastRadius: pf.importedBy,
|
|
3325
|
+
pillarHint: pf.pillarHint
|
|
3326
|
+
}));
|
|
3327
|
+
return [
|
|
3328
|
+
{
|
|
3329
|
+
type: "delta",
|
|
3330
|
+
path: "delta_targets.json",
|
|
3331
|
+
content: JSON.stringify(deltaTargets, null, 2)
|
|
3332
|
+
}
|
|
3333
|
+
];
|
|
3334
|
+
}
|
|
3335
|
+
};
|
|
3336
|
+
|
|
3337
|
+
// dist/export/renderers/AgentMarkdownRenderer.js
|
|
3338
|
+
var AgentMarkdownRenderer = class {
|
|
3339
|
+
budget;
|
|
3340
|
+
constructor(budget = 8e3) {
|
|
3341
|
+
this.budget = budget;
|
|
3342
|
+
}
|
|
3343
|
+
render(viewModel, store) {
|
|
3344
|
+
let md = `# Architectural Dossier: ${viewModel.projectRoot}
|
|
3345
|
+
|
|
3346
|
+
`;
|
|
3347
|
+
if (viewModel.map.brief) {
|
|
3348
|
+
md += `## Project Brief
|
|
3349
|
+
${viewModel.map.brief}
|
|
3350
|
+
|
|
3351
|
+
`;
|
|
3352
|
+
}
|
|
3353
|
+
md += `## Stack & Entrypoints
|
|
3354
|
+
`;
|
|
3355
|
+
md += `- Stack: ${viewModel.map.stack.join(", ")}
|
|
3356
|
+
`;
|
|
3357
|
+
md += `- Entrypoints: ${viewModel.map.entrypoints.join(", ")}
|
|
3358
|
+
|
|
3359
|
+
`;
|
|
3360
|
+
const allDecisions = viewModel.pillars.flatMap((p) => p.decisions).concat(viewModel.wildDiscoveries);
|
|
3361
|
+
const uniqueDecisions = /* @__PURE__ */ new Map();
|
|
3362
|
+
for (const d of allDecisions) {
|
|
3363
|
+
if (d.primaryFile && !uniqueDecisions.has(d.primaryFile)) {
|
|
3364
|
+
uniqueDecisions.set(d.primaryFile, d);
|
|
3365
|
+
}
|
|
3366
|
+
}
|
|
3367
|
+
const tier1 = [];
|
|
3368
|
+
const tier2 = [];
|
|
3369
|
+
const tier3 = [];
|
|
3370
|
+
const sortedFiles = Object.values(store.files).filter((f) => f.isRealSource).sort((a, b) => b.gravity - a.gravity);
|
|
3371
|
+
for (const f of sortedFiles) {
|
|
3372
|
+
const card = uniqueDecisions.get(f.relativePath);
|
|
3373
|
+
const isCritical = card && card.severity >= 4;
|
|
3374
|
+
if (f.gravity >= 70 || isCritical) {
|
|
3375
|
+
tier1.push(f.relativePath);
|
|
3376
|
+
} else if (f.gravity >= 40 || card) {
|
|
3377
|
+
tier2.push(f.relativePath);
|
|
3378
|
+
} else {
|
|
3379
|
+
tier3.push(f.relativePath);
|
|
3380
|
+
}
|
|
3381
|
+
}
|
|
3382
|
+
md += `## Tier 1: Critical Files & Risks
|
|
3383
|
+
|
|
3384
|
+
`;
|
|
3385
|
+
for (const path of tier1) {
|
|
3386
|
+
const f = store.files[path];
|
|
3387
|
+
const card = uniqueDecisions.get(path);
|
|
3388
|
+
const recs = viewModel.recommendations[path] || [];
|
|
3389
|
+
md += `### ${path}
|
|
3390
|
+
`;
|
|
3391
|
+
md += `- Gravity: ${Math.round(f.gravity)} | Heat: ${Math.round(f.heat)}
|
|
3392
|
+
`;
|
|
3393
|
+
md += `- Domain: ${f.productDomain} | Role: ${f.frameworkRole}
|
|
3394
|
+
`;
|
|
3395
|
+
if (card) {
|
|
3396
|
+
md += `
|
|
3397
|
+
**Verdict**: ${card.thesis}
|
|
3398
|
+
`;
|
|
3399
|
+
md += `**Severity**: ${card.severity} | **Category**: ${card.category}
|
|
3400
|
+
`;
|
|
3401
|
+
md += `**Narrative**: ${card.narrative}
|
|
3402
|
+
`;
|
|
3403
|
+
}
|
|
3404
|
+
if (recs.length > 0) {
|
|
3405
|
+
md += `
|
|
3406
|
+
**Safe Patch Strategies**:
|
|
3407
|
+
`;
|
|
3408
|
+
for (const r of recs) {
|
|
3409
|
+
md += `- **${r.strategy}**: ${r.description}
|
|
3410
|
+
`;
|
|
3411
|
+
}
|
|
3412
|
+
}
|
|
3413
|
+
md += `
|
|
3414
|
+
---
|
|
3415
|
+
|
|
3416
|
+
`;
|
|
3417
|
+
}
|
|
3418
|
+
md += `## Tier 2: Important Files
|
|
3419
|
+
|
|
3420
|
+
`;
|
|
3421
|
+
for (const path of tier2) {
|
|
3422
|
+
const f = store.files[path];
|
|
3423
|
+
const card = uniqueDecisions.get(path);
|
|
3424
|
+
md += `- **${path}** (Gravity: ${Math.round(f.gravity)})`;
|
|
3425
|
+
if (card) {
|
|
3426
|
+
md += ` \u2014 ${card.thesis}`;
|
|
3427
|
+
}
|
|
3428
|
+
md += `
|
|
3429
|
+
`;
|
|
3430
|
+
}
|
|
3431
|
+
md += `
|
|
3432
|
+
`;
|
|
3433
|
+
md += `## Tier 3: Index
|
|
3434
|
+
|
|
3435
|
+
`;
|
|
3436
|
+
for (const path of tier3) {
|
|
3437
|
+
const f = store.files[path];
|
|
3438
|
+
md += `- ${path} (Gravity: ${Math.round(f.gravity)})
|
|
3439
|
+
`;
|
|
3440
|
+
}
|
|
3441
|
+
return [
|
|
3442
|
+
{
|
|
3443
|
+
type: "markdown",
|
|
3444
|
+
path: "dossier.agent.md",
|
|
3445
|
+
content: md
|
|
3446
|
+
}
|
|
3447
|
+
];
|
|
3448
|
+
}
|
|
3449
|
+
};
|
|
3450
|
+
|
|
3451
|
+
// dist/export/renderers/ValidationRenderer.js
|
|
3452
|
+
var ValidationRenderer = class {
|
|
3453
|
+
render(viewModel, _store) {
|
|
3454
|
+
if (!viewModel.map.validation)
|
|
3455
|
+
return [];
|
|
3456
|
+
return [
|
|
3457
|
+
{
|
|
3458
|
+
type: "validation",
|
|
3459
|
+
path: "validation_report.json",
|
|
3460
|
+
content: JSON.stringify(viewModel.map.validation, null, 2)
|
|
3461
|
+
}
|
|
3462
|
+
];
|
|
3463
|
+
}
|
|
3464
|
+
};
|
|
3465
|
+
|
|
3466
|
+
// dist/export/renderers/RawAnalysisRenderer.js
|
|
3467
|
+
var RawAnalysisRenderer = class {
|
|
3468
|
+
render(_viewModel, store) {
|
|
3469
|
+
return [
|
|
3470
|
+
{
|
|
3471
|
+
type: "analysis",
|
|
3472
|
+
path: "analysis.json",
|
|
3473
|
+
content: JSON.stringify(store, null, 2)
|
|
3474
|
+
}
|
|
3475
|
+
];
|
|
3476
|
+
}
|
|
3477
|
+
};
|
|
3478
|
+
|
|
3479
|
+
// dist/export/renderers/GraphRenderer.js
|
|
3480
|
+
var GraphRenderer = class {
|
|
3481
|
+
graph;
|
|
3482
|
+
constructor(graph) {
|
|
3483
|
+
this.graph = graph;
|
|
3484
|
+
}
|
|
3485
|
+
render(_viewModel, _store) {
|
|
3486
|
+
if (!this.graph)
|
|
3487
|
+
return [];
|
|
3488
|
+
return [
|
|
3489
|
+
{
|
|
3490
|
+
type: "graph",
|
|
3491
|
+
path: "graph.json",
|
|
3492
|
+
content: JSON.stringify(this.graph, null, 2)
|
|
3493
|
+
}
|
|
3494
|
+
];
|
|
3495
|
+
}
|
|
3496
|
+
};
|
|
3497
|
+
|
|
3498
|
+
// dist/export/ExportOrchestrator.js
|
|
3499
|
+
var ExportOrchestrator = class {
|
|
3500
|
+
projectRoot;
|
|
3501
|
+
constructor(projectRoot) {
|
|
3502
|
+
this.projectRoot = projectRoot;
|
|
3503
|
+
}
|
|
3504
|
+
async writeBundle(dossier, options = {}, store, graph) {
|
|
3505
|
+
const finalStore = store || await readAnalysis(this.projectRoot);
|
|
3506
|
+
if (!finalStore) {
|
|
3507
|
+
throw new Error("Analysis store not found. Scan the project first.");
|
|
3508
|
+
}
|
|
3509
|
+
for (const p of dossier.pillars) {
|
|
3510
|
+
p.decisions = p.decisions.filter((c) => !(c.severity === 1 && c.category === "Convention"));
|
|
3511
|
+
p.cardCount = p.decisions.length;
|
|
3512
|
+
}
|
|
3513
|
+
const viewModel = this.buildViewModel(dossier, finalStore);
|
|
3514
|
+
const artifacts = [];
|
|
3515
|
+
artifacts.push(...await new JsonRenderer().render(viewModel, finalStore));
|
|
3516
|
+
artifacts.push(...await new DeltaRenderer().render(viewModel, finalStore));
|
|
3517
|
+
artifacts.push(...await new ValidationRenderer().render(viewModel, finalStore));
|
|
3518
|
+
artifacts.push(...await new RawAnalysisRenderer().render(viewModel, finalStore));
|
|
3519
|
+
if (graph) {
|
|
3520
|
+
artifacts.push(...await new GraphRenderer(graph).render(viewModel, finalStore));
|
|
3521
|
+
}
|
|
3522
|
+
const formats = ["html", "markdown"];
|
|
3523
|
+
if (options.format && options.format !== "json" && options.format !== "delta") {
|
|
3524
|
+
formats.length = 0;
|
|
3525
|
+
formats.push(options.format);
|
|
3526
|
+
}
|
|
3527
|
+
if (formats.includes("html")) {
|
|
3528
|
+
artifacts.push(...await new HtmlRenderer().render(viewModel, finalStore));
|
|
3529
|
+
}
|
|
3530
|
+
if (formats.includes("markdown")) {
|
|
3531
|
+
artifacts.push(...await new AgentMarkdownRenderer(options.budget).render(viewModel, finalStore));
|
|
3532
|
+
}
|
|
3533
|
+
const writer = new ArtifactBundleWriter(this.projectRoot);
|
|
3534
|
+
await writer.writeBundle(artifacts);
|
|
3535
|
+
}
|
|
3536
|
+
buildViewModel(dossier, store) {
|
|
3537
|
+
const recommendations = {};
|
|
3538
|
+
for (const file of dossier.map.topGravity) {
|
|
3539
|
+
const persisted = store.files[file];
|
|
3540
|
+
if (persisted) {
|
|
3541
|
+
recommendations[file] = RecommendationEngine.generateRecommendations(persisted);
|
|
3542
|
+
}
|
|
3543
|
+
}
|
|
3544
|
+
for (const file of dossier.map.topHeat) {
|
|
3545
|
+
if (!recommendations[file]) {
|
|
3546
|
+
const persisted = store.files[file];
|
|
3547
|
+
if (persisted) {
|
|
3548
|
+
recommendations[file] = RecommendationEngine.generateRecommendations(persisted);
|
|
3549
|
+
}
|
|
3550
|
+
}
|
|
3551
|
+
}
|
|
3552
|
+
return {
|
|
3553
|
+
...dossier,
|
|
3554
|
+
recommendations
|
|
3555
|
+
};
|
|
3556
|
+
}
|
|
3557
|
+
};
|
|
3558
|
+
|
|
3559
|
+
// dist/export/Watcher.js
|
|
3472
3560
|
import chokidar from "chokidar";
|
|
3473
3561
|
import { createHash as createHash2 } from "crypto";
|
|
3474
|
-
import { readFile as
|
|
3475
|
-
import { join as
|
|
3562
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
3563
|
+
import { join as join12 } from "path";
|
|
3476
3564
|
function startWatcher(projectRoot, watchedPaths) {
|
|
3477
3565
|
const watcher = chokidar.watch(watchedPaths.length > 0 ? watchedPaths : projectRoot, {
|
|
3478
3566
|
ignoreInitial: true,
|
|
@@ -3484,14 +3572,14 @@ function startWatcher(projectRoot, watchedPaths) {
|
|
|
3484
3572
|
const dossier = await readDossier(projectRoot);
|
|
3485
3573
|
if (!dossier)
|
|
3486
3574
|
return;
|
|
3487
|
-
const content = await
|
|
3575
|
+
const content = await readFile9(filepath, "utf8");
|
|
3488
3576
|
const newHash = createHash2("sha256").update(content).digest("hex");
|
|
3489
3577
|
let mutated = false;
|
|
3490
3578
|
for (const pillar of dossier.pillars) {
|
|
3491
3579
|
for (const card of pillar.decisions) {
|
|
3492
3580
|
if (!card.primaryFile)
|
|
3493
3581
|
continue;
|
|
3494
|
-
const absMatch = filepath ===
|
|
3582
|
+
const absMatch = filepath === join12(projectRoot, card.primaryFile) || filepath.endsWith("/" + card.primaryFile);
|
|
3495
3583
|
if (absMatch && card.lastScannedHash !== newHash) {
|
|
3496
3584
|
card.status = "stale";
|
|
3497
3585
|
const rel = card.primaryFile;
|
|
@@ -3501,8 +3589,11 @@ function startWatcher(projectRoot, watchedPaths) {
|
|
|
3501
3589
|
}
|
|
3502
3590
|
}
|
|
3503
3591
|
}
|
|
3504
|
-
if (mutated)
|
|
3505
|
-
|
|
3592
|
+
if (mutated) {
|
|
3593
|
+
const orchestrator = new ExportOrchestrator(projectRoot);
|
|
3594
|
+
await orchestrator.writeBundle(dossier);
|
|
3595
|
+
console.error(`[vibe-splain] File changed: ${filepath}. Dossier artifacts updated.`);
|
|
3596
|
+
}
|
|
3506
3597
|
} catch (err) {
|
|
3507
3598
|
console.error("[vibe-splain] Watcher error:", err);
|
|
3508
3599
|
}
|
|
@@ -3525,7 +3616,7 @@ var scanProjectTool = {
|
|
|
3525
3616
|
required: ["projectRoot"]
|
|
3526
3617
|
}
|
|
3527
3618
|
};
|
|
3528
|
-
async function handleScanProject(args) {
|
|
3619
|
+
async function handleScanProject(args, options = {}) {
|
|
3529
3620
|
const projectRoot = args.projectRoot;
|
|
3530
3621
|
if (!projectRoot)
|
|
3531
3622
|
throw new Error("projectRoot is required");
|
|
@@ -3537,7 +3628,15 @@ async function handleScanProject(args) {
|
|
|
3537
3628
|
version: "2.0.0",
|
|
3538
3629
|
scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3539
3630
|
projectRoot,
|
|
3540
|
-
map: {
|
|
3631
|
+
map: {
|
|
3632
|
+
...result.map,
|
|
3633
|
+
brief,
|
|
3634
|
+
validation: result.validation ? {
|
|
3635
|
+
passed: result.validation.passed,
|
|
3636
|
+
errors: result.validation.errors,
|
|
3637
|
+
warnings: result.validation.warnings
|
|
3638
|
+
} : void 0
|
|
3639
|
+
},
|
|
3541
3640
|
pillars: existing?.pillars ?? [],
|
|
3542
3641
|
wildDiscoveries: existing?.wildDiscoveries ?? [],
|
|
3543
3642
|
stalePaths: existing?.stalePaths ?? []
|
|
@@ -3547,12 +3646,22 @@ async function handleScanProject(args) {
|
|
|
3547
3646
|
dossier.pillars.push({ name: def.name, cardCount: 0, decisions: [] });
|
|
3548
3647
|
}
|
|
3549
3648
|
}
|
|
3550
|
-
|
|
3649
|
+
const orchestrator = new ExportOrchestrator(projectRoot);
|
|
3650
|
+
await orchestrator.writeBundle(dossier, {
|
|
3651
|
+
format: options.format,
|
|
3652
|
+
budget: options.budget ? parseInt(options.budget, 10) : void 0,
|
|
3653
|
+
scope: options.scope
|
|
3654
|
+
}, result.store, result.graph);
|
|
3551
3655
|
startWatcher(projectRoot, result.files.map((f) => f.path));
|
|
3552
3656
|
console.error(`[vibe-splain] Scan complete. ${result.totalFilesScanned} files, ${result.realSourceCount} real-source, ${result.wildCandidates.length} wild candidates.`);
|
|
3553
3657
|
const validation = result.validation ?? { passed: true, errors: 0, warnings: 0, reportPath: ".vibe-splainer/validation_report.json" };
|
|
3658
|
+
let statusMsg = "Scan complete.";
|
|
3659
|
+
if (!validation.passed) {
|
|
3660
|
+
statusMsg = `SCAN QUALITY WARNING: ${validation.errors} errors and ${validation.warnings} warnings found in validation report. Delta Engine automation may be blocked.`;
|
|
3661
|
+
}
|
|
3554
3662
|
return {
|
|
3555
3663
|
ok: true,
|
|
3664
|
+
message: statusMsg,
|
|
3556
3665
|
validation: {
|
|
3557
3666
|
passed: validation.passed,
|
|
3558
3667
|
errors: validation.errors,
|
|
@@ -3636,7 +3745,7 @@ var setProjectBriefTool = {
|
|
|
3636
3745
|
required: ["projectRoot", "brief"]
|
|
3637
3746
|
}
|
|
3638
3747
|
};
|
|
3639
|
-
async function handleSetProjectBrief(args) {
|
|
3748
|
+
async function handleSetProjectBrief(args, options = {}) {
|
|
3640
3749
|
const projectRoot = args.projectRoot;
|
|
3641
3750
|
const brief = args.brief;
|
|
3642
3751
|
if (!projectRoot || !brief)
|
|
@@ -3646,7 +3755,12 @@ async function handleSetProjectBrief(args) {
|
|
|
3646
3755
|
return { error: "No project map found. Run scan_project first." };
|
|
3647
3756
|
}
|
|
3648
3757
|
dossier.map.brief = brief;
|
|
3649
|
-
|
|
3758
|
+
const orchestrator = new ExportOrchestrator(projectRoot);
|
|
3759
|
+
await orchestrator.writeBundle(dossier, {
|
|
3760
|
+
format: options.format,
|
|
3761
|
+
budget: options.budget ? parseInt(options.budget, 10) : void 0,
|
|
3762
|
+
scope: options.scope
|
|
3763
|
+
});
|
|
3650
3764
|
const documented = new Set([...dossier.pillars.flatMap((p) => p.decisions), ...dossier.wildDiscoveries].map((c) => c.primaryFile).filter(Boolean));
|
|
3651
3765
|
const startHere = dossier.map.topGravity.filter((f) => !documented.has(f));
|
|
3652
3766
|
const wild = dossier.map.topHeat.filter((f) => !documented.has(f));
|
|
@@ -3661,8 +3775,8 @@ async function handleSetProjectBrief(args) {
|
|
|
3661
3775
|
}
|
|
3662
3776
|
|
|
3663
3777
|
// dist/mcp/tools/get_file_context.js
|
|
3664
|
-
import { readFile as
|
|
3665
|
-
import { join as
|
|
3778
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
3779
|
+
import { join as join13, relative as relative4, isAbsolute } from "path";
|
|
3666
3780
|
var getFileContextTool = {
|
|
3667
3781
|
name: "get_file_context",
|
|
3668
3782
|
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.",
|
|
@@ -3682,8 +3796,8 @@ async function handleGetFileContext(args) {
|
|
|
3682
3796
|
const full = args.full === true;
|
|
3683
3797
|
if (!projectRoot || !filePath)
|
|
3684
3798
|
throw new Error("projectRoot and filePath are required");
|
|
3685
|
-
const fullPath = isAbsolute(filePath) ? filePath :
|
|
3686
|
-
const relPath =
|
|
3799
|
+
const fullPath = isAbsolute(filePath) ? filePath : join13(projectRoot, filePath);
|
|
3800
|
+
const relPath = relative4(projectRoot, fullPath);
|
|
3687
3801
|
const evidence = await getFileAnalysis(fullPath);
|
|
3688
3802
|
if (!evidence) {
|
|
3689
3803
|
throw new Error(`Could not analyze ${relPath} (unsupported language or parse failure).`);
|
|
@@ -3707,7 +3821,7 @@ async function handleGetFileContext(args) {
|
|
|
3707
3821
|
smellSpans: evidence.smellSpans
|
|
3708
3822
|
};
|
|
3709
3823
|
if (full) {
|
|
3710
|
-
result.source = await
|
|
3824
|
+
result.source = await readFile10(fullPath, "utf8");
|
|
3711
3825
|
}
|
|
3712
3826
|
return result;
|
|
3713
3827
|
}
|
|
@@ -3715,8 +3829,8 @@ async function handleGetFileContext(args) {
|
|
|
3715
3829
|
// dist/mcp/tools/write_decision_card.js
|
|
3716
3830
|
import { v4 as uuidv4 } from "uuid";
|
|
3717
3831
|
import { createHash as createHash3 } from "crypto";
|
|
3718
|
-
import { readFile as
|
|
3719
|
-
import { join as
|
|
3832
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
3833
|
+
import { join as join14 } from "path";
|
|
3720
3834
|
var CATEGORIES = ["Bottleneck", "Hack", "Smart-Move", "Risk", "Convention", "Dead-Weight"];
|
|
3721
3835
|
function normalizeSnippet(s) {
|
|
3722
3836
|
let out = (s ?? "").replace(/\r\n/g, "\n");
|
|
@@ -3761,7 +3875,7 @@ var writeDecisionCardTool = {
|
|
|
3761
3875
|
required: ["projectRoot", "pillar", "primaryFile", "title", "thesis", "category", "severity", "narrative", "confidence", "evidence"]
|
|
3762
3876
|
}
|
|
3763
3877
|
};
|
|
3764
|
-
async function handleWriteDecisionCard(args) {
|
|
3878
|
+
async function handleWriteDecisionCard(args, options = {}) {
|
|
3765
3879
|
const projectRoot = args.projectRoot;
|
|
3766
3880
|
const pillar = args.pillar;
|
|
3767
3881
|
const primaryFile = args.primaryFile;
|
|
@@ -3804,11 +3918,16 @@ async function handleWriteDecisionCard(args) {
|
|
|
3804
3918
|
}
|
|
3805
3919
|
const store = await readAnalysis(projectRoot);
|
|
3806
3920
|
const persisted = store?.files[primaryFile];
|
|
3921
|
+
const machineConfidence = persisted?.confidence || "low";
|
|
3922
|
+
const confidenceOrder = { low: 0, medium: 1, high: 2 };
|
|
3923
|
+
if (confidenceOrder[confidence] > confidenceOrder[machineConfidence]) {
|
|
3924
|
+
throw new Error(`Requested confidence "${confidence}" exceeds machine-derived confidence "${machineConfidence}" for this file. Ground your narrative in the MRI evidence.`);
|
|
3925
|
+
}
|
|
3807
3926
|
const gravity = persisted ? Math.round(persisted.gravity) : void 0;
|
|
3808
3927
|
const heat = persisted ? Math.round(persisted.heat) : void 0;
|
|
3809
3928
|
let primaryContent = "";
|
|
3810
3929
|
try {
|
|
3811
|
-
primaryContent = await
|
|
3930
|
+
primaryContent = await readFile11(join14(projectRoot, primaryFile), "utf8");
|
|
3812
3931
|
} catch {
|
|
3813
3932
|
}
|
|
3814
3933
|
const hash = createHash3("sha256").update(primaryContent).digest("hex");
|
|
@@ -3842,7 +3961,12 @@ async function handleWriteDecisionCard(args) {
|
|
|
3842
3961
|
}
|
|
3843
3962
|
bucket.decisions.push(card);
|
|
3844
3963
|
bucket.cardCount = bucket.decisions.length;
|
|
3845
|
-
|
|
3964
|
+
const orchestrator = new ExportOrchestrator(projectRoot);
|
|
3965
|
+
await orchestrator.writeBundle(dossier, {
|
|
3966
|
+
format: options.format,
|
|
3967
|
+
budget: options.budget ? parseInt(options.budget, 10) : void 0,
|
|
3968
|
+
scope: options.scope
|
|
3969
|
+
});
|
|
3846
3970
|
console.error(`[vibe-splain] Card written: "${title}" [${category} sev${severity}] in "${pillar}"${isWild ? " (Wild Discovery)" : ""}`);
|
|
3847
3971
|
const documented = new Set([...dossier.pillars.flatMap((p) => p.decisions), ...dossier.wildDiscoveries].map((c) => c.primaryFile).filter(Boolean));
|
|
3848
3972
|
const remaining = [.../* @__PURE__ */ new Set([...dossier.map.topGravity, ...dossier.map.topHeat])].filter((f) => !documented.has(f));
|
|
@@ -3992,7 +4116,7 @@ var markStaleTool = {
|
|
|
3992
4116
|
required: ["projectRoot", "filePaths"]
|
|
3993
4117
|
}
|
|
3994
4118
|
};
|
|
3995
|
-
async function handleMarkStale(args) {
|
|
4119
|
+
async function handleMarkStale(args, options = {}) {
|
|
3996
4120
|
const projectRoot = args.projectRoot;
|
|
3997
4121
|
const filePaths = args.filePaths;
|
|
3998
4122
|
if (!projectRoot || !filePaths)
|
|
@@ -4020,7 +4144,12 @@ async function handleMarkStale(args) {
|
|
|
4020
4144
|
dossier.stalePaths.push(filePath);
|
|
4021
4145
|
}
|
|
4022
4146
|
}
|
|
4023
|
-
|
|
4147
|
+
const orchestrator = new ExportOrchestrator(projectRoot);
|
|
4148
|
+
await orchestrator.writeBundle(dossier, {
|
|
4149
|
+
format: options.format,
|
|
4150
|
+
budget: options.budget ? parseInt(options.budget, 10) : void 0,
|
|
4151
|
+
scope: options.scope
|
|
4152
|
+
});
|
|
4024
4153
|
return {
|
|
4025
4154
|
success: true,
|
|
4026
4155
|
staleCardsMarked: staleCount,
|
|
@@ -4102,10 +4231,10 @@ var TOOL_HANDLERS = {
|
|
|
4102
4231
|
get_wild_discoveries: handleGetWildDiscoveries,
|
|
4103
4232
|
mark_stale: handleMarkStale
|
|
4104
4233
|
};
|
|
4105
|
-
async function startMCPServer() {
|
|
4234
|
+
async function startMCPServer(options = {}) {
|
|
4106
4235
|
await initParser2();
|
|
4107
4236
|
console.error("[vibe-splain] Tree-Sitter parser initialized");
|
|
4108
|
-
const server = new Server({ name: "vibe-splain", version: "
|
|
4237
|
+
const server = new Server({ name: "vibe-splain", version: "3.0.0" }, { capabilities: { tools: {}, prompts: {} } });
|
|
4109
4238
|
server.setRequestHandler(ListPromptsRequestSchema, async () => ({
|
|
4110
4239
|
prompts: [
|
|
4111
4240
|
{
|
|
@@ -4202,7 +4331,7 @@ When done, share the exact file:// UI link returned by scan_project. Never inven
|
|
|
4202
4331
|
};
|
|
4203
4332
|
}
|
|
4204
4333
|
try {
|
|
4205
|
-
const result = await handler(args
|
|
4334
|
+
const result = await handler(args, options);
|
|
4206
4335
|
return {
|
|
4207
4336
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
4208
4337
|
};
|
|
@@ -4221,14 +4350,33 @@ When done, share the exact file:// UI link returned by scan_project. Never inven
|
|
|
4221
4350
|
}
|
|
4222
4351
|
|
|
4223
4352
|
// dist/commands/serve.js
|
|
4224
|
-
async function serveCommand() {
|
|
4353
|
+
async function serveCommand(options) {
|
|
4225
4354
|
console.error("[vibe-splain] Starting MCP server...");
|
|
4226
|
-
await startMCPServer();
|
|
4355
|
+
await startMCPServer(options);
|
|
4356
|
+
}
|
|
4357
|
+
|
|
4358
|
+
// dist/commands/export.js
|
|
4359
|
+
async function exportCommand(projectRoot, options) {
|
|
4360
|
+
const root = projectRoot || process.cwd();
|
|
4361
|
+
console.error(`[vibe-splain] Exporting dossier for ${root}`);
|
|
4362
|
+
const dossier = await readDossier(root);
|
|
4363
|
+
if (!dossier) {
|
|
4364
|
+
console.error("[vibe-splain] Dossier not found. Run scan first.");
|
|
4365
|
+
process.exit(1);
|
|
4366
|
+
}
|
|
4367
|
+
const orchestrator = new ExportOrchestrator(root);
|
|
4368
|
+
await orchestrator.writeBundle(dossier, {
|
|
4369
|
+
format: options.format,
|
|
4370
|
+
budget: options.budget ? parseInt(options.budget, 10) : void 0,
|
|
4371
|
+
scope: options.scope
|
|
4372
|
+
});
|
|
4373
|
+
console.error("[vibe-splain] Export complete.");
|
|
4227
4374
|
}
|
|
4228
4375
|
|
|
4229
4376
|
// dist/index.js
|
|
4230
4377
|
var program = new Command();
|
|
4231
|
-
program.name("vibe-splain").description("Architectural dossier engine for vibe-coded projects").version("
|
|
4378
|
+
program.name("vibe-splain").description("Architectural dossier engine for vibe-coded projects").version("3.0.0");
|
|
4232
4379
|
program.command("install").description("Patch coding agent MCP config files to register vibe-splain").action(installCommand);
|
|
4233
|
-
program.command("serve").description("Start the MCP server (called by the coding agent, not by you)").action(serveCommand);
|
|
4380
|
+
program.command("serve").description("Start the MCP server (called by the coding agent, not by you)").option("--format <format>", "Export format (html, markdown, etc.)").option("--budget <budget>", "Token budget for markdown").option("--scope <scope>", "Scope for export").action((options) => serveCommand(options));
|
|
4381
|
+
program.command("export [projectRoot]").description("Manually trigger bundle generation").option("--format <format>", "Export format (html, markdown, etc.)").option("--budget <budget>", "Token budget for markdown").option("--scope <scope>", "Scope for export").action(exportCommand);
|
|
4234
4382
|
program.parse();
|