vibe-splain 2.7.3 → 3.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/dist/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 +74 -0
- package/dist/export/ExportOrchestrator.d.ts +12 -0
- package/dist/export/ExportOrchestrator.js +73 -0
- package/dist/export/Watcher.d.ts +1 -0
- package/dist/export/Watcher.js +55 -0
- package/dist/export/renderers/AgentMarkdownRenderer.d.ts +9 -0
- package/dist/export/renderers/AgentMarkdownRenderer.js +106 -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 +77 -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 +888 -660
- 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 +25 -5
- 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",
|
|
@@ -2276,7 +2276,8 @@ async function runActionBinding(projectRoot, inv, res) {
|
|
|
2276
2276
|
const startLine = node.startPosition.row + 1;
|
|
2277
2277
|
const startCol = node.startPosition.column;
|
|
2278
2278
|
const endLine = node.endPosition.row + 1;
|
|
2279
|
-
const
|
|
2279
|
+
const endCol = node.endPosition.column;
|
|
2280
|
+
const isDuplicate = functions.some((f) => f.startLine === startLine && f.startCol === startCol && f.endLine === endLine && f.functionKind === node.type);
|
|
2280
2281
|
if (isDuplicate)
|
|
2281
2282
|
continue;
|
|
2282
2283
|
functionsExtracted++;
|
|
@@ -2538,19 +2539,36 @@ async function runActionBinding(projectRoot, inv, res) {
|
|
|
2538
2539
|
for (const fileRec of Object.values(artifact.files)) {
|
|
2539
2540
|
for (const fnRec of fileRec.functions) {
|
|
2540
2541
|
for (const callRec of fnRec.calls) {
|
|
2541
|
-
if (callRec.
|
|
2542
|
-
|
|
2543
|
-
|
|
2544
|
-
|
|
2545
|
-
|
|
2546
|
-
|
|
2547
|
-
|
|
2542
|
+
if (callRec.resolvedTargetFunctionId)
|
|
2543
|
+
continue;
|
|
2544
|
+
if (!callRec.resolvedFilePath)
|
|
2545
|
+
continue;
|
|
2546
|
+
const targetFile = artifact.files[callRec.resolvedFilePath];
|
|
2547
|
+
if (!targetFile)
|
|
2548
|
+
continue;
|
|
2549
|
+
if (callRec.resolutionKind === "named_import_match") {
|
|
2550
|
+
const targetFn = targetFile.functions.find((f) => f.displayName === callRec.calleeRoot && f.isExported);
|
|
2551
|
+
if (targetFn) {
|
|
2552
|
+
callRec.resolvedTargetFunctionId = targetFn.functionId;
|
|
2553
|
+
callRec.confidence = "high";
|
|
2554
|
+
}
|
|
2555
|
+
} else if (callRec.resolutionKind === "namespace_import_property" && callRec.calleeProperty) {
|
|
2556
|
+
const targetFn = targetFile.functions.find((f) => f.displayName === callRec.calleeProperty && f.isExported);
|
|
2557
|
+
if (targetFn) {
|
|
2558
|
+
callRec.resolvedTargetFunctionId = targetFn.functionId;
|
|
2559
|
+
callRec.confidence = "high";
|
|
2560
|
+
}
|
|
2561
|
+
} else if (callRec.resolutionKind === "namespace_import_property" && !callRec.calleeProperty) {
|
|
2562
|
+
const defaultFn = targetFile.functions.find((f) => f.displayName === "default");
|
|
2563
|
+
if (defaultFn) {
|
|
2564
|
+
callRec.resolvedTargetFunctionId = defaultFn.functionId;
|
|
2565
|
+
callRec.confidence = "high";
|
|
2548
2566
|
}
|
|
2549
2567
|
}
|
|
2550
2568
|
}
|
|
2551
2569
|
}
|
|
2552
2570
|
}
|
|
2553
|
-
await
|
|
2571
|
+
await writeFile5(join5(projectRoot, ".vibe-splainer", "action_bindings.json"), JSON.stringify(artifact, null, 2), "utf8");
|
|
2554
2572
|
const summary = {
|
|
2555
2573
|
filesProcessed,
|
|
2556
2574
|
functionsExtracted,
|
|
@@ -2560,14 +2578,14 @@ async function runActionBinding(projectRoot, inv, res) {
|
|
|
2560
2578
|
entrypointsFound,
|
|
2561
2579
|
namedImportsExtracted
|
|
2562
2580
|
};
|
|
2563
|
-
await
|
|
2581
|
+
await writeFile5(join5(projectRoot, ".vibe-splainer", "stage-09-action-bindings-summary.json"), JSON.stringify(summary, null, 2), "utf8");
|
|
2564
2582
|
return { artifact };
|
|
2565
2583
|
}
|
|
2566
2584
|
async function traverseCallChain(projectRoot, args) {
|
|
2567
|
-
const artifactPath =
|
|
2585
|
+
const artifactPath = join5(projectRoot, ".vibe-splainer", "action_bindings.json");
|
|
2568
2586
|
let artifact;
|
|
2569
2587
|
try {
|
|
2570
|
-
const raw = await
|
|
2588
|
+
const raw = await readFile4(artifactPath, "utf8");
|
|
2571
2589
|
artifact = JSON.parse(raw);
|
|
2572
2590
|
} catch {
|
|
2573
2591
|
throw new Error("action_bindings.json not found. Run scan_project first.");
|
|
@@ -2591,14 +2609,15 @@ async function traverseCallChain(projectRoot, args) {
|
|
|
2591
2609
|
const chain = [];
|
|
2592
2610
|
const unresolvedEdges = [];
|
|
2593
2611
|
const visited = /* @__PURE__ */ new Set();
|
|
2594
|
-
const queue = seedFunctionIds.map((id) => ({ functionId: id, depth: 0 }));
|
|
2612
|
+
const queue = seedFunctionIds.map((id) => ({ functionId: id, callerFunctionId: null, depth: 0 }));
|
|
2595
2613
|
let targetReached = false;
|
|
2596
2614
|
let truncatedAtDepth = false;
|
|
2597
2615
|
while (queue.length > 0) {
|
|
2598
|
-
const { functionId, depth } = queue.shift();
|
|
2599
|
-
|
|
2616
|
+
const { functionId, callerFunctionId, depth, callsite } = queue.shift();
|
|
2617
|
+
const visitKey = `${callerFunctionId}->${functionId}`;
|
|
2618
|
+
if (visited.has(visitKey))
|
|
2600
2619
|
continue;
|
|
2601
|
-
visited.add(
|
|
2620
|
+
visited.add(visitKey);
|
|
2602
2621
|
const indexEntry = artifact.functionIndex[functionId];
|
|
2603
2622
|
if (!indexEntry)
|
|
2604
2623
|
continue;
|
|
@@ -2610,63 +2629,42 @@ async function traverseCallChain(projectRoot, args) {
|
|
|
2610
2629
|
const fnRec = fileRec.functions.find((f) => f.functionId === functionId);
|
|
2611
2630
|
if (!fnRec)
|
|
2612
2631
|
continue;
|
|
2613
|
-
|
|
2614
|
-
|
|
2615
|
-
|
|
2616
|
-
|
|
2617
|
-
|
|
2618
|
-
|
|
2619
|
-
|
|
2620
|
-
|
|
2621
|
-
|
|
2622
|
-
|
|
2623
|
-
|
|
2624
|
-
|
|
2625
|
-
|
|
2626
|
-
|
|
2627
|
-
|
|
2628
|
-
|
|
2629
|
-
|
|
2630
|
-
|
|
2631
|
-
evidenceText: call.evidenceText,
|
|
2632
|
-
isTarget,
|
|
2633
|
-
depth
|
|
2634
|
-
});
|
|
2635
|
-
} else {
|
|
2636
|
-
unresolvedEdges.push({
|
|
2637
|
-
fromFunctionId: functionId,
|
|
2638
|
-
calleeText: call.calleeText,
|
|
2639
|
-
sourceLine: call.sourceLine,
|
|
2640
|
-
reason: "depth limit reached"
|
|
2641
|
-
});
|
|
2642
|
-
truncatedAtDepth = true;
|
|
2643
|
-
}
|
|
2644
|
-
} else {
|
|
2645
|
-
unresolvedEdges.push({
|
|
2646
|
-
fromFunctionId: functionId,
|
|
2647
|
-
calleeText: call.calleeText,
|
|
2648
|
-
sourceLine: call.sourceLine,
|
|
2649
|
-
reason: call.resolutionKind
|
|
2650
|
-
});
|
|
2651
|
-
}
|
|
2652
|
-
}
|
|
2632
|
+
let isTarget = false;
|
|
2633
|
+
if (targetFunctionName && fnRec.displayName === targetFunctionName)
|
|
2634
|
+
isTarget = true;
|
|
2635
|
+
if (isTarget)
|
|
2636
|
+
targetReached = true;
|
|
2637
|
+
chain.push({
|
|
2638
|
+
functionId,
|
|
2639
|
+
callerFunctionId,
|
|
2640
|
+
displayName: fnRec.displayName,
|
|
2641
|
+
filePath: fnRec.filePath,
|
|
2642
|
+
startLine: fnRec.startLine,
|
|
2643
|
+
edgeKind: "call_edge",
|
|
2644
|
+
confidence: "high",
|
|
2645
|
+
evidenceText: fnRec.evidenceText,
|
|
2646
|
+
isTarget,
|
|
2647
|
+
depth,
|
|
2648
|
+
callsite
|
|
2649
|
+
});
|
|
2653
2650
|
for (const action of fnRec.semanticActions) {
|
|
2654
|
-
let
|
|
2651
|
+
let isActionTarget = false;
|
|
2655
2652
|
if (targetActionKind && action.actionKind === targetActionKind) {
|
|
2656
|
-
|
|
2653
|
+
isActionTarget = true;
|
|
2657
2654
|
if (targetModel && action.targetModel !== targetModel)
|
|
2658
|
-
|
|
2655
|
+
isActionTarget = false;
|
|
2659
2656
|
if (targetOperation && action.targetOperation !== targetOperation)
|
|
2660
|
-
|
|
2657
|
+
isActionTarget = false;
|
|
2661
2658
|
} else if (targetModel && action.targetModel === targetModel) {
|
|
2662
|
-
|
|
2659
|
+
isActionTarget = true;
|
|
2663
2660
|
if (targetOperation && action.targetOperation !== targetOperation)
|
|
2664
|
-
|
|
2661
|
+
isActionTarget = false;
|
|
2665
2662
|
}
|
|
2666
|
-
if (
|
|
2663
|
+
if (isActionTarget)
|
|
2667
2664
|
targetReached = true;
|
|
2668
2665
|
chain.push({
|
|
2669
|
-
functionId: action.
|
|
2666
|
+
functionId: action.actionId,
|
|
2667
|
+
callerFunctionId: functionId,
|
|
2670
2668
|
displayName: action.calleeText,
|
|
2671
2669
|
filePath: fileRec.filePath,
|
|
2672
2670
|
startLine: action.sourceLine,
|
|
@@ -2676,10 +2674,35 @@ async function traverseCallChain(projectRoot, args) {
|
|
|
2676
2674
|
targetOperation: action.targetOperation || void 0,
|
|
2677
2675
|
confidence: action.confidence,
|
|
2678
2676
|
evidenceText: action.evidenceText,
|
|
2679
|
-
isTarget,
|
|
2680
|
-
depth
|
|
2677
|
+
isTarget: isActionTarget,
|
|
2678
|
+
depth: depth + 1
|
|
2681
2679
|
});
|
|
2682
2680
|
}
|
|
2681
|
+
if (depth < maxDepth) {
|
|
2682
|
+
for (const call of fnRec.calls) {
|
|
2683
|
+
if (call.resolvedTargetFunctionId) {
|
|
2684
|
+
queue.push({
|
|
2685
|
+
functionId: call.resolvedTargetFunctionId,
|
|
2686
|
+
callerFunctionId: functionId,
|
|
2687
|
+
depth: depth + 1,
|
|
2688
|
+
callsite: {
|
|
2689
|
+
file: fileRec.filePath,
|
|
2690
|
+
line: call.sourceLine,
|
|
2691
|
+
text: call.calleeText
|
|
2692
|
+
}
|
|
2693
|
+
});
|
|
2694
|
+
} else {
|
|
2695
|
+
unresolvedEdges.push({
|
|
2696
|
+
fromFunctionId: functionId,
|
|
2697
|
+
calleeText: call.calleeText,
|
|
2698
|
+
sourceLine: call.sourceLine,
|
|
2699
|
+
reason: call.resolutionKind
|
|
2700
|
+
});
|
|
2701
|
+
}
|
|
2702
|
+
}
|
|
2703
|
+
} else if (fnRec.calls.length > 0) {
|
|
2704
|
+
truncatedAtDepth = true;
|
|
2705
|
+
}
|
|
2683
2706
|
}
|
|
2684
2707
|
return {
|
|
2685
2708
|
targetReached,
|
|
@@ -2690,9 +2713,8 @@ async function traverseCallChain(projectRoot, args) {
|
|
|
2690
2713
|
}
|
|
2691
2714
|
|
|
2692
2715
|
// ../brain/dist/pipeline/scoring.js
|
|
2693
|
-
import { join as
|
|
2694
|
-
import {
|
|
2695
|
-
import { createHash } from "crypto";
|
|
2716
|
+
import { join as join6 } from "path";
|
|
2717
|
+
import { mkdir as mkdir4, readFile as readFile5 } from "fs/promises";
|
|
2696
2718
|
function computeSeverity(sideEffectProfile, productDomain, gravity, heat, maxNesting, hasLongFunctions, swallowedCatches, runtimeEntrypoints) {
|
|
2697
2719
|
let score = 0;
|
|
2698
2720
|
if (sideEffectProfile.includes("database_write"))
|
|
@@ -2759,149 +2781,6 @@ function applyCorrections(file) {
|
|
|
2759
2781
|
file.canonicalLoadBearing = true;
|
|
2760
2782
|
}
|
|
2761
2783
|
}
|
|
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
2784
|
function deriveConfidence(fanIn, gravity) {
|
|
2906
2785
|
if (fanIn >= 10 && gravity >= 40)
|
|
2907
2786
|
return "high";
|
|
@@ -2910,12 +2789,12 @@ function deriveConfidence(fanIn, gravity) {
|
|
|
2910
2789
|
return "low";
|
|
2911
2790
|
}
|
|
2912
2791
|
async function runScoring(projectRoot, cr, binding) {
|
|
2913
|
-
const dir =
|
|
2914
|
-
await
|
|
2792
|
+
const dir = join6(projectRoot, ".vibe-splainer");
|
|
2793
|
+
await mkdir4(dir, { recursive: true });
|
|
2915
2794
|
let bindingArtifact = binding?.artifact;
|
|
2916
2795
|
if (!bindingArtifact) {
|
|
2917
2796
|
try {
|
|
2918
|
-
const raw = await
|
|
2797
|
+
const raw = await readFile5(join6(projectRoot, ".vibe-splainer", "action_bindings.json"), "utf8");
|
|
2919
2798
|
bindingArtifact = JSON.parse(raw);
|
|
2920
2799
|
} catch {
|
|
2921
2800
|
}
|
|
@@ -2924,7 +2803,7 @@ async function runScoring(projectRoot, cr, binding) {
|
|
|
2924
2803
|
const severityBreakdowns = {};
|
|
2925
2804
|
for (const f of cr.classified) {
|
|
2926
2805
|
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
|
|
2806
|
+
const confidence = deriveConfidence(f.gravitySignals.fanIn, f.gravity);
|
|
2928
2807
|
const pf = {
|
|
2929
2808
|
relativePath: f.rel,
|
|
2930
2809
|
language: f.lang,
|
|
@@ -2946,161 +2825,25 @@ async function runScoring(projectRoot, cr, binding) {
|
|
|
2946
2825
|
riskTypes: f.riskTypes,
|
|
2947
2826
|
writeIntents: f.writeIntents,
|
|
2948
2827
|
canonicalSeverity: severity,
|
|
2949
|
-
canonicalLoadBearing: isLoadBearing
|
|
2828
|
+
canonicalLoadBearing: f.isLoadBearing,
|
|
2829
|
+
// STRICT: fanIn >= 10
|
|
2830
|
+
isOperationallyCritical: f.isOperationallyCritical,
|
|
2831
|
+
confidence
|
|
2950
2832
|
};
|
|
2951
2833
|
applyCorrections(pf);
|
|
2952
2834
|
persisted[f.rel] = pf;
|
|
2953
2835
|
severityBreakdowns[f.rel] = `severity=${pf.canonicalSeverity} loadBearing=${pf.canonicalLoadBearing} effects=${pf.sideEffectProfile.join(",")} domain=${pf.productDomain}`;
|
|
2954
2836
|
}
|
|
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
2837
|
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");
|
|
2838
|
+
const deltaTargets = Object.values(persisted).filter((pf) => pf.isRealSource).sort((a, b) => b.gravity - a.gravity).map((pf) => ({
|
|
2839
|
+
path: pf.relativePath,
|
|
2840
|
+
gravity: Math.round(pf.gravity),
|
|
2841
|
+
isLoadBearing: pf.canonicalLoadBearing,
|
|
2842
|
+
// STRICT: fanIn >= 10
|
|
2843
|
+
blastRadius: pf.importedBy,
|
|
2844
|
+
pillarHint: pf.pillarHint
|
|
2845
|
+
}));
|
|
2846
|
+
const validationReport = await buildValidationReport(store, deltaTargets, projectRoot, cr);
|
|
3104
2847
|
for (const e of validationReport.errors) {
|
|
3105
2848
|
console.error(`[vibe-splain] VALIDATION ERROR [${e.rule}] ${e.file}: ${e.detail}`);
|
|
3106
2849
|
}
|
|
@@ -3109,24 +2852,27 @@ async function runScoring(projectRoot, cr, binding) {
|
|
|
3109
2852
|
}
|
|
3110
2853
|
return { store, deltaTargets, validationReport };
|
|
3111
2854
|
}
|
|
3112
|
-
async function buildValidationReport(store, deltaTargets, projectRoot) {
|
|
2855
|
+
async function buildValidationReport(store, deltaTargets, projectRoot, cr) {
|
|
3113
2856
|
const errors = [];
|
|
3114
2857
|
const warnings = [];
|
|
3115
2858
|
let passCount = 0;
|
|
3116
|
-
|
|
2859
|
+
let tracedCount = 0;
|
|
2860
|
+
let realCount = 0;
|
|
3117
2861
|
for (const [, pf] of Object.entries(store.files)) {
|
|
3118
2862
|
if (!pf.isRealSource)
|
|
3119
2863
|
continue;
|
|
3120
|
-
|
|
3121
|
-
|
|
3122
|
-
|
|
3123
|
-
|
|
3124
|
-
|
|
3125
|
-
|
|
3126
|
-
|
|
3127
|
-
|
|
3128
|
-
|
|
3129
|
-
|
|
2864
|
+
realCount++;
|
|
2865
|
+
const classified = cr.classified.find((f) => f.rel === pf.relativePath);
|
|
2866
|
+
if (classified && classified.entrypointTraceStatus === "complete")
|
|
2867
|
+
tracedCount++;
|
|
2868
|
+
if (pf.canonicalSeverity === 5 && !pf.canonicalLoadBearing && pf.gravitySignals.fanIn < 10) {
|
|
2869
|
+
if (!pf.isOperationallyCritical) {
|
|
2870
|
+
errors.push({
|
|
2871
|
+
file: pf.relativePath,
|
|
2872
|
+
rule: "severity_5_no_criticality",
|
|
2873
|
+
detail: "severity=5 but not load-bearing and not operationally critical"
|
|
2874
|
+
});
|
|
2875
|
+
}
|
|
3130
2876
|
}
|
|
3131
2877
|
if (pf.writeIntents.includes("handle_payment_webhook") && pf.sideEffectProfile.includes("none_detected")) {
|
|
3132
2878
|
errors.push({
|
|
@@ -3138,7 +2884,7 @@ async function buildValidationReport(store, deltaTargets, projectRoot) {
|
|
|
3138
2884
|
});
|
|
3139
2885
|
continue;
|
|
3140
2886
|
}
|
|
3141
|
-
if (pf.productDomain === "booking_creation" &&
|
|
2887
|
+
if (pf.productDomain === "booking_creation" && classified?.entrypointTraceStatus === "no_runtime_entrypoint_found" && pf.importsUnresolved.length === 0) {
|
|
3142
2888
|
errors.push({
|
|
3143
2889
|
file: pf.relativePath,
|
|
3144
2890
|
rule: "booking_creation_no_entrypoint_no_blockers",
|
|
@@ -3146,135 +2892,65 @@ async function buildValidationReport(store, deltaTargets, projectRoot) {
|
|
|
3146
2892
|
});
|
|
3147
2893
|
continue;
|
|
3148
2894
|
}
|
|
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) {
|
|
2895
|
+
if (pf.canonicalSeverity >= 4 && pf.hotSpans.length === 0) {
|
|
3160
2896
|
errors.push({
|
|
3161
2897
|
file: pf.relativePath,
|
|
3162
2898
|
rule: "high_severity_no_evidence",
|
|
3163
|
-
detail: `severity=${pf.canonicalSeverity} but
|
|
2899
|
+
detail: `severity=${pf.canonicalSeverity} but no evidence hotSpans found`
|
|
3164
2900
|
});
|
|
3165
2901
|
continue;
|
|
3166
2902
|
}
|
|
3167
|
-
if (pf.canonicalSeverity >= 4 && (
|
|
2903
|
+
if (pf.canonicalSeverity >= 4 && (classified?.runtimeEntrypoints.length ?? 0) === 0) {
|
|
3168
2904
|
warnings.push({
|
|
3169
2905
|
file: pf.relativePath,
|
|
3170
2906
|
rule: "high_severity_no_entrypoints",
|
|
3171
2907
|
detail: `severity=${pf.canonicalSeverity} but no runtime entrypoints found \u2014 check alias resolution`
|
|
3172
2908
|
});
|
|
3173
2909
|
}
|
|
3174
|
-
if (
|
|
3175
|
-
const foundPaths =
|
|
2910
|
+
if (classified?.entrypointTraceStatus === "partial_wrong_surface") {
|
|
2911
|
+
const foundPaths = classified.runtimeEntrypoints.map((e) => e.path).join(", ");
|
|
3176
2912
|
warnings.push({
|
|
3177
2913
|
file: pf.relativePath,
|
|
3178
2914
|
rule: "partial_wrong_surface",
|
|
3179
2915
|
detail: `Entrypoints found but domain surface mismatch for ${pf.productDomain}. Found: ${foundPaths}`
|
|
3180
2916
|
});
|
|
3181
2917
|
}
|
|
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
2918
|
passCount++;
|
|
3216
2919
|
}
|
|
3217
2920
|
const PAYMENT_PROVIDER_PATH_TERMS = ["stripe", "paypal", "btcpay", "btcpayserver", "alby", "hitpay", "payment"];
|
|
3218
|
-
const PAYMENT_CONTENT_TERMS = [
|
|
3219
|
-
"constructEvent",
|
|
3220
|
-
"checkoutSession",
|
|
3221
|
-
"paymentIntent",
|
|
3222
|
-
"stripe-signature",
|
|
3223
|
-
"webhook-signature",
|
|
3224
|
-
"payment_mutation",
|
|
3225
|
-
"paymentStatus",
|
|
3226
|
-
"invoicePaid",
|
|
3227
|
-
"chargeSucceeded"
|
|
3228
|
-
];
|
|
3229
2921
|
for (const [rel, pf] of Object.entries(store.files)) {
|
|
3230
2922
|
if (!pf.isRealSource)
|
|
3231
2923
|
continue;
|
|
3232
|
-
const
|
|
3233
|
-
|
|
2924
|
+
const hasIntent = pf.writeIntents.includes("handle_payment_webhook");
|
|
2925
|
+
const hasEffects = pf.sideEffectProfile.includes("webhook_ingress") || pf.sideEffectProfile.includes("payment_mutation");
|
|
2926
|
+
const pathMentionsPayment = PAYMENT_PROVIDER_PATH_TERMS.some((t) => rel.toLowerCase().includes(t));
|
|
2927
|
+
if (!hasIntent && !(hasEffects && pathMentionsPayment))
|
|
3234
2928
|
continue;
|
|
3235
|
-
const primaryTrigger = PAYMENT_PROVIDER_PATH_TERMS.some((t) => pathLower.includes(t));
|
|
3236
|
-
let secondaryTrigger = false;
|
|
3237
|
-
if (!primaryTrigger && pf.productDomain !== "payments_webhooks") {
|
|
3238
|
-
try {
|
|
3239
|
-
const src = await readFile7(join8(projectRoot, rel), "utf8");
|
|
3240
|
-
secondaryTrigger = PAYMENT_CONTENT_TERMS.some((t) => src.includes(t));
|
|
3241
|
-
} catch {
|
|
3242
|
-
}
|
|
3243
|
-
}
|
|
3244
|
-
if (!primaryTrigger && !secondaryTrigger)
|
|
3245
|
-
continue;
|
|
3246
|
-
const delta = deltaByPath.get(rel);
|
|
3247
|
-
const triggerLabel = primaryTrigger ? "path" : "content";
|
|
3248
2929
|
const webhookChecks = [
|
|
3249
2930
|
[
|
|
3250
2931
|
pf.productDomain !== "payments_webhooks",
|
|
3251
2932
|
"webhook_domain",
|
|
3252
|
-
`Payment webhook
|
|
2933
|
+
`Payment webhook not classified as payments_webhooks`
|
|
3253
2934
|
],
|
|
3254
2935
|
[
|
|
3255
2936
|
!pf.sideEffectProfile.includes("webhook_ingress"),
|
|
3256
2937
|
"webhook_ingress_missing",
|
|
3257
|
-
`Payment webhook
|
|
2938
|
+
`Payment webhook missing webhook_ingress side effect`
|
|
3258
2939
|
],
|
|
3259
2940
|
[
|
|
3260
2941
|
!pf.sideEffectProfile.includes("payment_mutation"),
|
|
3261
2942
|
"webhook_payment_mutation_missing",
|
|
3262
|
-
`Payment webhook
|
|
2943
|
+
`Payment webhook missing payment_mutation side effect`
|
|
3263
2944
|
],
|
|
3264
2945
|
[
|
|
3265
2946
|
!pf.writeIntents.includes("handle_payment_webhook"),
|
|
3266
2947
|
"webhook_write_intent_missing",
|
|
3267
|
-
`Payment webhook
|
|
2948
|
+
`Payment webhook missing handle_payment_webhook write intent`
|
|
3268
2949
|
],
|
|
3269
2950
|
[
|
|
3270
|
-
|
|
3271
|
-
"
|
|
3272
|
-
`Payment webhook
|
|
3273
|
-
],
|
|
3274
|
-
[
|
|
3275
|
-
!pf.canonicalLoadBearing,
|
|
3276
|
-
"webhook_load_bearing",
|
|
3277
|
-
`Payment webhook (${triggerLabel} trigger) must be load-bearing`
|
|
2951
|
+
!pf.isOperationallyCritical,
|
|
2952
|
+
"webhook_criticality",
|
|
2953
|
+
`Payment webhook must be operationally critical`
|
|
3278
2954
|
]
|
|
3279
2955
|
];
|
|
3280
2956
|
for (const [condition, rule, detail] of webhookChecks) {
|
|
@@ -3282,12 +2958,13 @@ async function buildValidationReport(store, deltaTargets, projectRoot) {
|
|
|
3282
2958
|
errors.push({ file: rel, rule, detail });
|
|
3283
2959
|
}
|
|
3284
2960
|
}
|
|
2961
|
+
const coverage = realCount > 0 ? Math.round(tracedCount / realCount * 100) : 0;
|
|
3285
2962
|
return {
|
|
3286
2963
|
timestamp: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3287
2964
|
passed: errors.length === 0,
|
|
3288
2965
|
errors,
|
|
3289
2966
|
warnings,
|
|
3290
|
-
summary: { errorCount: errors.length, warningCount: warnings.length, passCount }
|
|
2967
|
+
summary: { errorCount: errors.length, warningCount: warnings.length, passCount, entrypointTraceCoverage: coverage }
|
|
3291
2968
|
};
|
|
3292
2969
|
}
|
|
3293
2970
|
|
|
@@ -3298,8 +2975,6 @@ async function runPipeline(projectRoot) {
|
|
|
3298
2975
|
const binding = await runActionBinding(projectRoot, inv, res);
|
|
3299
2976
|
const cr = await runClassification(projectRoot, inv, res);
|
|
3300
2977
|
const scoring = await runScoring(projectRoot, cr, binding);
|
|
3301
|
-
await writeGraph(projectRoot, res.graph);
|
|
3302
|
-
await writeAnalysis(projectRoot, scoring.store);
|
|
3303
2978
|
const files = cr.classified.filter((f) => f.isRealSource).sort((a, b) => b.gravity - a.gravity).map((f) => ({
|
|
3304
2979
|
path: f.abs,
|
|
3305
2980
|
relativePath: f.rel,
|
|
@@ -3332,7 +3007,7 @@ async function runPipeline(projectRoot) {
|
|
|
3332
3007
|
productDomain: f.productDomain,
|
|
3333
3008
|
sideEffectProfile: f.sideEffectProfile
|
|
3334
3009
|
}));
|
|
3335
|
-
const uiUrl = `file://${
|
|
3010
|
+
const uiUrl = `file://${join7(projectRoot, ".vibe-splainer", "ui", "index.html")}`;
|
|
3336
3011
|
return {
|
|
3337
3012
|
projectRoot,
|
|
3338
3013
|
totalFilesScanned: cr.classified.length,
|
|
@@ -3342,6 +3017,7 @@ async function runPipeline(projectRoot) {
|
|
|
3342
3017
|
wildCandidates,
|
|
3343
3018
|
uiUrl,
|
|
3344
3019
|
graph: res.graph,
|
|
3020
|
+
store: scoring.store,
|
|
3345
3021
|
validation: {
|
|
3346
3022
|
passed: scoring.validationReport.passed,
|
|
3347
3023
|
errors: scoring.validationReport.summary.errorCount,
|
|
@@ -3365,7 +3041,7 @@ async function getFileAnalysis(absPath) {
|
|
|
3365
3041
|
return null;
|
|
3366
3042
|
let source;
|
|
3367
3043
|
try {
|
|
3368
|
-
source = await
|
|
3044
|
+
source = await readFile6(absPath, "utf8");
|
|
3369
3045
|
} catch {
|
|
3370
3046
|
return null;
|
|
3371
3047
|
}
|
|
@@ -3402,56 +3078,39 @@ async function getFileAnalysis(absPath) {
|
|
|
3402
3078
|
};
|
|
3403
3079
|
}
|
|
3404
3080
|
|
|
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");
|
|
3081
|
+
// ../brain/dist/analysis.js
|
|
3082
|
+
import { join as join8 } from "path";
|
|
3083
|
+
import { readFile as readFile7, writeFile as writeFile6, mkdir as mkdir5 } from "fs/promises";
|
|
3084
|
+
async function readAnalysis(projectRoot) {
|
|
3085
|
+
const p = join8(projectRoot, ".vibe-splainer", "analysis.json");
|
|
3415
3086
|
try {
|
|
3416
|
-
const raw = await
|
|
3087
|
+
const raw = await readFile7(p, "utf8");
|
|
3417
3088
|
return JSON.parse(raw);
|
|
3418
3089
|
} catch {
|
|
3419
3090
|
return null;
|
|
3420
3091
|
}
|
|
3421
3092
|
}
|
|
3422
|
-
async function
|
|
3423
|
-
|
|
3424
|
-
|
|
3425
|
-
|
|
3426
|
-
|
|
3427
|
-
|
|
3428
|
-
|
|
3429
|
-
|
|
3430
|
-
const dossierPath = join10(dir, "dossier.json");
|
|
3431
|
-
const tmp = dossierPath + ".tmp";
|
|
3432
|
-
await writeFile9(tmp, JSON.stringify(dossier, null, 2), "utf8");
|
|
3433
|
-
const { rename } = await import("fs/promises");
|
|
3434
|
-
await rename(tmp, dossierPath);
|
|
3435
|
-
await regenerateUI(projectRoot, dossier);
|
|
3436
|
-
});
|
|
3093
|
+
async function readActionBindings(projectRoot) {
|
|
3094
|
+
const p = join8(projectRoot, ".vibe-splainer", "action_bindings.json");
|
|
3095
|
+
try {
|
|
3096
|
+
const raw = await readFile7(p, "utf8");
|
|
3097
|
+
return JSON.parse(raw);
|
|
3098
|
+
} catch {
|
|
3099
|
+
return null;
|
|
3100
|
+
}
|
|
3437
3101
|
}
|
|
3438
|
-
|
|
3439
|
-
|
|
3440
|
-
|
|
3441
|
-
|
|
3442
|
-
|
|
3443
|
-
|
|
3444
|
-
|
|
3445
|
-
|
|
3446
|
-
|
|
3447
|
-
|
|
3102
|
+
|
|
3103
|
+
// ../brain/dist/dossier.js
|
|
3104
|
+
import { join as join9 } from "path";
|
|
3105
|
+
import { readFile as readFile8 } from "fs/promises";
|
|
3106
|
+
async function readDossier(projectRoot) {
|
|
3107
|
+
const dossierPath = join9(projectRoot, ".vibe-splainer", "dossier.json");
|
|
3108
|
+
try {
|
|
3109
|
+
const raw = await readFile8(dossierPath, "utf8");
|
|
3110
|
+
return JSON.parse(raw);
|
|
3111
|
+
} catch {
|
|
3112
|
+
return null;
|
|
3448
3113
|
}
|
|
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
3114
|
}
|
|
3456
3115
|
function validateMermaidNodeCount(diagram) {
|
|
3457
3116
|
if (!diagram)
|
|
@@ -3468,12 +3127,520 @@ function validateMermaidNodeCount(diagram) {
|
|
|
3468
3127
|
return nodes.size <= 7;
|
|
3469
3128
|
}
|
|
3470
3129
|
|
|
3471
|
-
// ../brain/dist/
|
|
3130
|
+
// ../brain/dist/policy/RecommendationEngine.js
|
|
3131
|
+
function getEffortValue(effort) {
|
|
3132
|
+
if (effort === "low")
|
|
3133
|
+
return 1;
|
|
3134
|
+
if (effort === "medium")
|
|
3135
|
+
return 2;
|
|
3136
|
+
return 3;
|
|
3137
|
+
}
|
|
3138
|
+
function getImpactValue(impact) {
|
|
3139
|
+
if (impact === "low")
|
|
3140
|
+
return 1;
|
|
3141
|
+
if (impact === "medium")
|
|
3142
|
+
return 2;
|
|
3143
|
+
return 3;
|
|
3144
|
+
}
|
|
3145
|
+
var RecommendationEngine = class {
|
|
3146
|
+
static generateRecommendations(file) {
|
|
3147
|
+
const recommendations = [];
|
|
3148
|
+
if (file.riskTypes.includes("mutation_orchestration")) {
|
|
3149
|
+
recommendations.push({
|
|
3150
|
+
strategy: "Extract Decision Logic",
|
|
3151
|
+
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.",
|
|
3152
|
+
effort: "high",
|
|
3153
|
+
impact: "high"
|
|
3154
|
+
});
|
|
3155
|
+
}
|
|
3156
|
+
if (file.riskTypes.includes("registry_bottleneck")) {
|
|
3157
|
+
recommendations.push({
|
|
3158
|
+
strategy: "Append-Only Registry Updates",
|
|
3159
|
+
description: "Add new entries without removing existing keys. Treat the registry map as append-only until all consumers are verified.",
|
|
3160
|
+
effort: "low",
|
|
3161
|
+
impact: "medium"
|
|
3162
|
+
});
|
|
3163
|
+
}
|
|
3164
|
+
if (file.riskTypes.includes("registry_consumer")) {
|
|
3165
|
+
recommendations.push({
|
|
3166
|
+
strategy: "Verify Registry Contract",
|
|
3167
|
+
description: "Verify the registry contract before patching. Changes to field types must be reflected in both the registry and all rendering paths.",
|
|
3168
|
+
effort: "medium",
|
|
3169
|
+
impact: "low"
|
|
3170
|
+
});
|
|
3171
|
+
}
|
|
3172
|
+
if (file.riskTypes.includes("route_handler_write_path")) {
|
|
3173
|
+
recommendations.push({
|
|
3174
|
+
strategy: "Integration Testing First",
|
|
3175
|
+
description: "Add integration tests covering success and failure paths before modifying. Verify HTTP status codes and response shapes are preserved.",
|
|
3176
|
+
effort: "medium",
|
|
3177
|
+
impact: "high"
|
|
3178
|
+
});
|
|
3179
|
+
}
|
|
3180
|
+
if (file.riskTypes.includes("god_component") || file.riskTypes.includes("god_hook")) {
|
|
3181
|
+
recommendations.push({
|
|
3182
|
+
strategy: "Extract Sub-Concerns",
|
|
3183
|
+
description: "Extract sub-concerns into separate modules first. Only refactor the extraction points after tests confirm equivalence.",
|
|
3184
|
+
effort: "high",
|
|
3185
|
+
impact: "high"
|
|
3186
|
+
});
|
|
3187
|
+
}
|
|
3188
|
+
if (file.sideEffectProfile.includes("database_write")) {
|
|
3189
|
+
recommendations.push({
|
|
3190
|
+
strategy: "Feature Flags & Staging",
|
|
3191
|
+
description: "Wrap changes in a transaction or use a feature flag. Run against a staging database before production.",
|
|
3192
|
+
effort: "medium",
|
|
3193
|
+
impact: "high"
|
|
3194
|
+
});
|
|
3195
|
+
}
|
|
3196
|
+
if (recommendations.length === 0 && file.importedBy.length >= 5) {
|
|
3197
|
+
recommendations.push({
|
|
3198
|
+
strategy: "Review Blast Radius",
|
|
3199
|
+
description: "Review importedBy before patching. Run affected integration tests.",
|
|
3200
|
+
effort: "medium",
|
|
3201
|
+
impact: "medium"
|
|
3202
|
+
});
|
|
3203
|
+
}
|
|
3204
|
+
recommendations.sort((a, b) => {
|
|
3205
|
+
const ratioA = getImpactValue(a.impact) / getEffortValue(a.effort);
|
|
3206
|
+
const ratioB = getImpactValue(b.impact) / getEffortValue(b.effort);
|
|
3207
|
+
return ratioB - ratioA;
|
|
3208
|
+
});
|
|
3209
|
+
return recommendations;
|
|
3210
|
+
}
|
|
3211
|
+
};
|
|
3212
|
+
|
|
3213
|
+
// dist/export/ArtifactBundleWriter.js
|
|
3214
|
+
import { join as join10 } from "path";
|
|
3215
|
+
import { writeFile as writeFile7, mkdir as mkdir6, rm, rename } from "fs/promises";
|
|
3216
|
+
import { createHash } from "crypto";
|
|
3217
|
+
var ArtifactBundleWriter = class {
|
|
3218
|
+
projectRoot;
|
|
3219
|
+
constructor(projectRoot) {
|
|
3220
|
+
this.projectRoot = projectRoot;
|
|
3221
|
+
}
|
|
3222
|
+
async writeBundle(artifacts) {
|
|
3223
|
+
const outputDir = join10(this.projectRoot, ".vibe-splainer");
|
|
3224
|
+
const stagingDir = join10(this.projectRoot, ".vibe-splainer.tmp");
|
|
3225
|
+
const oldDir = join10(this.projectRoot, ".vibe-splainer.old");
|
|
3226
|
+
try {
|
|
3227
|
+
await rm(stagingDir, { recursive: true, force: true });
|
|
3228
|
+
await rm(oldDir, { recursive: true, force: true });
|
|
3229
|
+
const { existsSync: existsSync5 } = await import("fs");
|
|
3230
|
+
const { cp } = await import("fs/promises");
|
|
3231
|
+
if (existsSync5(outputDir)) {
|
|
3232
|
+
await cp(outputDir, stagingDir, { recursive: true });
|
|
3233
|
+
} else {
|
|
3234
|
+
await mkdir6(stagingDir, { recursive: true });
|
|
3235
|
+
}
|
|
3236
|
+
const manifestArtifacts = [];
|
|
3237
|
+
for (const artifact of artifacts) {
|
|
3238
|
+
const destPath = join10(stagingDir, artifact.path);
|
|
3239
|
+
await mkdir6(join10(destPath, ".."), { recursive: true });
|
|
3240
|
+
await writeFile7(destPath, artifact.content);
|
|
3241
|
+
const contentStr = artifact.content;
|
|
3242
|
+
const buffer = typeof contentStr === "string" ? Buffer.from(contentStr, "utf-8") : contentStr;
|
|
3243
|
+
manifestArtifacts.push({
|
|
3244
|
+
type: artifact.type,
|
|
3245
|
+
path: artifact.path,
|
|
3246
|
+
checksum: "sha256:" + createHash("sha256").update(buffer).digest("hex"),
|
|
3247
|
+
sizeBytes: buffer.length
|
|
3248
|
+
});
|
|
3249
|
+
}
|
|
3250
|
+
const manifest = {
|
|
3251
|
+
schemaVersion: "1.0.0",
|
|
3252
|
+
generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3253
|
+
projectRoot: this.projectRoot,
|
|
3254
|
+
artifacts: manifestArtifacts
|
|
3255
|
+
};
|
|
3256
|
+
await writeFile7(join10(stagingDir, "artifact_manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
|
|
3257
|
+
let swapped = false;
|
|
3258
|
+
if (existsSync5(outputDir)) {
|
|
3259
|
+
await rename(outputDir, oldDir);
|
|
3260
|
+
swapped = true;
|
|
3261
|
+
}
|
|
3262
|
+
try {
|
|
3263
|
+
await rename(stagingDir, outputDir);
|
|
3264
|
+
} catch (err) {
|
|
3265
|
+
if (swapped) {
|
|
3266
|
+
await rename(oldDir, outputDir);
|
|
3267
|
+
}
|
|
3268
|
+
throw err;
|
|
3269
|
+
}
|
|
3270
|
+
if (swapped) {
|
|
3271
|
+
await rm(oldDir, { recursive: true, force: true });
|
|
3272
|
+
}
|
|
3273
|
+
} catch (err) {
|
|
3274
|
+
await rm(stagingDir, { recursive: true, force: true });
|
|
3275
|
+
throw err;
|
|
3276
|
+
}
|
|
3277
|
+
}
|
|
3278
|
+
};
|
|
3279
|
+
|
|
3280
|
+
// dist/export/renderers/JsonRenderer.js
|
|
3281
|
+
var JsonRenderer = class {
|
|
3282
|
+
render(viewModel, _store) {
|
|
3283
|
+
return [
|
|
3284
|
+
{
|
|
3285
|
+
type: "dossier",
|
|
3286
|
+
path: "dossier.json",
|
|
3287
|
+
content: JSON.stringify(viewModel, null, 2)
|
|
3288
|
+
}
|
|
3289
|
+
];
|
|
3290
|
+
}
|
|
3291
|
+
};
|
|
3292
|
+
|
|
3293
|
+
// dist/export/renderers/HtmlRenderer.js
|
|
3294
|
+
import { join as join11, dirname as dirname3, relative as relative3 } from "path";
|
|
3295
|
+
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
3296
|
+
import { existsSync as existsSync4, readFileSync, readdirSync, statSync } from "fs";
|
|
3297
|
+
var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
|
|
3298
|
+
function getAllFiles(dirPath, arrayOfFiles = []) {
|
|
3299
|
+
const files = readdirSync(dirPath);
|
|
3300
|
+
files.forEach(function(file) {
|
|
3301
|
+
const fullPath = join11(dirPath, file);
|
|
3302
|
+
if (statSync(fullPath).isDirectory()) {
|
|
3303
|
+
arrayOfFiles = getAllFiles(fullPath, arrayOfFiles);
|
|
3304
|
+
} else {
|
|
3305
|
+
arrayOfFiles.push(fullPath);
|
|
3306
|
+
}
|
|
3307
|
+
});
|
|
3308
|
+
return arrayOfFiles;
|
|
3309
|
+
}
|
|
3310
|
+
var HtmlRenderer = class {
|
|
3311
|
+
render(viewModel, _store) {
|
|
3312
|
+
const candidatePaths = [
|
|
3313
|
+
join11(__dirname2, "ui"),
|
|
3314
|
+
// bundled: dist/index.js -> dist/ui
|
|
3315
|
+
join11(__dirname2, "..", "..", "ui"),
|
|
3316
|
+
// unbundled: dist/export/renderers -> dist/ui
|
|
3317
|
+
join11(__dirname2, "..", "ui"),
|
|
3318
|
+
// alt bundle
|
|
3319
|
+
join11(__dirname2, "..", "..", "..", "ui", "dist"),
|
|
3320
|
+
// dev: packages/cli/src/export/renderers -> packages/ui/dist
|
|
3321
|
+
join11(__dirname2, "..", "..", "packages", "ui", "dist")
|
|
3322
|
+
// repo root -> packages/ui/dist
|
|
3323
|
+
];
|
|
3324
|
+
let templateDir = "";
|
|
3325
|
+
for (const p of candidatePaths) {
|
|
3326
|
+
if (existsSync4(p) && existsSync4(join11(p, "index.html"))) {
|
|
3327
|
+
if (!existsSync4(join11(p, "vite.config.ts")) || p.endsWith("dist")) {
|
|
3328
|
+
templateDir = p;
|
|
3329
|
+
break;
|
|
3330
|
+
}
|
|
3331
|
+
}
|
|
3332
|
+
}
|
|
3333
|
+
if (!templateDir) {
|
|
3334
|
+
for (const p of candidatePaths) {
|
|
3335
|
+
if (existsSync4(join11(p, "index.html"))) {
|
|
3336
|
+
templateDir = p;
|
|
3337
|
+
break;
|
|
3338
|
+
}
|
|
3339
|
+
}
|
|
3340
|
+
}
|
|
3341
|
+
if (!templateDir) {
|
|
3342
|
+
console.error("[vibe-splain] UI template not found. Checked:", candidatePaths);
|
|
3343
|
+
return [];
|
|
3344
|
+
}
|
|
3345
|
+
const artifacts = [];
|
|
3346
|
+
const allFiles = getAllFiles(templateDir);
|
|
3347
|
+
for (const file of allFiles) {
|
|
3348
|
+
const relPath = relative3(templateDir, file);
|
|
3349
|
+
if (relPath === "index.html") {
|
|
3350
|
+
const templateHtml = readFileSync(file, "utf8");
|
|
3351
|
+
const injection = `<script>window.__VIBE_DOSSIER__ = ${JSON.stringify(viewModel)};</script>`;
|
|
3352
|
+
const bakedHtml = templateHtml.replace("<!-- VIBE_DOSSIER_INJECTION_POINT -->", injection);
|
|
3353
|
+
artifacts.push({
|
|
3354
|
+
type: "html",
|
|
3355
|
+
path: join11("ui", relPath),
|
|
3356
|
+
content: bakedHtml
|
|
3357
|
+
});
|
|
3358
|
+
} else {
|
|
3359
|
+
artifacts.push({
|
|
3360
|
+
type: "asset",
|
|
3361
|
+
path: join11("ui", relPath),
|
|
3362
|
+
content: readFileSync(file)
|
|
3363
|
+
});
|
|
3364
|
+
}
|
|
3365
|
+
}
|
|
3366
|
+
return artifacts;
|
|
3367
|
+
}
|
|
3368
|
+
};
|
|
3369
|
+
|
|
3370
|
+
// dist/export/renderers/DeltaRenderer.js
|
|
3371
|
+
var DeltaRenderer = class {
|
|
3372
|
+
render(_viewModel, store) {
|
|
3373
|
+
const deltaTargets = Object.values(store.files).filter((pf) => pf.isRealSource).sort((a, b) => b.gravity - a.gravity).map((pf) => ({
|
|
3374
|
+
path: pf.relativePath,
|
|
3375
|
+
gravity: Math.round(pf.gravity),
|
|
3376
|
+
isLoadBearing: pf.canonicalLoadBearing,
|
|
3377
|
+
blastRadius: pf.importedBy,
|
|
3378
|
+
pillarHint: pf.pillarHint
|
|
3379
|
+
}));
|
|
3380
|
+
return [
|
|
3381
|
+
{
|
|
3382
|
+
type: "delta",
|
|
3383
|
+
path: "delta_targets.json",
|
|
3384
|
+
content: JSON.stringify(deltaTargets, null, 2)
|
|
3385
|
+
}
|
|
3386
|
+
];
|
|
3387
|
+
}
|
|
3388
|
+
};
|
|
3389
|
+
|
|
3390
|
+
// dist/export/renderers/AgentMarkdownRenderer.js
|
|
3391
|
+
var AgentMarkdownRenderer = class {
|
|
3392
|
+
budget;
|
|
3393
|
+
bindings;
|
|
3394
|
+
constructor(budget = 8e3, bindings = null) {
|
|
3395
|
+
this.budget = budget;
|
|
3396
|
+
this.bindings = bindings;
|
|
3397
|
+
}
|
|
3398
|
+
render(viewModel, store) {
|
|
3399
|
+
let md = `# Architectural Dossier: ${viewModel.projectRoot}
|
|
3400
|
+
|
|
3401
|
+
`;
|
|
3402
|
+
if (viewModel.map.brief) {
|
|
3403
|
+
md += `## Project Brief
|
|
3404
|
+
${viewModel.map.brief}
|
|
3405
|
+
|
|
3406
|
+
`;
|
|
3407
|
+
}
|
|
3408
|
+
md += `## Stack & Entrypoints
|
|
3409
|
+
`;
|
|
3410
|
+
md += `- Stack: ${viewModel.map.stack.join(", ")}
|
|
3411
|
+
`;
|
|
3412
|
+
md += `- Entrypoints: ${viewModel.map.entrypoints.join(", ")}
|
|
3413
|
+
|
|
3414
|
+
`;
|
|
3415
|
+
const allDecisions = viewModel.pillars.flatMap((p) => p.decisions).concat(viewModel.wildDiscoveries);
|
|
3416
|
+
const uniqueDecisions = /* @__PURE__ */ new Map();
|
|
3417
|
+
for (const d of allDecisions) {
|
|
3418
|
+
if (d.primaryFile && !uniqueDecisions.has(d.primaryFile)) {
|
|
3419
|
+
uniqueDecisions.set(d.primaryFile, d);
|
|
3420
|
+
}
|
|
3421
|
+
}
|
|
3422
|
+
const tier1 = [];
|
|
3423
|
+
const tier2 = [];
|
|
3424
|
+
const tier3 = [];
|
|
3425
|
+
const sortedFiles = Object.values(store.files).filter((f) => f.isRealSource).sort((a, b) => b.gravity - a.gravity);
|
|
3426
|
+
for (const f of sortedFiles) {
|
|
3427
|
+
const card = uniqueDecisions.get(f.relativePath);
|
|
3428
|
+
const isCritical = card && card.severity >= 4;
|
|
3429
|
+
if (f.gravity >= 70 || isCritical) {
|
|
3430
|
+
tier1.push(f.relativePath);
|
|
3431
|
+
} else if (f.gravity >= 40 || card) {
|
|
3432
|
+
tier2.push(f.relativePath);
|
|
3433
|
+
} else {
|
|
3434
|
+
tier3.push(f.relativePath);
|
|
3435
|
+
}
|
|
3436
|
+
}
|
|
3437
|
+
md += `## Tier 1: Critical Files & Risks
|
|
3438
|
+
|
|
3439
|
+
`;
|
|
3440
|
+
for (const path of tier1) {
|
|
3441
|
+
const f = store.files[path];
|
|
3442
|
+
const card = uniqueDecisions.get(path);
|
|
3443
|
+
const recs = viewModel.recommendations[path] || [];
|
|
3444
|
+
md += `### ${path}
|
|
3445
|
+
`;
|
|
3446
|
+
md += `- Gravity: ${Math.round(f.gravity)} | Heat: ${Math.round(f.heat)}
|
|
3447
|
+
`;
|
|
3448
|
+
md += `- Domain: ${f.productDomain} | Role: ${f.frameworkRole}
|
|
3449
|
+
`;
|
|
3450
|
+
if (card) {
|
|
3451
|
+
md += `
|
|
3452
|
+
**Verdict**: ${card.thesis}
|
|
3453
|
+
`;
|
|
3454
|
+
md += `**Severity**: ${card.severity} | **Category**: ${card.category}
|
|
3455
|
+
`;
|
|
3456
|
+
md += `**Narrative**: ${card.narrative}
|
|
3457
|
+
`;
|
|
3458
|
+
}
|
|
3459
|
+
if (this.bindings && this.bindings.files[path]) {
|
|
3460
|
+
const fileBinding = this.bindings.files[path];
|
|
3461
|
+
const criticalFunctions = fileBinding.functions.filter((fn) => fn.semanticActions.length > 0 || fn.isEntrypoint);
|
|
3462
|
+
if (criticalFunctions.length > 0) {
|
|
3463
|
+
md += `
|
|
3464
|
+
**Critical Functions**:
|
|
3465
|
+
`;
|
|
3466
|
+
for (const fn of criticalFunctions) {
|
|
3467
|
+
md += `- \`${fn.displayName}\` (lines ${fn.startLine}-${fn.endLine})${fn.isEntrypoint ? " [Entrypoint]" : ""}
|
|
3468
|
+
`;
|
|
3469
|
+
for (const action of fn.semanticActions) {
|
|
3470
|
+
md += ` - **${action.actionKind}**${action.targetModel ? ` on ${action.targetModel}` : ""}: \`${action.calleeText}\` (line ${action.sourceLine})
|
|
3471
|
+
`;
|
|
3472
|
+
}
|
|
3473
|
+
}
|
|
3474
|
+
}
|
|
3475
|
+
}
|
|
3476
|
+
if (recs.length > 0) {
|
|
3477
|
+
md += `
|
|
3478
|
+
**Safe Patch Strategies**:
|
|
3479
|
+
`;
|
|
3480
|
+
for (const r of recs) {
|
|
3481
|
+
md += `- **${r.strategy}**: ${r.description}
|
|
3482
|
+
`;
|
|
3483
|
+
}
|
|
3484
|
+
}
|
|
3485
|
+
md += `
|
|
3486
|
+
---
|
|
3487
|
+
|
|
3488
|
+
`;
|
|
3489
|
+
}
|
|
3490
|
+
md += `## Tier 2: Important Files
|
|
3491
|
+
|
|
3492
|
+
`;
|
|
3493
|
+
for (const path of tier2) {
|
|
3494
|
+
const f = store.files[path];
|
|
3495
|
+
const card = uniqueDecisions.get(path);
|
|
3496
|
+
md += `- **${path}** (Gravity: ${Math.round(f.gravity)})`;
|
|
3497
|
+
if (card) {
|
|
3498
|
+
md += ` \u2014 ${card.thesis}`;
|
|
3499
|
+
}
|
|
3500
|
+
md += `
|
|
3501
|
+
`;
|
|
3502
|
+
}
|
|
3503
|
+
md += `
|
|
3504
|
+
`;
|
|
3505
|
+
md += `## Tier 3: Index
|
|
3506
|
+
|
|
3507
|
+
`;
|
|
3508
|
+
for (const path of tier3) {
|
|
3509
|
+
const f = store.files[path];
|
|
3510
|
+
md += `- ${path} (Gravity: ${Math.round(f.gravity)})
|
|
3511
|
+
`;
|
|
3512
|
+
}
|
|
3513
|
+
return [
|
|
3514
|
+
{
|
|
3515
|
+
type: "markdown",
|
|
3516
|
+
path: "dossier.agent.md",
|
|
3517
|
+
content: md
|
|
3518
|
+
}
|
|
3519
|
+
];
|
|
3520
|
+
}
|
|
3521
|
+
};
|
|
3522
|
+
|
|
3523
|
+
// dist/export/renderers/ValidationRenderer.js
|
|
3524
|
+
var ValidationRenderer = class {
|
|
3525
|
+
render(viewModel, _store) {
|
|
3526
|
+
if (!viewModel.map.validation)
|
|
3527
|
+
return [];
|
|
3528
|
+
return [
|
|
3529
|
+
{
|
|
3530
|
+
type: "validation",
|
|
3531
|
+
path: "validation_report.json",
|
|
3532
|
+
content: JSON.stringify(viewModel.map.validation, null, 2)
|
|
3533
|
+
}
|
|
3534
|
+
];
|
|
3535
|
+
}
|
|
3536
|
+
};
|
|
3537
|
+
|
|
3538
|
+
// dist/export/renderers/RawAnalysisRenderer.js
|
|
3539
|
+
var RawAnalysisRenderer = class {
|
|
3540
|
+
render(_viewModel, store) {
|
|
3541
|
+
return [
|
|
3542
|
+
{
|
|
3543
|
+
type: "analysis",
|
|
3544
|
+
path: "analysis.json",
|
|
3545
|
+
content: JSON.stringify(store, null, 2)
|
|
3546
|
+
}
|
|
3547
|
+
];
|
|
3548
|
+
}
|
|
3549
|
+
};
|
|
3550
|
+
|
|
3551
|
+
// dist/export/renderers/GraphRenderer.js
|
|
3552
|
+
var GraphRenderer = class {
|
|
3553
|
+
graph;
|
|
3554
|
+
constructor(graph) {
|
|
3555
|
+
this.graph = graph;
|
|
3556
|
+
}
|
|
3557
|
+
render(_viewModel, _store) {
|
|
3558
|
+
if (!this.graph)
|
|
3559
|
+
return [];
|
|
3560
|
+
return [
|
|
3561
|
+
{
|
|
3562
|
+
type: "graph",
|
|
3563
|
+
path: "graph.json",
|
|
3564
|
+
content: JSON.stringify(this.graph, null, 2)
|
|
3565
|
+
}
|
|
3566
|
+
];
|
|
3567
|
+
}
|
|
3568
|
+
};
|
|
3569
|
+
|
|
3570
|
+
// dist/export/ExportOrchestrator.js
|
|
3571
|
+
var ExportOrchestrator = class {
|
|
3572
|
+
projectRoot;
|
|
3573
|
+
constructor(projectRoot) {
|
|
3574
|
+
this.projectRoot = projectRoot;
|
|
3575
|
+
}
|
|
3576
|
+
async writeBundle(dossier, options = {}, store, graph) {
|
|
3577
|
+
const finalStore = store || await readAnalysis(this.projectRoot);
|
|
3578
|
+
if (!finalStore) {
|
|
3579
|
+
throw new Error("Analysis store not found. Scan the project first.");
|
|
3580
|
+
}
|
|
3581
|
+
const bindings = await readActionBindings(this.projectRoot);
|
|
3582
|
+
for (const p of dossier.pillars) {
|
|
3583
|
+
p.decisions = p.decisions.filter((c) => !(c.severity === 1 && c.category === "Convention"));
|
|
3584
|
+
p.cardCount = p.decisions.length;
|
|
3585
|
+
}
|
|
3586
|
+
const viewModel = this.buildViewModel(dossier, finalStore);
|
|
3587
|
+
const artifacts = [];
|
|
3588
|
+
artifacts.push(...await new JsonRenderer().render(viewModel, finalStore));
|
|
3589
|
+
artifacts.push(...await new DeltaRenderer().render(viewModel, finalStore));
|
|
3590
|
+
artifacts.push(...await new ValidationRenderer().render(viewModel, finalStore));
|
|
3591
|
+
artifacts.push(...await new RawAnalysisRenderer().render(viewModel, finalStore));
|
|
3592
|
+
if (graph) {
|
|
3593
|
+
artifacts.push(...await new GraphRenderer(graph).render(viewModel, finalStore));
|
|
3594
|
+
}
|
|
3595
|
+
const formats = ["html", "markdown"];
|
|
3596
|
+
if (options.format && options.format !== "json" && options.format !== "delta") {
|
|
3597
|
+
formats.length = 0;
|
|
3598
|
+
formats.push(options.format);
|
|
3599
|
+
}
|
|
3600
|
+
if (formats.includes("html")) {
|
|
3601
|
+
artifacts.push(...await new HtmlRenderer().render(viewModel, finalStore));
|
|
3602
|
+
}
|
|
3603
|
+
if (formats.includes("markdown")) {
|
|
3604
|
+
artifacts.push(...await new AgentMarkdownRenderer(options.budget, bindings).render(viewModel, finalStore));
|
|
3605
|
+
}
|
|
3606
|
+
const writer = new ArtifactBundleWriter(this.projectRoot);
|
|
3607
|
+
await writer.writeBundle(artifacts);
|
|
3608
|
+
}
|
|
3609
|
+
buildViewModel(dossier, store) {
|
|
3610
|
+
const recommendations = {};
|
|
3611
|
+
for (const file of dossier.map.topGravity) {
|
|
3612
|
+
const persisted = store.files[file];
|
|
3613
|
+
if (persisted) {
|
|
3614
|
+
recommendations[file] = RecommendationEngine.generateRecommendations(persisted);
|
|
3615
|
+
}
|
|
3616
|
+
}
|
|
3617
|
+
for (const file of dossier.map.topHeat) {
|
|
3618
|
+
if (!recommendations[file]) {
|
|
3619
|
+
const persisted = store.files[file];
|
|
3620
|
+
if (persisted) {
|
|
3621
|
+
recommendations[file] = RecommendationEngine.generateRecommendations(persisted);
|
|
3622
|
+
}
|
|
3623
|
+
}
|
|
3624
|
+
}
|
|
3625
|
+
return {
|
|
3626
|
+
...dossier,
|
|
3627
|
+
recommendations
|
|
3628
|
+
};
|
|
3629
|
+
}
|
|
3630
|
+
};
|
|
3631
|
+
|
|
3632
|
+
// dist/export/Watcher.js
|
|
3472
3633
|
import chokidar from "chokidar";
|
|
3473
3634
|
import { createHash as createHash2 } from "crypto";
|
|
3474
|
-
import { readFile as
|
|
3475
|
-
import { join as
|
|
3476
|
-
|
|
3635
|
+
import { readFile as readFile9 } from "fs/promises";
|
|
3636
|
+
import { join as join12 } from "path";
|
|
3637
|
+
var activeWatchers = /* @__PURE__ */ new Map();
|
|
3638
|
+
async function startWatcher(projectRoot, watchedPaths) {
|
|
3639
|
+
const existing = activeWatchers.get(projectRoot);
|
|
3640
|
+
if (existing) {
|
|
3641
|
+
await existing.close();
|
|
3642
|
+
activeWatchers.delete(projectRoot);
|
|
3643
|
+
}
|
|
3477
3644
|
const watcher = chokidar.watch(watchedPaths.length > 0 ? watchedPaths : projectRoot, {
|
|
3478
3645
|
ignoreInitial: true,
|
|
3479
3646
|
ignored: ["**/node_modules/**", "**/dist/**", "**/build/**", "**/.vibe-splainer/**"],
|
|
@@ -3484,14 +3651,14 @@ function startWatcher(projectRoot, watchedPaths) {
|
|
|
3484
3651
|
const dossier = await readDossier(projectRoot);
|
|
3485
3652
|
if (!dossier)
|
|
3486
3653
|
return;
|
|
3487
|
-
const content = await
|
|
3654
|
+
const content = await readFile9(filepath, "utf8");
|
|
3488
3655
|
const newHash = createHash2("sha256").update(content).digest("hex");
|
|
3489
3656
|
let mutated = false;
|
|
3490
3657
|
for (const pillar of dossier.pillars) {
|
|
3491
3658
|
for (const card of pillar.decisions) {
|
|
3492
3659
|
if (!card.primaryFile)
|
|
3493
3660
|
continue;
|
|
3494
|
-
const absMatch = filepath ===
|
|
3661
|
+
const absMatch = filepath === join12(projectRoot, card.primaryFile) || filepath.endsWith("/" + card.primaryFile);
|
|
3495
3662
|
if (absMatch && card.lastScannedHash !== newHash) {
|
|
3496
3663
|
card.status = "stale";
|
|
3497
3664
|
const rel = card.primaryFile;
|
|
@@ -3501,12 +3668,16 @@ function startWatcher(projectRoot, watchedPaths) {
|
|
|
3501
3668
|
}
|
|
3502
3669
|
}
|
|
3503
3670
|
}
|
|
3504
|
-
if (mutated)
|
|
3505
|
-
|
|
3671
|
+
if (mutated) {
|
|
3672
|
+
const orchestrator = new ExportOrchestrator(projectRoot);
|
|
3673
|
+
await orchestrator.writeBundle(dossier);
|
|
3674
|
+
console.error(`[vibe-splain] File changed: ${filepath}. Dossier artifacts updated.`);
|
|
3675
|
+
}
|
|
3506
3676
|
} catch (err) {
|
|
3507
3677
|
console.error("[vibe-splain] Watcher error:", err);
|
|
3508
3678
|
}
|
|
3509
3679
|
});
|
|
3680
|
+
activeWatchers.set(projectRoot, watcher);
|
|
3510
3681
|
console.error("[vibe-splain] File watcher started");
|
|
3511
3682
|
}
|
|
3512
3683
|
|
|
@@ -3525,7 +3696,7 @@ var scanProjectTool = {
|
|
|
3525
3696
|
required: ["projectRoot"]
|
|
3526
3697
|
}
|
|
3527
3698
|
};
|
|
3528
|
-
async function handleScanProject(args) {
|
|
3699
|
+
async function handleScanProject(args, options = {}) {
|
|
3529
3700
|
const projectRoot = args.projectRoot;
|
|
3530
3701
|
if (!projectRoot)
|
|
3531
3702
|
throw new Error("projectRoot is required");
|
|
@@ -3537,7 +3708,15 @@ async function handleScanProject(args) {
|
|
|
3537
3708
|
version: "2.0.0",
|
|
3538
3709
|
scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
|
|
3539
3710
|
projectRoot,
|
|
3540
|
-
map: {
|
|
3711
|
+
map: {
|
|
3712
|
+
...result.map,
|
|
3713
|
+
brief,
|
|
3714
|
+
validation: result.validation ? {
|
|
3715
|
+
passed: result.validation.passed,
|
|
3716
|
+
errors: result.validation.errors,
|
|
3717
|
+
warnings: result.validation.warnings
|
|
3718
|
+
} : void 0
|
|
3719
|
+
},
|
|
3541
3720
|
pillars: existing?.pillars ?? [],
|
|
3542
3721
|
wildDiscoveries: existing?.wildDiscoveries ?? [],
|
|
3543
3722
|
stalePaths: existing?.stalePaths ?? []
|
|
@@ -3547,12 +3726,22 @@ async function handleScanProject(args) {
|
|
|
3547
3726
|
dossier.pillars.push({ name: def.name, cardCount: 0, decisions: [] });
|
|
3548
3727
|
}
|
|
3549
3728
|
}
|
|
3550
|
-
|
|
3551
|
-
|
|
3729
|
+
const orchestrator = new ExportOrchestrator(projectRoot);
|
|
3730
|
+
await orchestrator.writeBundle(dossier, {
|
|
3731
|
+
format: options.format,
|
|
3732
|
+
budget: options.budget ? parseInt(options.budget, 10) : void 0,
|
|
3733
|
+
scope: options.scope
|
|
3734
|
+
}, result.store, result.graph);
|
|
3735
|
+
await startWatcher(projectRoot, result.files.map((f) => f.path));
|
|
3552
3736
|
console.error(`[vibe-splain] Scan complete. ${result.totalFilesScanned} files, ${result.realSourceCount} real-source, ${result.wildCandidates.length} wild candidates.`);
|
|
3553
3737
|
const validation = result.validation ?? { passed: true, errors: 0, warnings: 0, reportPath: ".vibe-splainer/validation_report.json" };
|
|
3738
|
+
let statusMsg = "Scan complete.";
|
|
3739
|
+
if (!validation.passed) {
|
|
3740
|
+
statusMsg = `SCAN QUALITY WARNING: ${validation.errors} errors and ${validation.warnings} warnings found in validation report. Delta Engine automation may be blocked.`;
|
|
3741
|
+
}
|
|
3554
3742
|
return {
|
|
3555
3743
|
ok: true,
|
|
3744
|
+
message: statusMsg,
|
|
3556
3745
|
validation: {
|
|
3557
3746
|
passed: validation.passed,
|
|
3558
3747
|
errors: validation.errors,
|
|
@@ -3636,7 +3825,7 @@ var setProjectBriefTool = {
|
|
|
3636
3825
|
required: ["projectRoot", "brief"]
|
|
3637
3826
|
}
|
|
3638
3827
|
};
|
|
3639
|
-
async function handleSetProjectBrief(args) {
|
|
3828
|
+
async function handleSetProjectBrief(args, options = {}) {
|
|
3640
3829
|
const projectRoot = args.projectRoot;
|
|
3641
3830
|
const brief = args.brief;
|
|
3642
3831
|
if (!projectRoot || !brief)
|
|
@@ -3646,7 +3835,12 @@ async function handleSetProjectBrief(args) {
|
|
|
3646
3835
|
return { error: "No project map found. Run scan_project first." };
|
|
3647
3836
|
}
|
|
3648
3837
|
dossier.map.brief = brief;
|
|
3649
|
-
|
|
3838
|
+
const orchestrator = new ExportOrchestrator(projectRoot);
|
|
3839
|
+
await orchestrator.writeBundle(dossier, {
|
|
3840
|
+
format: options.format,
|
|
3841
|
+
budget: options.budget ? parseInt(options.budget, 10) : void 0,
|
|
3842
|
+
scope: options.scope
|
|
3843
|
+
});
|
|
3650
3844
|
const documented = new Set([...dossier.pillars.flatMap((p) => p.decisions), ...dossier.wildDiscoveries].map((c) => c.primaryFile).filter(Boolean));
|
|
3651
3845
|
const startHere = dossier.map.topGravity.filter((f) => !documented.has(f));
|
|
3652
3846
|
const wild = dossier.map.topHeat.filter((f) => !documented.has(f));
|
|
@@ -3661,8 +3855,8 @@ async function handleSetProjectBrief(args) {
|
|
|
3661
3855
|
}
|
|
3662
3856
|
|
|
3663
3857
|
// dist/mcp/tools/get_file_context.js
|
|
3664
|
-
import { readFile as
|
|
3665
|
-
import { join as
|
|
3858
|
+
import { readFile as readFile10 } from "fs/promises";
|
|
3859
|
+
import { join as join13, relative as relative4, isAbsolute } from "path";
|
|
3666
3860
|
var getFileContextTool = {
|
|
3667
3861
|
name: "get_file_context",
|
|
3668
3862
|
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 +3876,8 @@ async function handleGetFileContext(args) {
|
|
|
3682
3876
|
const full = args.full === true;
|
|
3683
3877
|
if (!projectRoot || !filePath)
|
|
3684
3878
|
throw new Error("projectRoot and filePath are required");
|
|
3685
|
-
const fullPath = isAbsolute(filePath) ? filePath :
|
|
3686
|
-
const relPath =
|
|
3879
|
+
const fullPath = isAbsolute(filePath) ? filePath : join13(projectRoot, filePath);
|
|
3880
|
+
const relPath = relative4(projectRoot, fullPath);
|
|
3687
3881
|
const evidence = await getFileAnalysis(fullPath);
|
|
3688
3882
|
if (!evidence) {
|
|
3689
3883
|
throw new Error(`Could not analyze ${relPath} (unsupported language or parse failure).`);
|
|
@@ -3707,7 +3901,7 @@ async function handleGetFileContext(args) {
|
|
|
3707
3901
|
smellSpans: evidence.smellSpans
|
|
3708
3902
|
};
|
|
3709
3903
|
if (full) {
|
|
3710
|
-
result.source = await
|
|
3904
|
+
result.source = await readFile10(fullPath, "utf8");
|
|
3711
3905
|
}
|
|
3712
3906
|
return result;
|
|
3713
3907
|
}
|
|
@@ -3715,8 +3909,8 @@ async function handleGetFileContext(args) {
|
|
|
3715
3909
|
// dist/mcp/tools/write_decision_card.js
|
|
3716
3910
|
import { v4 as uuidv4 } from "uuid";
|
|
3717
3911
|
import { createHash as createHash3 } from "crypto";
|
|
3718
|
-
import { readFile as
|
|
3719
|
-
import { join as
|
|
3912
|
+
import { readFile as readFile11 } from "fs/promises";
|
|
3913
|
+
import { join as join14 } from "path";
|
|
3720
3914
|
var CATEGORIES = ["Bottleneck", "Hack", "Smart-Move", "Risk", "Convention", "Dead-Weight"];
|
|
3721
3915
|
function normalizeSnippet(s) {
|
|
3722
3916
|
let out = (s ?? "").replace(/\r\n/g, "\n");
|
|
@@ -3761,7 +3955,7 @@ var writeDecisionCardTool = {
|
|
|
3761
3955
|
required: ["projectRoot", "pillar", "primaryFile", "title", "thesis", "category", "severity", "narrative", "confidence", "evidence"]
|
|
3762
3956
|
}
|
|
3763
3957
|
};
|
|
3764
|
-
async function handleWriteDecisionCard(args) {
|
|
3958
|
+
async function handleWriteDecisionCard(args, options = {}) {
|
|
3765
3959
|
const projectRoot = args.projectRoot;
|
|
3766
3960
|
const pillar = args.pillar;
|
|
3767
3961
|
const primaryFile = args.primaryFile;
|
|
@@ -3804,11 +3998,16 @@ async function handleWriteDecisionCard(args) {
|
|
|
3804
3998
|
}
|
|
3805
3999
|
const store = await readAnalysis(projectRoot);
|
|
3806
4000
|
const persisted = store?.files[primaryFile];
|
|
4001
|
+
const machineConfidence = persisted?.confidence || "low";
|
|
4002
|
+
const confidenceOrder = { low: 0, medium: 1, high: 2 };
|
|
4003
|
+
if (confidenceOrder[confidence] > confidenceOrder[machineConfidence]) {
|
|
4004
|
+
throw new Error(`Requested confidence "${confidence}" exceeds machine-derived confidence "${machineConfidence}" for this file. Ground your narrative in the MRI evidence.`);
|
|
4005
|
+
}
|
|
3807
4006
|
const gravity = persisted ? Math.round(persisted.gravity) : void 0;
|
|
3808
4007
|
const heat = persisted ? Math.round(persisted.heat) : void 0;
|
|
3809
4008
|
let primaryContent = "";
|
|
3810
4009
|
try {
|
|
3811
|
-
primaryContent = await
|
|
4010
|
+
primaryContent = await readFile11(join14(projectRoot, primaryFile), "utf8");
|
|
3812
4011
|
} catch {
|
|
3813
4012
|
}
|
|
3814
4013
|
const hash = createHash3("sha256").update(primaryContent).digest("hex");
|
|
@@ -3842,7 +4041,12 @@ async function handleWriteDecisionCard(args) {
|
|
|
3842
4041
|
}
|
|
3843
4042
|
bucket.decisions.push(card);
|
|
3844
4043
|
bucket.cardCount = bucket.decisions.length;
|
|
3845
|
-
|
|
4044
|
+
const orchestrator = new ExportOrchestrator(projectRoot);
|
|
4045
|
+
await orchestrator.writeBundle(dossier, {
|
|
4046
|
+
format: options.format,
|
|
4047
|
+
budget: options.budget ? parseInt(options.budget, 10) : void 0,
|
|
4048
|
+
scope: options.scope
|
|
4049
|
+
});
|
|
3846
4050
|
console.error(`[vibe-splain] Card written: "${title}" [${category} sev${severity}] in "${pillar}"${isWild ? " (Wild Discovery)" : ""}`);
|
|
3847
4051
|
const documented = new Set([...dossier.pillars.flatMap((p) => p.decisions), ...dossier.wildDiscoveries].map((c) => c.primaryFile).filter(Boolean));
|
|
3848
4052
|
const remaining = [.../* @__PURE__ */ new Set([...dossier.map.topGravity, ...dossier.map.topHeat])].filter((f) => !documented.has(f));
|
|
@@ -3992,7 +4196,7 @@ var markStaleTool = {
|
|
|
3992
4196
|
required: ["projectRoot", "filePaths"]
|
|
3993
4197
|
}
|
|
3994
4198
|
};
|
|
3995
|
-
async function handleMarkStale(args) {
|
|
4199
|
+
async function handleMarkStale(args, options = {}) {
|
|
3996
4200
|
const projectRoot = args.projectRoot;
|
|
3997
4201
|
const filePaths = args.filePaths;
|
|
3998
4202
|
if (!projectRoot || !filePaths)
|
|
@@ -4020,7 +4224,12 @@ async function handleMarkStale(args) {
|
|
|
4020
4224
|
dossier.stalePaths.push(filePath);
|
|
4021
4225
|
}
|
|
4022
4226
|
}
|
|
4023
|
-
|
|
4227
|
+
const orchestrator = new ExportOrchestrator(projectRoot);
|
|
4228
|
+
await orchestrator.writeBundle(dossier, {
|
|
4229
|
+
format: options.format,
|
|
4230
|
+
budget: options.budget ? parseInt(options.budget, 10) : void 0,
|
|
4231
|
+
scope: options.scope
|
|
4232
|
+
});
|
|
4024
4233
|
return {
|
|
4025
4234
|
success: true,
|
|
4026
4235
|
staleCardsMarked: staleCount,
|
|
@@ -4102,10 +4311,10 @@ var TOOL_HANDLERS = {
|
|
|
4102
4311
|
get_wild_discoveries: handleGetWildDiscoveries,
|
|
4103
4312
|
mark_stale: handleMarkStale
|
|
4104
4313
|
};
|
|
4105
|
-
async function startMCPServer() {
|
|
4314
|
+
async function startMCPServer(options = {}) {
|
|
4106
4315
|
await initParser2();
|
|
4107
4316
|
console.error("[vibe-splain] Tree-Sitter parser initialized");
|
|
4108
|
-
const server = new Server({ name: "vibe-splain", version: "
|
|
4317
|
+
const server = new Server({ name: "vibe-splain", version: "3.0.0" }, { capabilities: { tools: {}, prompts: {} } });
|
|
4109
4318
|
server.setRequestHandler(ListPromptsRequestSchema, async () => ({
|
|
4110
4319
|
prompts: [
|
|
4111
4320
|
{
|
|
@@ -4202,7 +4411,7 @@ When done, share the exact file:// UI link returned by scan_project. Never inven
|
|
|
4202
4411
|
};
|
|
4203
4412
|
}
|
|
4204
4413
|
try {
|
|
4205
|
-
const result = await handler(args
|
|
4414
|
+
const result = await handler(args, options);
|
|
4206
4415
|
return {
|
|
4207
4416
|
content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
|
|
4208
4417
|
};
|
|
@@ -4221,14 +4430,33 @@ When done, share the exact file:// UI link returned by scan_project. Never inven
|
|
|
4221
4430
|
}
|
|
4222
4431
|
|
|
4223
4432
|
// dist/commands/serve.js
|
|
4224
|
-
async function serveCommand() {
|
|
4433
|
+
async function serveCommand(options) {
|
|
4225
4434
|
console.error("[vibe-splain] Starting MCP server...");
|
|
4226
|
-
await startMCPServer();
|
|
4435
|
+
await startMCPServer(options);
|
|
4436
|
+
}
|
|
4437
|
+
|
|
4438
|
+
// dist/commands/export.js
|
|
4439
|
+
async function exportCommand(projectRoot, options) {
|
|
4440
|
+
const root = projectRoot || process.cwd();
|
|
4441
|
+
console.error(`[vibe-splain] Exporting dossier for ${root}`);
|
|
4442
|
+
const dossier = await readDossier(root);
|
|
4443
|
+
if (!dossier) {
|
|
4444
|
+
console.error("[vibe-splain] Dossier not found. Run scan first.");
|
|
4445
|
+
process.exit(1);
|
|
4446
|
+
}
|
|
4447
|
+
const orchestrator = new ExportOrchestrator(root);
|
|
4448
|
+
await orchestrator.writeBundle(dossier, {
|
|
4449
|
+
format: options.format,
|
|
4450
|
+
budget: options.budget ? parseInt(options.budget, 10) : void 0,
|
|
4451
|
+
scope: options.scope
|
|
4452
|
+
});
|
|
4453
|
+
console.error("[vibe-splain] Export complete.");
|
|
4227
4454
|
}
|
|
4228
4455
|
|
|
4229
4456
|
// dist/index.js
|
|
4230
4457
|
var program = new Command();
|
|
4231
|
-
program.name("vibe-splain").description("Architectural dossier engine for vibe-coded projects").version("
|
|
4458
|
+
program.name("vibe-splain").description("Architectural dossier engine for vibe-coded projects").version("3.0.0");
|
|
4232
4459
|
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);
|
|
4460
|
+
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));
|
|
4461
|
+
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
4462
|
program.parse();
|