vibe-splain 2.7.3 → 3.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (39) hide show
  1. package/dist/commands/export.d.ts +1 -0
  2. package/dist/commands/export.js +19 -0
  3. package/dist/commands/serve.d.ts +1 -1
  4. package/dist/commands/serve.js +2 -2
  5. package/dist/export/ArtifactBundleWriter.d.ts +22 -0
  6. package/dist/export/ArtifactBundleWriter.js +56 -0
  7. package/dist/export/ExportOrchestrator.d.ts +12 -0
  8. package/dist/export/ExportOrchestrator.js +72 -0
  9. package/dist/export/Watcher.d.ts +1 -0
  10. package/dist/export/Watcher.js +47 -0
  11. package/dist/export/renderers/AgentMarkdownRenderer.d.ts +8 -0
  12. package/dist/export/renderers/AgentMarkdownRenderer.js +90 -0
  13. package/dist/export/renderers/DeltaRenderer.d.ts +6 -0
  14. package/dist/export/renderers/DeltaRenderer.js +22 -0
  15. package/dist/export/renderers/GraphRenderer.d.ts +9 -0
  16. package/dist/export/renderers/GraphRenderer.js +18 -0
  17. package/dist/export/renderers/HtmlRenderer.d.ts +6 -0
  18. package/dist/export/renderers/HtmlRenderer.js +54 -0
  19. package/dist/export/renderers/JsonRenderer.d.ts +6 -0
  20. package/dist/export/renderers/JsonRenderer.js +12 -0
  21. package/dist/export/renderers/RawAnalysisRenderer.d.ts +6 -0
  22. package/dist/export/renderers/RawAnalysisRenderer.js +12 -0
  23. package/dist/export/renderers/Renderer.d.ts +5 -0
  24. package/dist/export/renderers/Renderer.js +2 -0
  25. package/dist/export/renderers/ValidationRenderer.d.ts +6 -0
  26. package/dist/export/renderers/ValidationRenderer.js +14 -0
  27. package/dist/index.js +722 -574
  28. package/dist/mcp/server.d.ts +1 -1
  29. package/dist/mcp/server.js +3 -3
  30. package/dist/mcp/tools/mark_stale.d.ts +1 -1
  31. package/dist/mcp/tools/mark_stale.js +9 -3
  32. package/dist/mcp/tools/scan_project.d.ts +1 -1
  33. package/dist/mcp/tools/scan_project.js +24 -4
  34. package/dist/mcp/tools/set_project_brief.d.ts +1 -1
  35. package/dist/mcp/tools/set_project_brief.js +9 -3
  36. package/dist/mcp/tools/write_decision_card.d.ts +1 -1
  37. package/dist/mcp/tools/write_decision_card.js +15 -4
  38. package/dist/ui/index.html +2 -2
  39. 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 readFile8 } from "fs/promises";
92
+ import { readFile as readFile6 } from "fs/promises";
93
93
 
94
94
  // ../brain/dist/pipeline/orchestrator.js
95
- import { join as join9 } from "path";
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 join4, dirname, relative, extname, basename, sep } from "path";
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 readFile4, readdir, writeFile as writeFile4, mkdir as mkdir3 } from "fs/promises";
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 = join4(wasmsDir, "out", file);
133
+ const p = join2(wasmsDir, "out", file);
166
134
  if (existsSync2(p))
167
135
  return p;
168
136
  } catch {
169
137
  }
170
- const local = join4(__dirname, "../../wasm", file);
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 = join4(dir, entry.name);
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 = join4(projectRoot, "package.json");
663
+ const pkgPath = join2(projectRoot, "package.json");
696
664
  if (existsSync2(pkgPath)) {
697
665
  try {
698
- const pkg = JSON.parse(await readFile4(pkgPath, "utf8"));
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 = join4(projectRoot, p);
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 = join4(projectRoot, "pyproject.toml");
723
- const setupPy = join4(projectRoot, "setup.py");
724
- const requirements = join4(projectRoot, "requirements.txt");
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 readFile4(f, "utf8");
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(join4(projectRoot, "go.mod")))
709
+ if (existsSync2(join2(projectRoot, "go.mod")))
742
710
  stack.add("Go");
743
- if (existsSync2(join4(projectRoot, "Cargo.toml")))
711
+ if (existsSync2(join2(projectRoot, "Cargo.toml")))
744
712
  stack.add("Rust");
745
- if (existsSync2(join4(projectRoot, "pom.xml")) || existsSync2(join4(projectRoot, "build.gradle")))
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 readFile4(file, "utf8");
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 = join4(projectRoot, ".vibe-splainer");
1201
- await mkdir3(dir, { recursive: true });
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 writeFile4(join4(dir, "stage-01-inventory.json"), JSON.stringify(stage01, null, 2), "utf8");
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 writeFile4(join4(dir, "stage-02-framework-roles.json"), JSON.stringify(stage02, null, 2), "utf8");
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 writeFile4(join4(dir, "stage-03-domains.json"), JSON.stringify(stage03, null, 2), "utf8");
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 join5, dirname as dirname2, relative as relative2, extname as extname2, sep as sep2 } from "path";
1222
- import { readFile as readFile5, writeFile as writeFile5, mkdir as mkdir4 } from "fs/promises";
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 readFile5(tsconfigPath, "utf8");
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
- const baseFile = join5(dirname2(tsconfigPath), parsed.extends);
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" ? join5(dirname2(tsconfigPath), opts.baseUrl) : dirname2(tsconfigPath);
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, join5(baseUrl, first));
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 = join5(projectRoot, "package.json");
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 readFile5(pkgPath, "utf8"));
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 = join5(projectRoot, prefix);
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 = join5(absPrefix, entry, "package.json");
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 readFile5(wsPkgPath, "utf8"));
1300
+ const wsPkg = JSON.parse(await readFile3(wsPkgPath, "utf8"));
1295
1301
  if (typeof wsPkg.name === "string") {
1296
- packages[wsPkg.name] = relative2(projectRoot, join5(absPrefix, entry));
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 rootPaths = await extractTsConfigPaths(join5(projectRoot, "tsconfig.json"), projectRoot);
1325
+ const allPaths = await discoverAllTsConfigs(projectRoot, projectRoot);
1341
1326
  const workspacePackages = await discoverWorkspacePackages(projectRoot);
1342
- const appPaths = await discoverAppTsConfigPaths(projectRoot);
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(join5(base, "index" + ext));
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 ? join5(dir, rest) : dir;
1358
+ modulePath = rest ? join3(dir, rest) : dir;
1375
1359
  } else {
1376
- modulePath = join5(projectRoot, spec.replace(/\./g, sep2));
1360
+ modulePath = join3(projectRoot, spec.replace(/\./g, sep2));
1377
1361
  }
1378
- for (const c of [modulePath + ".py", join5(modulePath, "__init__.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 = join5(dirname2(fromAbs), spec);
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 = join5(projectRoot, replacement, rest);
1398
+ const base = join3(projectRoot, replacement, rest);
1413
1399
  const resolved = tryJsCandidates(base, projectRoot, fileSet);
1414
- return { resolved, isAlias: true, reason: resolved ? void 0 : `alias '${prefix}' found but path '${replacement}/${rest}' not in file set` };
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 = join5(projectRoot, pkgDir, rest);
1413
+ const base = join3(projectRoot, pkgDir, rest);
1421
1414
  const resolved = tryJsCandidates(base, projectRoot, fileSet);
1422
- return { resolved, isAlias: true, reason: resolved ? void 0 : `workspace package '${pkgName}' found but subpath '${rest}' not in file set` };
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 = join5(projectRoot, rest);
1422
+ const base = join3(projectRoot, rest);
1429
1423
  const resolved = tryJsCandidates(base, projectRoot, fileSet);
1430
- return { resolved, isAlias: true, reason: resolved ? void 0 : `conventional alias '${prefix}' \u2192 path not found` };
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 = join5(projectRoot, ".vibe-splainer");
1481
- await mkdir4(dir, { recursive: true });
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 writeFile5(join5(dir, "stage-04-aliases.json"), JSON.stringify(stage04, null, 2), "utf8");
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 join6, basename as basename2, extname as extname3, sep as sep3 } from "path";
1505
- import { writeFile as writeFile6, mkdir as mkdir5 } from "fs/promises";
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 = join6(projectRoot, ".vibe-splainer");
2143
- await mkdir5(dir, { recursive: true });
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 writeFile6(join6(dir, "stage-05-side-effects.json"), JSON.stringify(stage05, null, 2), "utf8");
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 writeFile6(join6(dir, "stage-06-write-intents.json"), JSON.stringify(stage06, null, 2), "utf8");
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 writeFile6(join6(dir, "stage-07-risk-types.json"), JSON.stringify(stage07, null, 2), "utf8");
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.loadBearingScore >= 5,
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 writeFile6(join6(dir, "stage-08-load-bearing.json"), JSON.stringify(stage08, null, 2), "utf8");
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 join7 } from "path";
2175
- import { writeFile as writeFile7, readFile as readFile6 } from "fs/promises";
2174
+ import { join as join5 } from "path";
2175
+ import { writeFile as writeFile5, readFile as readFile4 } from "fs/promises";
2176
2176
  var FUNCTION_TYPES2 = /* @__PURE__ */ new Set([
2177
2177
  "function_declaration",
2178
2178
  "function_expression",
@@ -2550,7 +2550,7 @@ async function runActionBinding(projectRoot, inv, res) {
2550
2550
  }
2551
2551
  }
2552
2552
  }
2553
- await writeFile7(join7(projectRoot, ".vibe-splainer", "action_bindings.json"), JSON.stringify(artifact, null, 2), "utf8");
2553
+ await writeFile5(join5(projectRoot, ".vibe-splainer", "action_bindings.json"), JSON.stringify(artifact, null, 2), "utf8");
2554
2554
  const summary = {
2555
2555
  filesProcessed,
2556
2556
  functionsExtracted,
@@ -2560,14 +2560,14 @@ async function runActionBinding(projectRoot, inv, res) {
2560
2560
  entrypointsFound,
2561
2561
  namedImportsExtracted
2562
2562
  };
2563
- await writeFile7(join7(projectRoot, ".vibe-splainer", "stage-09-action-bindings-summary.json"), JSON.stringify(summary, null, 2), "utf8");
2563
+ await writeFile5(join5(projectRoot, ".vibe-splainer", "stage-09-action-bindings-summary.json"), JSON.stringify(summary, null, 2), "utf8");
2564
2564
  return { artifact };
2565
2565
  }
2566
2566
  async function traverseCallChain(projectRoot, args) {
2567
- const artifactPath = join7(projectRoot, ".vibe-splainer", "action_bindings.json");
2567
+ const artifactPath = join5(projectRoot, ".vibe-splainer", "action_bindings.json");
2568
2568
  let artifact;
2569
2569
  try {
2570
- const raw = await readFile6(artifactPath, "utf8");
2570
+ const raw = await readFile4(artifactPath, "utf8");
2571
2571
  artifact = JSON.parse(raw);
2572
2572
  } catch {
2573
2573
  throw new Error("action_bindings.json not found. Run scan_project first.");
@@ -2690,9 +2690,8 @@ async function traverseCallChain(projectRoot, args) {
2690
2690
  }
2691
2691
 
2692
2692
  // ../brain/dist/pipeline/scoring.js
2693
- import { join as join8 } from "path";
2694
- import { writeFile as writeFile8, mkdir as mkdir6, readFile as readFile7 } from "fs/promises";
2695
- import { createHash } from "crypto";
2693
+ import { join as join6 } from "path";
2694
+ import { mkdir as mkdir4, readFile as readFile5 } from "fs/promises";
2696
2695
  function computeSeverity(sideEffectProfile, productDomain, gravity, heat, maxNesting, hasLongFunctions, swallowedCatches, runtimeEntrypoints) {
2697
2696
  let score = 0;
2698
2697
  if (sideEffectProfile.includes("database_write"))
@@ -2759,149 +2758,6 @@ function applyCorrections(file) {
2759
2758
  file.canonicalLoadBearing = true;
2760
2759
  }
2761
2760
  }
2762
- function inferObservableOutputs(frameworkRole, productDomain, sideEffectProfile) {
2763
- const outputs = [];
2764
- const ENTRYPOINT_ROLES2 = /* @__PURE__ */ new Set(["app_route_page", "app_route_handler", "pages_route", "pages_api_route", "trpc_api_route"]);
2765
- if (sideEffectProfile.includes("redirect"))
2766
- outputs.push("redirect_url");
2767
- if (ENTRYPOINT_ROLES2.has(frameworkRole))
2768
- outputs.push("http_status");
2769
- if (frameworkRole === "app_route_handler" || frameworkRole === "pages_api_route") {
2770
- outputs.push("json_response_shape");
2771
- }
2772
- if (productDomain === "booking_creation" || productDomain === "booking_management")
2773
- outputs.push("booking_uid");
2774
- if (productDomain === "payments" || productDomain === "payments_webhooks")
2775
- outputs.push("payment_status");
2776
- if (productDomain === "auth_oauth")
2777
- outputs.push("auth_token");
2778
- if (sideEffectProfile.includes("webhook_delivery") || sideEffectProfile.includes("webhook_ingress")) {
2779
- outputs.push("webhook_payload");
2780
- }
2781
- if (sideEffectProfile.includes("calendar_mutation"))
2782
- outputs.push("calendar_event_id");
2783
- if (sideEffectProfile.includes("email_send"))
2784
- outputs.push("email_payload");
2785
- if (sideEffectProfile.includes("analytics_event"))
2786
- outputs.push("sdk_event_name");
2787
- if (frameworkRole === "hook" || frameworkRole === "store")
2788
- outputs.push("ui_state_transition");
2789
- if (productDomain === "data_table" && frameworkRole === "provider") {
2790
- outputs.push("ui_state_transition", "filter_state", "selected_segment");
2791
- }
2792
- return [...new Set(outputs)];
2793
- }
2794
- function inferPatchRisk(productDomain, riskTypes, sideEffectProfile, importedByCount, loadBearingScore) {
2795
- if (loadBearingScore >= 12 || productDomain === "booking_creation" && riskTypes.includes("mutation_orchestration")) {
2796
- return {
2797
- level: "critical",
2798
- reason: `${productDomain} domain with ${riskTypes.join(", ")} \u2014 any patch risks breaking live booking, payment, or auth flows.`
2799
- };
2800
- }
2801
- if (loadBearingScore >= 8 || sideEffectProfile.includes("payment_mutation") || sideEffectProfile.includes("auth_token_mutation")) {
2802
- const external = sideEffectProfile.filter((s) => ["payment_mutation", "auth_token_mutation", "database_write", "webhook_delivery"].includes(s));
2803
- return {
2804
- level: "high",
2805
- reason: `${productDomain} writes to external state (${external.join(", ") || "database"}). Changes require integration testing.`
2806
- };
2807
- }
2808
- if (riskTypes.includes("registry_bottleneck")) {
2809
- return {
2810
- level: "high",
2811
- reason: "registry_bottleneck: central dispatch point \u2014 blast radius not measurable by fan-in alone."
2812
- };
2813
- }
2814
- if (loadBearingScore >= 5 || importedByCount >= 5) {
2815
- return { level: "medium", reason: `Imported by ${importedByCount} files. Interface changes will cascade.` };
2816
- }
2817
- if (productDomain === "data_table" && riskTypes.includes("state_machine")) {
2818
- return {
2819
- level: "medium",
2820
- reason: "data_table state machine: controls user-visible workflow state (filters, segments, pagination) \u2014 regression risk not captured by mutation scoring."
2821
- };
2822
- }
2823
- return { level: "low", reason: "Locally contained \u2014 limited blast radius." };
2824
- }
2825
- function inferSafePatchStrategy(riskTypes, sideEffectProfile) {
2826
- if (riskTypes.includes("mutation_orchestration")) {
2827
- return "Do not rewrite inline. Extract pure decision logic into a tested reducer or state machine first. Preserve all side-effect call sites (redirect URLs, SDK event names, response shapes) as invariants.";
2828
- }
2829
- if (riskTypes.includes("registry_bottleneck")) {
2830
- return "Add new entries without removing existing keys. Treat the registry map as append-only until all consumers are verified.";
2831
- }
2832
- if (riskTypes.includes("registry_consumer")) {
2833
- return "Verify the registry contract (Components.tsx) before patching. Changes to field types must be reflected in both the registry and all rendering paths.";
2834
- }
2835
- if (riskTypes.includes("route_handler_write_path")) {
2836
- return "Add integration tests covering success and failure paths before modifying. Verify HTTP status codes and response shapes are preserved.";
2837
- }
2838
- if (riskTypes.includes("god_component") || riskTypes.includes("god_hook")) {
2839
- return "Extract sub-concerns into separate modules first. Only refactor the extraction points after tests confirm equivalence.";
2840
- }
2841
- if (sideEffectProfile.includes("database_write")) {
2842
- return "Wrap changes in a transaction or use a feature flag. Run against a staging database before production.";
2843
- }
2844
- return "Review importedBy before patching. Run affected integration tests.";
2845
- }
2846
- function inferDoNotTouch(sideEffectProfile, productDomain) {
2847
- const items = [];
2848
- if (sideEffectProfile.includes("payment_mutation"))
2849
- items.push("payment flow branch");
2850
- if (sideEffectProfile.includes("auth_token_mutation"))
2851
- items.push("token issuance / refresh branch");
2852
- if (sideEffectProfile.includes("webhook_delivery") || sideEffectProfile.includes("webhook_ingress")) {
2853
- items.push("webhook payload shape");
2854
- }
2855
- if (sideEffectProfile.includes("redirect"))
2856
- items.push("redirect URL strings");
2857
- if (sideEffectProfile.includes("analytics_event"))
2858
- items.push("SDK event names");
2859
- if (sideEffectProfile.includes("booking_mutation")) {
2860
- items.push("booking success response shape", "recurring booking branch");
2861
- }
2862
- if (productDomain === "auth_oauth")
2863
- items.push("OAuth callback URLs", "token scopes");
2864
- return items;
2865
- }
2866
- function inferTestProbes(writeIntents, observableOutputs) {
2867
- const probes = [];
2868
- if (writeIntents.includes("create_booking")) {
2869
- probes.push({
2870
- name: "standard booking success",
2871
- scenario: "create a standard booking and assert success redirect and booking uid",
2872
- expectedObservable: ["booking_uid", "redirect_url", "sdk_event_name"].filter((o) => observableOutputs.includes(o))
2873
- });
2874
- }
2875
- if (writeIntents.includes("reschedule_booking")) {
2876
- probes.push({
2877
- name: "reschedule booking",
2878
- scenario: "reschedule an existing booking and assert reschedule event path",
2879
- expectedObservable: ["booking_uid", "redirect_url"].filter((o) => observableOutputs.includes(o))
2880
- });
2881
- }
2882
- if (writeIntents.includes("create_recurring_booking")) {
2883
- probes.push({
2884
- name: "recurring booking",
2885
- scenario: "create recurring booking and assert recurring success behavior",
2886
- expectedObservable: ["booking_uid", "redirect_url"].filter((o) => observableOutputs.includes(o))
2887
- });
2888
- }
2889
- if (writeIntents.includes("handle_payment_webhook")) {
2890
- probes.push({
2891
- name: "payment webhook ingestion",
2892
- scenario: "send a valid payment webhook and assert booking/payment state updated",
2893
- expectedObservable: ["payment_status", "booking_uid", "http_status"].filter((o) => observableOutputs.includes(o))
2894
- });
2895
- }
2896
- if (writeIntents.includes("issue_auth_token")) {
2897
- probes.push({
2898
- name: "token issuance",
2899
- scenario: "complete OAuth flow and assert access token issued with correct scopes",
2900
- expectedObservable: ["auth_token", "http_status"].filter((o) => observableOutputs.includes(o))
2901
- });
2902
- }
2903
- return probes;
2904
- }
2905
2761
  function deriveConfidence(fanIn, gravity) {
2906
2762
  if (fanIn >= 10 && gravity >= 40)
2907
2763
  return "high";
@@ -2910,12 +2766,12 @@ function deriveConfidence(fanIn, gravity) {
2910
2766
  return "low";
2911
2767
  }
2912
2768
  async function runScoring(projectRoot, cr, binding) {
2913
- const dir = join8(projectRoot, ".vibe-splainer");
2914
- await mkdir6(dir, { recursive: true });
2769
+ const dir = join6(projectRoot, ".vibe-splainer");
2770
+ await mkdir4(dir, { recursive: true });
2915
2771
  let bindingArtifact = binding?.artifact;
2916
2772
  if (!bindingArtifact) {
2917
2773
  try {
2918
- const raw = await readFile7(join8(projectRoot, ".vibe-splainer", "action_bindings.json"), "utf8");
2774
+ const raw = await readFile5(join6(projectRoot, ".vibe-splainer", "action_bindings.json"), "utf8");
2919
2775
  bindingArtifact = JSON.parse(raw);
2920
2776
  } catch {
2921
2777
  }
@@ -2924,7 +2780,7 @@ async function runScoring(projectRoot, cr, binding) {
2924
2780
  const severityBreakdowns = {};
2925
2781
  for (const f of cr.classified) {
2926
2782
  const severity = computeSeverity(f.sideEffectProfile, f.productDomain, f.gravity, f.heat, f.heatSignals.maxNesting, f.heatSignals.longFunctions > 0, f.heatSignals.swallowedCatches, f.runtimeEntrypoints);
2927
- const isLoadBearing = f.loadBearingScore >= 5;
2783
+ const confidence = deriveConfidence(f.gravitySignals.fanIn, f.gravity);
2928
2784
  const pf = {
2929
2785
  relativePath: f.rel,
2930
2786
  language: f.lang,
@@ -2946,161 +2802,25 @@ async function runScoring(projectRoot, cr, binding) {
2946
2802
  riskTypes: f.riskTypes,
2947
2803
  writeIntents: f.writeIntents,
2948
2804
  canonicalSeverity: severity,
2949
- canonicalLoadBearing: isLoadBearing
2805
+ canonicalLoadBearing: f.isLoadBearing,
2806
+ // STRICT: fanIn >= 10
2807
+ isOperationallyCritical: f.isOperationallyCritical,
2808
+ confidence
2950
2809
  };
2951
2810
  applyCorrections(pf);
2952
2811
  persisted[f.rel] = pf;
2953
2812
  severityBreakdowns[f.rel] = `severity=${pf.canonicalSeverity} loadBearing=${pf.canonicalLoadBearing} effects=${pf.sideEffectProfile.join(",")} domain=${pf.productDomain}`;
2954
2813
  }
2955
- const stage09 = Object.fromEntries(Object.entries(persisted).filter(([, pf]) => pf.isRealSource).map(([rel, pf]) => [rel, { canonicalSeverity: pf.canonicalSeverity, canonicalLoadBearing: pf.canonicalLoadBearing, scoreBreakdown: severityBreakdowns[rel] }]));
2956
- await writeFile8(join8(dir, "stage-09-severity.json"), JSON.stringify(stage09, null, 2), "utf8");
2957
2814
  const store = { files: persisted };
2958
- const importedByMapForDelta = /* @__PURE__ */ new Map();
2959
- for (const [rel, pf] of Object.entries(persisted)) {
2960
- importedByMapForDelta.set(rel, new Set(pf.importedBy));
2961
- }
2962
- const metaForDelta = new Map(Object.entries(persisted).map(([rel, pf]) => [rel, { frameworkRole: pf.frameworkRole, productDomain: pf.productDomain }]));
2963
- const deltaTargets = Object.values(persisted).filter((pf) => pf.isRealSource).sort((a, b) => b.gravity - a.gravity).map((pf) => {
2964
- const runtimeEntrypoints = findRuntimeEntrypoints(pf.relativePath, importedByMapForDelta, metaForDelta);
2965
- const entrypointTraceStatus = deriveEntrypointTraceStatus(pf.productDomain, runtimeEntrypoints, pf.importsUnresolved);
2966
- const smellMaxSeverity = pf.smells.length > 0 ? Math.max(...pf.smells.map((s) => s.severity)) : 0;
2967
- const loadBearingScore = computeLoadBearingScore(pf.gravity, pf.heat, pf.importedBy.length, pf.sideEffectProfile, pf.productDomain, smellMaxSeverity, runtimeEntrypoints);
2968
- const observableOutputs = inferObservableOutputs(pf.frameworkRole, pf.productDomain, pf.sideEffectProfile);
2969
- const patchRisk = inferPatchRisk(pf.productDomain, pf.riskTypes, pf.sideEffectProfile, pf.importedBy.length, loadBearingScore);
2970
- const confidence = deriveConfidence(pf.gravitySignals.fanIn, pf.gravity);
2971
- const fileHashInput = pf.hotSpans.map((h) => h.snippet).join("");
2972
- const fileHash = createHash("sha256").update(fileHashInput || pf.relativePath).digest("hex").slice(0, 12);
2973
- const rawEvidence = pf.hotSpans.map((span) => ({
2974
- file: pf.relativePath,
2975
- startLine: span.startLine,
2976
- endLine: span.endLine,
2977
- rawSourceExcerpt: span.rawExcerpt,
2978
- evidenceHash: createHash("sha256").update(span.rawExcerpt).digest("hex").slice(0, 12)
2979
- }));
2980
- const displayEvidence = pf.hotSpans.map((span) => ({
2981
- file: pf.relativePath,
2982
- startLine: span.startLine,
2983
- endLine: span.endLine,
2984
- excerpt: span.snippet,
2985
- isTruncated: span.rawExcerpt.length > 2e3
2986
- }));
2987
- let criticalFunctions = void 0;
2988
- if (bindingArtifact) {
2989
- const fileBinding = bindingArtifact.files[pf.relativePath];
2990
- if (fileBinding) {
2991
- const scoredFunctions = fileBinding.functions.map((fn) => {
2992
- let fnScore = 0;
2993
- const reasons = [];
2994
- if (fn.semanticActions.length > 0) {
2995
- fnScore += 3;
2996
- reasons.push("Contains semantic actions");
2997
- }
2998
- if (fn.isEntrypoint) {
2999
- fnScore += 2;
3000
- reasons.push("Is a framework entrypoint");
3001
- }
3002
- const resolvedOutbound = fn.calls.filter((c) => c.resolvedTargetFunctionId).length;
3003
- if (resolvedOutbound > 0) {
3004
- const callPts = Math.min(3, resolvedOutbound);
3005
- fnScore += callPts;
3006
- reasons.push(`Has ${resolvedOutbound} resolved outbound calls`);
3007
- } else if (fn.calls.length > 0) {
3008
- fnScore += 1;
3009
- reasons.push(`Has ${fn.calls.length} outbound calls`);
3010
- }
3011
- const writesModel = fn.semanticActions.some((a) => a.actionKind === "database_write" && a.targetModel);
3012
- if (writesModel) {
3013
- fnScore += 2;
3014
- reasons.push("Writes to a database model");
3015
- }
3016
- const authOrValid = fn.semanticActions.some((a) => a.actionKind === "auth_check" || a.actionKind === "validation");
3017
- if (authOrValid) {
3018
- fnScore += 1;
3019
- reasons.push("Performs auth/validation");
3020
- }
3021
- const hasEvidenceOverlap = rawEvidence.some((e) => fn.startLine <= e.endLine && fn.endLine >= e.startLine);
3022
- if (hasEvidenceOverlap) {
3023
- fnScore += 2;
3024
- reasons.push("Overlaps with raw evidence span");
3025
- }
3026
- return { fn, fnScore, reasons };
3027
- });
3028
- scoredFunctions.sort((a, b) => b.fnScore - a.fnScore);
3029
- const topFns = scoredFunctions.filter((x) => x.reasons.length > 0).slice(0, 5);
3030
- if (topFns.length > 0) {
3031
- criticalFunctions = topFns.map(({ fn, reasons }) => {
3032
- const evidence = fn.semanticActions.slice(0, 5).sort((a, b) => a.sourceLine - b.sourceLine).map((a) => ({
3033
- sourceLine: a.sourceLine,
3034
- text: a.evidenceText,
3035
- actionKind: a.actionKind,
3036
- targetModel: a.targetModel,
3037
- targetOperation: a.targetOperation,
3038
- confidence: a.confidence
3039
- }));
3040
- const confidences = fn.semanticActions.map((a) => a.confidence);
3041
- let confidence2 = "high";
3042
- if (confidences.includes("low"))
3043
- confidence2 = "low";
3044
- else if (confidences.includes("medium"))
3045
- confidence2 = "medium";
3046
- return {
3047
- functionId: fn.functionId,
3048
- displayName: fn.displayName,
3049
- functionKind: fn.functionKind,
3050
- startLine: fn.startLine,
3051
- endLine: fn.endLine,
3052
- isEntrypoint: fn.isEntrypoint,
3053
- isExported: fn.isExported,
3054
- actionKinds: [...new Set(fn.semanticActions.map((a) => a.actionKind))],
3055
- targetModels: [...new Set(fn.semanticActions.map((a) => a.targetModel).filter(Boolean))],
3056
- targetOperations: [...new Set(fn.semanticActions.map((a) => a.targetOperation).filter(Boolean))],
3057
- outboundCallCount: fn.calls.length,
3058
- resolvedOutboundCallCount: fn.calls.filter((c) => c.resolvedTargetFunctionId).length,
3059
- semanticActionCount: fn.semanticActions.length,
3060
- evidence,
3061
- confidence: confidence2,
3062
- reasons
3063
- };
3064
- });
3065
- }
3066
- }
3067
- }
3068
- return {
3069
- path: pf.relativePath,
3070
- frameworkRole: pf.frameworkRole,
3071
- productDomain: pf.productDomain,
3072
- gravity: Math.round(pf.gravity),
3073
- heat: Math.round(pf.heat),
3074
- severity: pf.canonicalSeverity,
3075
- confidence,
3076
- isLoadBearing: pf.canonicalLoadBearing || loadBearingScore >= 5,
3077
- loadBearingScore,
3078
- riskTypes: pf.riskTypes,
3079
- sideEffectProfile: pf.sideEffectProfile,
3080
- blastRadius: pf.importedBy,
3081
- runtimeEntrypoints,
3082
- entrypointTraceStatus,
3083
- blockedImports: pf.importsUnresolved,
3084
- observableOutputs,
3085
- writeIntents: pf.writeIntents,
3086
- patchRisk,
3087
- safePatchStrategy: inferSafePatchStrategy(pf.riskTypes, pf.sideEffectProfile),
3088
- doNotTouch: inferDoNotTouch(pf.sideEffectProfile, pf.productDomain),
3089
- testProbes: inferTestProbes(pf.writeIntents, observableOutputs),
3090
- rawEvidence,
3091
- displayEvidence,
3092
- criticalFunctions,
3093
- analysisAnnotation: `${pf.frameworkRole} in ${pf.productDomain} domain. fanIn=${pf.gravitySignals.fanIn} cyclomatic=${pf.gravitySignals.cyclomatic} loc=${pf.gravitySignals.loc}`,
3094
- hashes: { fileHash, evidenceHash: rawEvidence.map((e) => e.evidenceHash).join("-") }
3095
- };
3096
- });
3097
- const dest = join8(dir, "delta_targets.json");
3098
- const tmp = dest + ".tmp";
3099
- await writeFile8(tmp, JSON.stringify(deltaTargets, null, 2), "utf8");
3100
- const { rename } = await import("fs/promises");
3101
- await rename(tmp, dest);
3102
- const validationReport = await buildValidationReport(store, deltaTargets, projectRoot);
3103
- await writeFile8(join8(dir, "validation_report.json"), JSON.stringify(validationReport, null, 2), "utf8");
2815
+ const deltaTargets = Object.values(persisted).filter((pf) => pf.isRealSource).sort((a, b) => b.gravity - a.gravity).map((pf) => ({
2816
+ path: pf.relativePath,
2817
+ gravity: Math.round(pf.gravity),
2818
+ isLoadBearing: pf.canonicalLoadBearing,
2819
+ // STRICT: fanIn >= 10
2820
+ blastRadius: pf.importedBy,
2821
+ pillarHint: pf.pillarHint
2822
+ }));
2823
+ const validationReport = await buildValidationReport(store, deltaTargets, projectRoot, cr);
3104
2824
  for (const e of validationReport.errors) {
3105
2825
  console.error(`[vibe-splain] VALIDATION ERROR [${e.rule}] ${e.file}: ${e.detail}`);
3106
2826
  }
@@ -3109,24 +2829,27 @@ async function runScoring(projectRoot, cr, binding) {
3109
2829
  }
3110
2830
  return { store, deltaTargets, validationReport };
3111
2831
  }
3112
- async function buildValidationReport(store, deltaTargets, projectRoot) {
2832
+ async function buildValidationReport(store, deltaTargets, projectRoot, cr) {
3113
2833
  const errors = [];
3114
2834
  const warnings = [];
3115
2835
  let passCount = 0;
3116
- const deltaByPath = new Map(deltaTargets.map((d) => [d.path, d]));
2836
+ let tracedCount = 0;
2837
+ let realCount = 0;
3117
2838
  for (const [, pf] of Object.entries(store.files)) {
3118
2839
  if (!pf.isRealSource)
3119
2840
  continue;
3120
- const delta = deltaByPath.get(pf.relativePath);
3121
- if (pf.canonicalSeverity === 5 && !pf.canonicalLoadBearing) {
3122
- errors.push({
3123
- file: pf.relativePath,
3124
- rule: "severity_5_not_load_bearing",
3125
- detail: "severity=5 but canonicalLoadBearing=false \u2014 post-correction invariant violated",
3126
- expected: "canonicalLoadBearing=true",
3127
- actual: "canonicalLoadBearing=false"
3128
- });
3129
- continue;
2841
+ realCount++;
2842
+ const classified = cr.classified.find((f) => f.rel === pf.relativePath);
2843
+ if (classified && classified.entrypointTraceStatus === "complete")
2844
+ tracedCount++;
2845
+ if (pf.canonicalSeverity === 5 && !pf.canonicalLoadBearing && pf.gravitySignals.fanIn < 10) {
2846
+ if (!pf.isOperationallyCritical) {
2847
+ errors.push({
2848
+ file: pf.relativePath,
2849
+ rule: "severity_5_no_criticality",
2850
+ detail: "severity=5 but not load-bearing and not operationally critical"
2851
+ });
2852
+ }
3130
2853
  }
3131
2854
  if (pf.writeIntents.includes("handle_payment_webhook") && pf.sideEffectProfile.includes("none_detected")) {
3132
2855
  errors.push({
@@ -3138,7 +2861,7 @@ async function buildValidationReport(store, deltaTargets, projectRoot) {
3138
2861
  });
3139
2862
  continue;
3140
2863
  }
3141
- if (pf.productDomain === "booking_creation" && delta?.entrypointTraceStatus === "no_runtime_entrypoint_found" && pf.importsUnresolved.length === 0) {
2864
+ if (pf.productDomain === "booking_creation" && classified?.entrypointTraceStatus === "no_runtime_entrypoint_found" && pf.importsUnresolved.length === 0) {
3142
2865
  errors.push({
3143
2866
  file: pf.relativePath,
3144
2867
  rule: "booking_creation_no_entrypoint_no_blockers",
@@ -3146,72 +2869,29 @@ async function buildValidationReport(store, deltaTargets, projectRoot) {
3146
2869
  });
3147
2870
  continue;
3148
2871
  }
3149
- if (delta && delta.severity !== pf.canonicalSeverity) {
3150
- errors.push({
3151
- file: pf.relativePath,
3152
- rule: "severity_mismatch_delta",
3153
- detail: "DeltaTarget severity does not match canonicalSeverity",
3154
- expected: String(pf.canonicalSeverity),
3155
- actual: String(delta.severity)
3156
- });
3157
- continue;
3158
- }
3159
- if (pf.canonicalSeverity >= 4 && (delta?.rawEvidence.length ?? 0) === 0 && pf.hotSpans.length === 0) {
2872
+ if (pf.canonicalSeverity >= 4 && pf.hotSpans.length === 0) {
3160
2873
  errors.push({
3161
2874
  file: pf.relativePath,
3162
2875
  rule: "high_severity_no_evidence",
3163
- detail: `severity=${pf.canonicalSeverity} but rawEvidence is empty`
2876
+ detail: `severity=${pf.canonicalSeverity} but no evidence hotSpans found`
3164
2877
  });
3165
2878
  continue;
3166
2879
  }
3167
- if (pf.canonicalSeverity >= 4 && (delta?.runtimeEntrypoints.length ?? 0) === 0) {
2880
+ if (pf.canonicalSeverity >= 4 && (classified?.runtimeEntrypoints.length ?? 0) === 0) {
3168
2881
  warnings.push({
3169
2882
  file: pf.relativePath,
3170
2883
  rule: "high_severity_no_entrypoints",
3171
2884
  detail: `severity=${pf.canonicalSeverity} but no runtime entrypoints found \u2014 check alias resolution`
3172
2885
  });
3173
2886
  }
3174
- if (delta?.entrypointTraceStatus === "partial_wrong_surface") {
3175
- const foundPaths = delta.runtimeEntrypoints.map((e) => e.path).join(", ");
2887
+ if (classified?.entrypointTraceStatus === "partial_wrong_surface") {
2888
+ const foundPaths = classified.runtimeEntrypoints.map((e) => e.path).join(", ");
3176
2889
  warnings.push({
3177
2890
  file: pf.relativePath,
3178
2891
  rule: "partial_wrong_surface",
3179
2892
  detail: `Entrypoints found but domain surface mismatch for ${pf.productDomain}. Found: ${foundPaths}`
3180
2893
  });
3181
2894
  }
3182
- if (pf.riskTypes.includes("registry_bottleneck")) {
3183
- if (pf.canonicalSeverity < 4)
3184
- errors.push({
3185
- file: pf.relativePath,
3186
- rule: "registry_bottleneck_severity",
3187
- detail: "registry_bottleneck file must have severity >= 4",
3188
- expected: ">=4",
3189
- actual: String(pf.canonicalSeverity)
3190
- });
3191
- if (!pf.canonicalLoadBearing)
3192
- errors.push({
3193
- file: pf.relativePath,
3194
- rule: "registry_bottleneck_load_bearing",
3195
- detail: "registry_bottleneck file must be load-bearing",
3196
- expected: "true",
3197
- actual: "false"
3198
- });
3199
- if (delta && delta.patchRisk.level !== "high" && delta.patchRisk.level !== "critical")
3200
- errors.push({
3201
- file: pf.relativePath,
3202
- rule: "registry_bottleneck_patch_risk",
3203
- detail: "registry_bottleneck file must have patch risk high or critical",
3204
- expected: "high|critical",
3205
- actual: delta?.patchRisk.level ?? "unknown"
3206
- });
3207
- }
3208
- if (pf.productDomain === "data_table" && pf.riskTypes.includes("state_machine") && delta?.patchRisk.level === "low") {
3209
- warnings.push({
3210
- file: pf.relativePath,
3211
- rule: "data_table_state_machine_risk",
3212
- detail: "data_table state machine should have at least medium patch risk"
3213
- });
3214
- }
3215
2895
  passCount++;
3216
2896
  }
3217
2897
  const PAYMENT_PROVIDER_PATH_TERMS = ["stripe", "paypal", "btcpay", "btcpayserver", "alby", "hitpay", "payment"];
@@ -3236,45 +2916,38 @@ async function buildValidationReport(store, deltaTargets, projectRoot) {
3236
2916
  let secondaryTrigger = false;
3237
2917
  if (!primaryTrigger && pf.productDomain !== "payments_webhooks") {
3238
2918
  try {
3239
- const src = await readFile7(join8(projectRoot, rel), "utf8");
2919
+ const src = await readFile5(join6(projectRoot, rel), "utf8");
3240
2920
  secondaryTrigger = PAYMENT_CONTENT_TERMS.some((t) => src.includes(t));
3241
2921
  } catch {
3242
2922
  }
3243
2923
  }
3244
2924
  if (!primaryTrigger && !secondaryTrigger)
3245
2925
  continue;
3246
- const delta = deltaByPath.get(rel);
3247
- const triggerLabel = primaryTrigger ? "path" : "content";
3248
2926
  const webhookChecks = [
3249
2927
  [
3250
2928
  pf.productDomain !== "payments_webhooks",
3251
2929
  "webhook_domain",
3252
- `Payment webhook (${triggerLabel} trigger) not classified as payments_webhooks`
2930
+ `Payment webhook not classified as payments_webhooks`
3253
2931
  ],
3254
2932
  [
3255
2933
  !pf.sideEffectProfile.includes("webhook_ingress"),
3256
2934
  "webhook_ingress_missing",
3257
- `Payment webhook (${triggerLabel} trigger) missing webhook_ingress side effect`
2935
+ `Payment webhook missing webhook_ingress side effect`
3258
2936
  ],
3259
2937
  [
3260
2938
  !pf.sideEffectProfile.includes("payment_mutation"),
3261
2939
  "webhook_payment_mutation_missing",
3262
- `Payment webhook (${triggerLabel} trigger) missing payment_mutation side effect`
2940
+ `Payment webhook missing payment_mutation side effect`
3263
2941
  ],
3264
2942
  [
3265
2943
  !pf.writeIntents.includes("handle_payment_webhook"),
3266
2944
  "webhook_write_intent_missing",
3267
- `Payment webhook (${triggerLabel} trigger) missing handle_payment_webhook write intent`
2945
+ `Payment webhook missing handle_payment_webhook write intent`
3268
2946
  ],
3269
2947
  [
3270
- !!delta && delta.patchRisk.level !== "high" && delta.patchRisk.level !== "critical",
3271
- "webhook_patch_risk",
3272
- `Payment webhook (${triggerLabel} trigger) patchRisk must be high or critical`
3273
- ],
3274
- [
3275
- !pf.canonicalLoadBearing,
3276
- "webhook_load_bearing",
3277
- `Payment webhook (${triggerLabel} trigger) must be load-bearing`
2948
+ !pf.isOperationallyCritical,
2949
+ "webhook_criticality",
2950
+ `Payment webhook must be operationally critical`
3278
2951
  ]
3279
2952
  ];
3280
2953
  for (const [condition, rule, detail] of webhookChecks) {
@@ -3282,12 +2955,13 @@ async function buildValidationReport(store, deltaTargets, projectRoot) {
3282
2955
  errors.push({ file: rel, rule, detail });
3283
2956
  }
3284
2957
  }
2958
+ const coverage = realCount > 0 ? Math.round(tracedCount / realCount * 100) : 0;
3285
2959
  return {
3286
2960
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
3287
2961
  passed: errors.length === 0,
3288
2962
  errors,
3289
2963
  warnings,
3290
- summary: { errorCount: errors.length, warningCount: warnings.length, passCount }
2964
+ summary: { errorCount: errors.length, warningCount: warnings.length, passCount, entrypointTraceCoverage: coverage }
3291
2965
  };
3292
2966
  }
3293
2967
 
@@ -3298,8 +2972,6 @@ async function runPipeline(projectRoot) {
3298
2972
  const binding = await runActionBinding(projectRoot, inv, res);
3299
2973
  const cr = await runClassification(projectRoot, inv, res);
3300
2974
  const scoring = await runScoring(projectRoot, cr, binding);
3301
- await writeGraph(projectRoot, res.graph);
3302
- await writeAnalysis(projectRoot, scoring.store);
3303
2975
  const files = cr.classified.filter((f) => f.isRealSource).sort((a, b) => b.gravity - a.gravity).map((f) => ({
3304
2976
  path: f.abs,
3305
2977
  relativePath: f.rel,
@@ -3332,7 +3004,7 @@ async function runPipeline(projectRoot) {
3332
3004
  productDomain: f.productDomain,
3333
3005
  sideEffectProfile: f.sideEffectProfile
3334
3006
  }));
3335
- const uiUrl = `file://${join9(projectRoot, ".vibe-splainer", "ui", "index.html")}`;
3007
+ const uiUrl = `file://${join7(projectRoot, ".vibe-splainer", "ui", "index.html")}`;
3336
3008
  return {
3337
3009
  projectRoot,
3338
3010
  totalFilesScanned: cr.classified.length,
@@ -3342,6 +3014,7 @@ async function runPipeline(projectRoot) {
3342
3014
  wildCandidates,
3343
3015
  uiUrl,
3344
3016
  graph: res.graph,
3017
+ store: scoring.store,
3345
3018
  validation: {
3346
3019
  passed: scoring.validationReport.passed,
3347
3020
  errors: scoring.validationReport.summary.errorCount,
@@ -3365,7 +3038,7 @@ async function getFileAnalysis(absPath) {
3365
3038
  return null;
3366
3039
  let source;
3367
3040
  try {
3368
- source = await readFile8(absPath, "utf8");
3041
+ source = await readFile6(absPath, "utf8");
3369
3042
  } catch {
3370
3043
  return null;
3371
3044
  }
@@ -3402,56 +3075,30 @@ async function getFileAnalysis(absPath) {
3402
3075
  };
3403
3076
  }
3404
3077
 
3405
- // ../brain/dist/dossier.js
3406
- import { Mutex } from "async-mutex";
3407
- import { join as join10, dirname as dirname3 } from "path";
3408
- import { fileURLToPath as fileURLToPath2 } from "url";
3409
- import { readFile as readFile9, writeFile as writeFile9, mkdir as mkdir7 } from "fs/promises";
3410
- import { existsSync as existsSync4, cpSync } from "fs";
3411
- var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
3412
- var dossierMutex = new Mutex();
3413
- async function readDossier(projectRoot) {
3414
- const dossierPath = join10(projectRoot, ".vibe-splainer", "dossier.json");
3078
+ // ../brain/dist/analysis.js
3079
+ import { join as join8 } from "path";
3080
+ import { readFile as readFile7, writeFile as writeFile6, mkdir as mkdir5 } from "fs/promises";
3081
+ async function readAnalysis(projectRoot) {
3082
+ const p = join8(projectRoot, ".vibe-splainer", "analysis.json");
3415
3083
  try {
3416
- const raw = await readFile9(dossierPath, "utf8");
3084
+ const raw = await readFile7(p, "utf8");
3417
3085
  return JSON.parse(raw);
3418
3086
  } catch {
3419
3087
  return null;
3420
3088
  }
3421
3089
  }
3422
- async function writeDossier(projectRoot, dossier) {
3423
- await dossierMutex.runExclusive(async () => {
3424
- for (const p of dossier.pillars) {
3425
- p.decisions = p.decisions.filter((c) => !(c.severity === 1 && c.category === "Convention"));
3426
- p.cardCount = p.decisions.length;
3427
- }
3428
- const dir = join10(projectRoot, ".vibe-splainer");
3429
- await mkdir7(dir, { recursive: true });
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
- });
3437
- }
3438
- async function regenerateUI(projectRoot, dossier) {
3439
- const uiDir = join10(projectRoot, ".vibe-splainer", "ui");
3440
- await mkdir7(uiDir, { recursive: true });
3441
- let templateDir = join10(__dirname2, "ui");
3442
- if (!existsSync4(templateDir)) {
3443
- templateDir = join10(__dirname2, "../../cli/dist/ui");
3444
- }
3445
- if (!existsSync4(templateDir)) {
3446
- console.error("[vibe-splain] UI template not found at", templateDir, "- skipping UI regeneration");
3447
- return;
3090
+
3091
+ // ../brain/dist/dossier.js
3092
+ import { join as join9 } from "path";
3093
+ import { readFile as readFile8 } from "fs/promises";
3094
+ async function readDossier(projectRoot) {
3095
+ const dossierPath = join9(projectRoot, ".vibe-splainer", "dossier.json");
3096
+ try {
3097
+ const raw = await readFile8(dossierPath, "utf8");
3098
+ return JSON.parse(raw);
3099
+ } catch {
3100
+ return null;
3448
3101
  }
3449
- cpSync(templateDir, uiDir, { recursive: true });
3450
- let html = await readFile9(join10(templateDir, "index.html"), "utf8");
3451
- const injection = `<script>window.__VIBE_DOSSIER__ = ${JSON.stringify(dossier)};</script>`;
3452
- html = html.replace("<!-- VIBE_DOSSIER_INJECTION_POINT -->", injection);
3453
- await writeFile9(join10(uiDir, "index.html"), html, "utf8");
3454
- console.error("[vibe-splain] UI regenerated at", join10(uiDir, "index.html"));
3455
3102
  }
3456
3103
  function validateMermaidNodeCount(diagram) {
3457
3104
  if (!diagram)
@@ -3468,11 +3115,452 @@ function validateMermaidNodeCount(diagram) {
3468
3115
  return nodes.size <= 7;
3469
3116
  }
3470
3117
 
3471
- // ../brain/dist/watcher.js
3118
+ // ../brain/dist/policy/RecommendationEngine.js
3119
+ function getEffortValue(effort) {
3120
+ if (effort === "low")
3121
+ return 1;
3122
+ if (effort === "medium")
3123
+ return 2;
3124
+ return 3;
3125
+ }
3126
+ function getImpactValue(impact) {
3127
+ if (impact === "low")
3128
+ return 1;
3129
+ if (impact === "medium")
3130
+ return 2;
3131
+ return 3;
3132
+ }
3133
+ var RecommendationEngine = class {
3134
+ static generateRecommendations(file) {
3135
+ const recommendations = [];
3136
+ if (file.riskTypes.includes("mutation_orchestration")) {
3137
+ recommendations.push({
3138
+ strategy: "Extract Decision Logic",
3139
+ description: "Do not rewrite inline. Extract pure decision logic into a tested reducer or state machine first. Preserve all side-effect call sites (redirect URLs, SDK event names, response shapes) as invariants.",
3140
+ effort: "high",
3141
+ impact: "high"
3142
+ });
3143
+ }
3144
+ if (file.riskTypes.includes("registry_bottleneck")) {
3145
+ recommendations.push({
3146
+ strategy: "Append-Only Registry Updates",
3147
+ description: "Add new entries without removing existing keys. Treat the registry map as append-only until all consumers are verified.",
3148
+ effort: "low",
3149
+ impact: "medium"
3150
+ });
3151
+ }
3152
+ if (file.riskTypes.includes("registry_consumer")) {
3153
+ recommendations.push({
3154
+ strategy: "Verify Registry Contract",
3155
+ description: "Verify the registry contract before patching. Changes to field types must be reflected in both the registry and all rendering paths.",
3156
+ effort: "medium",
3157
+ impact: "low"
3158
+ });
3159
+ }
3160
+ if (file.riskTypes.includes("route_handler_write_path")) {
3161
+ recommendations.push({
3162
+ strategy: "Integration Testing First",
3163
+ description: "Add integration tests covering success and failure paths before modifying. Verify HTTP status codes and response shapes are preserved.",
3164
+ effort: "medium",
3165
+ impact: "high"
3166
+ });
3167
+ }
3168
+ if (file.riskTypes.includes("god_component") || file.riskTypes.includes("god_hook")) {
3169
+ recommendations.push({
3170
+ strategy: "Extract Sub-Concerns",
3171
+ description: "Extract sub-concerns into separate modules first. Only refactor the extraction points after tests confirm equivalence.",
3172
+ effort: "high",
3173
+ impact: "high"
3174
+ });
3175
+ }
3176
+ if (file.sideEffectProfile.includes("database_write")) {
3177
+ recommendations.push({
3178
+ strategy: "Feature Flags & Staging",
3179
+ description: "Wrap changes in a transaction or use a feature flag. Run against a staging database before production.",
3180
+ effort: "medium",
3181
+ impact: "high"
3182
+ });
3183
+ }
3184
+ if (recommendations.length === 0 && file.importedBy.length >= 5) {
3185
+ recommendations.push({
3186
+ strategy: "Review Blast Radius",
3187
+ description: "Review importedBy before patching. Run affected integration tests.",
3188
+ effort: "medium",
3189
+ impact: "medium"
3190
+ });
3191
+ }
3192
+ recommendations.sort((a, b) => {
3193
+ const ratioA = getImpactValue(a.impact) / getEffortValue(a.effort);
3194
+ const ratioB = getImpactValue(b.impact) / getEffortValue(b.effort);
3195
+ return ratioB - ratioA;
3196
+ });
3197
+ return recommendations;
3198
+ }
3199
+ };
3200
+
3201
+ // dist/export/ArtifactBundleWriter.js
3202
+ import { join as join10 } from "path";
3203
+ import { writeFile as writeFile7, mkdir as mkdir6, rm, rename } from "fs/promises";
3204
+ import { createHash } from "crypto";
3205
+ var ArtifactBundleWriter = class {
3206
+ projectRoot;
3207
+ constructor(projectRoot) {
3208
+ this.projectRoot = projectRoot;
3209
+ }
3210
+ async writeBundle(artifacts) {
3211
+ const outputDir = join10(this.projectRoot, ".vibe-splainer");
3212
+ const stagingDir = join10(this.projectRoot, ".vibe-splainer.tmp");
3213
+ try {
3214
+ await rm(stagingDir, { recursive: true, force: true });
3215
+ const { existsSync: existsSync5 } = await import("fs");
3216
+ const { cp } = await import("fs/promises");
3217
+ if (existsSync5(outputDir)) {
3218
+ await cp(outputDir, stagingDir, { recursive: true });
3219
+ } else {
3220
+ await mkdir6(stagingDir, { recursive: true });
3221
+ }
3222
+ const manifestArtifacts = [];
3223
+ for (const artifact of artifacts) {
3224
+ const destPath = join10(stagingDir, artifact.path);
3225
+ await mkdir6(join10(destPath, ".."), { recursive: true });
3226
+ await writeFile7(destPath, artifact.content);
3227
+ const contentStr = artifact.content;
3228
+ const buffer = typeof contentStr === "string" ? Buffer.from(contentStr, "utf-8") : contentStr;
3229
+ manifestArtifacts.push({
3230
+ type: artifact.type,
3231
+ path: artifact.path,
3232
+ checksum: "sha256:" + createHash("sha256").update(buffer).digest("hex"),
3233
+ sizeBytes: buffer.length
3234
+ });
3235
+ }
3236
+ const manifest = {
3237
+ schemaVersion: "1.0.0",
3238
+ generatedAt: (/* @__PURE__ */ new Date()).toISOString(),
3239
+ projectRoot: this.projectRoot,
3240
+ artifacts: manifestArtifacts
3241
+ };
3242
+ await writeFile7(join10(stagingDir, "artifact_manifest.json"), JSON.stringify(manifest, null, 2), "utf8");
3243
+ await rm(outputDir, { recursive: true, force: true });
3244
+ await rename(stagingDir, outputDir);
3245
+ } catch (err) {
3246
+ await rm(stagingDir, { recursive: true, force: true });
3247
+ throw err;
3248
+ }
3249
+ }
3250
+ };
3251
+
3252
+ // dist/export/renderers/JsonRenderer.js
3253
+ var JsonRenderer = class {
3254
+ render(viewModel, _store) {
3255
+ return [
3256
+ {
3257
+ type: "dossier",
3258
+ path: "dossier.json",
3259
+ content: JSON.stringify(viewModel, null, 2)
3260
+ }
3261
+ ];
3262
+ }
3263
+ };
3264
+
3265
+ // dist/export/renderers/HtmlRenderer.js
3266
+ import { join as join11, dirname as dirname3, relative as relative3 } from "path";
3267
+ import { fileURLToPath as fileURLToPath2 } from "url";
3268
+ import { existsSync as existsSync4, readFileSync, readdirSync, statSync } from "fs";
3269
+ var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
3270
+ function getAllFiles(dirPath, arrayOfFiles = []) {
3271
+ const files = readdirSync(dirPath);
3272
+ files.forEach(function(file) {
3273
+ const fullPath = join11(dirPath, file);
3274
+ if (statSync(fullPath).isDirectory()) {
3275
+ arrayOfFiles = getAllFiles(fullPath, arrayOfFiles);
3276
+ } else {
3277
+ arrayOfFiles.push(fullPath);
3278
+ }
3279
+ });
3280
+ return arrayOfFiles;
3281
+ }
3282
+ var HtmlRenderer = class {
3283
+ render(viewModel, _store) {
3284
+ let templateDir = join11(__dirname2, "..", "..", "ui");
3285
+ if (!existsSync4(templateDir)) {
3286
+ templateDir = join11(__dirname2, "..", "..", "..", "..", "dist", "ui");
3287
+ }
3288
+ if (!existsSync4(templateDir)) {
3289
+ console.error("[vibe-splain] UI template not found at", templateDir, "- skipping UI regeneration");
3290
+ return [];
3291
+ }
3292
+ const artifacts = [];
3293
+ const allFiles = getAllFiles(templateDir);
3294
+ for (const file of allFiles) {
3295
+ const relPath = relative3(templateDir, file);
3296
+ if (relPath === "index.html") {
3297
+ const templateHtml = readFileSync(file, "utf8");
3298
+ const injection = `<script>window.__VIBE_DOSSIER__ = ${JSON.stringify(viewModel)};</script>`;
3299
+ const bakedHtml = templateHtml.replace("<!-- VIBE_DOSSIER_INJECTION_POINT -->", injection);
3300
+ artifacts.push({
3301
+ type: "html",
3302
+ path: join11("ui", relPath),
3303
+ content: bakedHtml
3304
+ });
3305
+ } else {
3306
+ artifacts.push({
3307
+ type: "asset",
3308
+ path: join11("ui", relPath),
3309
+ content: readFileSync(file)
3310
+ });
3311
+ }
3312
+ }
3313
+ return artifacts;
3314
+ }
3315
+ };
3316
+
3317
+ // dist/export/renderers/DeltaRenderer.js
3318
+ var DeltaRenderer = class {
3319
+ render(_viewModel, store) {
3320
+ const deltaTargets = Object.values(store.files).filter((pf) => pf.isRealSource).sort((a, b) => b.gravity - a.gravity).map((pf) => ({
3321
+ path: pf.relativePath,
3322
+ gravity: Math.round(pf.gravity),
3323
+ isLoadBearing: pf.canonicalLoadBearing,
3324
+ blastRadius: pf.importedBy,
3325
+ pillarHint: pf.pillarHint
3326
+ }));
3327
+ return [
3328
+ {
3329
+ type: "delta",
3330
+ path: "delta_targets.json",
3331
+ content: JSON.stringify(deltaTargets, null, 2)
3332
+ }
3333
+ ];
3334
+ }
3335
+ };
3336
+
3337
+ // dist/export/renderers/AgentMarkdownRenderer.js
3338
+ var AgentMarkdownRenderer = class {
3339
+ budget;
3340
+ constructor(budget = 8e3) {
3341
+ this.budget = budget;
3342
+ }
3343
+ render(viewModel, store) {
3344
+ let md = `# Architectural Dossier: ${viewModel.projectRoot}
3345
+
3346
+ `;
3347
+ if (viewModel.map.brief) {
3348
+ md += `## Project Brief
3349
+ ${viewModel.map.brief}
3350
+
3351
+ `;
3352
+ }
3353
+ md += `## Stack & Entrypoints
3354
+ `;
3355
+ md += `- Stack: ${viewModel.map.stack.join(", ")}
3356
+ `;
3357
+ md += `- Entrypoints: ${viewModel.map.entrypoints.join(", ")}
3358
+
3359
+ `;
3360
+ const allDecisions = viewModel.pillars.flatMap((p) => p.decisions).concat(viewModel.wildDiscoveries);
3361
+ const uniqueDecisions = /* @__PURE__ */ new Map();
3362
+ for (const d of allDecisions) {
3363
+ if (d.primaryFile && !uniqueDecisions.has(d.primaryFile)) {
3364
+ uniqueDecisions.set(d.primaryFile, d);
3365
+ }
3366
+ }
3367
+ const tier1 = [];
3368
+ const tier2 = [];
3369
+ const tier3 = [];
3370
+ const sortedFiles = Object.values(store.files).filter((f) => f.isRealSource).sort((a, b) => b.gravity - a.gravity);
3371
+ for (const f of sortedFiles) {
3372
+ const card = uniqueDecisions.get(f.relativePath);
3373
+ const isCritical = card && card.severity >= 4;
3374
+ if (f.gravity >= 70 || isCritical) {
3375
+ tier1.push(f.relativePath);
3376
+ } else if (f.gravity >= 40 || card) {
3377
+ tier2.push(f.relativePath);
3378
+ } else {
3379
+ tier3.push(f.relativePath);
3380
+ }
3381
+ }
3382
+ md += `## Tier 1: Critical Files & Risks
3383
+
3384
+ `;
3385
+ for (const path of tier1) {
3386
+ const f = store.files[path];
3387
+ const card = uniqueDecisions.get(path);
3388
+ const recs = viewModel.recommendations[path] || [];
3389
+ md += `### ${path}
3390
+ `;
3391
+ md += `- Gravity: ${Math.round(f.gravity)} | Heat: ${Math.round(f.heat)}
3392
+ `;
3393
+ md += `- Domain: ${f.productDomain} | Role: ${f.frameworkRole}
3394
+ `;
3395
+ if (card) {
3396
+ md += `
3397
+ **Verdict**: ${card.thesis}
3398
+ `;
3399
+ md += `**Severity**: ${card.severity} | **Category**: ${card.category}
3400
+ `;
3401
+ md += `**Narrative**: ${card.narrative}
3402
+ `;
3403
+ }
3404
+ if (recs.length > 0) {
3405
+ md += `
3406
+ **Safe Patch Strategies**:
3407
+ `;
3408
+ for (const r of recs) {
3409
+ md += `- **${r.strategy}**: ${r.description}
3410
+ `;
3411
+ }
3412
+ }
3413
+ md += `
3414
+ ---
3415
+
3416
+ `;
3417
+ }
3418
+ md += `## Tier 2: Important Files
3419
+
3420
+ `;
3421
+ for (const path of tier2) {
3422
+ const f = store.files[path];
3423
+ const card = uniqueDecisions.get(path);
3424
+ md += `- **${path}** (Gravity: ${Math.round(f.gravity)})`;
3425
+ if (card) {
3426
+ md += ` \u2014 ${card.thesis}`;
3427
+ }
3428
+ md += `
3429
+ `;
3430
+ }
3431
+ md += `
3432
+ `;
3433
+ md += `## Tier 3: Index
3434
+
3435
+ `;
3436
+ for (const path of tier3) {
3437
+ const f = store.files[path];
3438
+ md += `- ${path} (Gravity: ${Math.round(f.gravity)})
3439
+ `;
3440
+ }
3441
+ return [
3442
+ {
3443
+ type: "markdown",
3444
+ path: "dossier.agent.md",
3445
+ content: md
3446
+ }
3447
+ ];
3448
+ }
3449
+ };
3450
+
3451
+ // dist/export/renderers/ValidationRenderer.js
3452
+ var ValidationRenderer = class {
3453
+ render(viewModel, _store) {
3454
+ if (!viewModel.map.validation)
3455
+ return [];
3456
+ return [
3457
+ {
3458
+ type: "validation",
3459
+ path: "validation_report.json",
3460
+ content: JSON.stringify(viewModel.map.validation, null, 2)
3461
+ }
3462
+ ];
3463
+ }
3464
+ };
3465
+
3466
+ // dist/export/renderers/RawAnalysisRenderer.js
3467
+ var RawAnalysisRenderer = class {
3468
+ render(_viewModel, store) {
3469
+ return [
3470
+ {
3471
+ type: "analysis",
3472
+ path: "analysis.json",
3473
+ content: JSON.stringify(store, null, 2)
3474
+ }
3475
+ ];
3476
+ }
3477
+ };
3478
+
3479
+ // dist/export/renderers/GraphRenderer.js
3480
+ var GraphRenderer = class {
3481
+ graph;
3482
+ constructor(graph) {
3483
+ this.graph = graph;
3484
+ }
3485
+ render(_viewModel, _store) {
3486
+ if (!this.graph)
3487
+ return [];
3488
+ return [
3489
+ {
3490
+ type: "graph",
3491
+ path: "graph.json",
3492
+ content: JSON.stringify(this.graph, null, 2)
3493
+ }
3494
+ ];
3495
+ }
3496
+ };
3497
+
3498
+ // dist/export/ExportOrchestrator.js
3499
+ var ExportOrchestrator = class {
3500
+ projectRoot;
3501
+ constructor(projectRoot) {
3502
+ this.projectRoot = projectRoot;
3503
+ }
3504
+ async writeBundle(dossier, options = {}, store, graph) {
3505
+ const finalStore = store || await readAnalysis(this.projectRoot);
3506
+ if (!finalStore) {
3507
+ throw new Error("Analysis store not found. Scan the project first.");
3508
+ }
3509
+ for (const p of dossier.pillars) {
3510
+ p.decisions = p.decisions.filter((c) => !(c.severity === 1 && c.category === "Convention"));
3511
+ p.cardCount = p.decisions.length;
3512
+ }
3513
+ const viewModel = this.buildViewModel(dossier, finalStore);
3514
+ const artifacts = [];
3515
+ artifacts.push(...await new JsonRenderer().render(viewModel, finalStore));
3516
+ artifacts.push(...await new DeltaRenderer().render(viewModel, finalStore));
3517
+ artifacts.push(...await new ValidationRenderer().render(viewModel, finalStore));
3518
+ artifacts.push(...await new RawAnalysisRenderer().render(viewModel, finalStore));
3519
+ if (graph) {
3520
+ artifacts.push(...await new GraphRenderer(graph).render(viewModel, finalStore));
3521
+ }
3522
+ const formats = ["html", "markdown"];
3523
+ if (options.format && options.format !== "json" && options.format !== "delta") {
3524
+ formats.length = 0;
3525
+ formats.push(options.format);
3526
+ }
3527
+ if (formats.includes("html")) {
3528
+ artifacts.push(...await new HtmlRenderer().render(viewModel, finalStore));
3529
+ }
3530
+ if (formats.includes("markdown")) {
3531
+ artifacts.push(...await new AgentMarkdownRenderer(options.budget).render(viewModel, finalStore));
3532
+ }
3533
+ const writer = new ArtifactBundleWriter(this.projectRoot);
3534
+ await writer.writeBundle(artifacts);
3535
+ }
3536
+ buildViewModel(dossier, store) {
3537
+ const recommendations = {};
3538
+ for (const file of dossier.map.topGravity) {
3539
+ const persisted = store.files[file];
3540
+ if (persisted) {
3541
+ recommendations[file] = RecommendationEngine.generateRecommendations(persisted);
3542
+ }
3543
+ }
3544
+ for (const file of dossier.map.topHeat) {
3545
+ if (!recommendations[file]) {
3546
+ const persisted = store.files[file];
3547
+ if (persisted) {
3548
+ recommendations[file] = RecommendationEngine.generateRecommendations(persisted);
3549
+ }
3550
+ }
3551
+ }
3552
+ return {
3553
+ ...dossier,
3554
+ recommendations
3555
+ };
3556
+ }
3557
+ };
3558
+
3559
+ // dist/export/Watcher.js
3472
3560
  import chokidar from "chokidar";
3473
3561
  import { createHash as createHash2 } from "crypto";
3474
- import { readFile as readFile10 } from "fs/promises";
3475
- import { join as join11 } from "path";
3562
+ import { readFile as readFile9 } from "fs/promises";
3563
+ import { join as join12 } from "path";
3476
3564
  function startWatcher(projectRoot, watchedPaths) {
3477
3565
  const watcher = chokidar.watch(watchedPaths.length > 0 ? watchedPaths : projectRoot, {
3478
3566
  ignoreInitial: true,
@@ -3484,14 +3572,14 @@ function startWatcher(projectRoot, watchedPaths) {
3484
3572
  const dossier = await readDossier(projectRoot);
3485
3573
  if (!dossier)
3486
3574
  return;
3487
- const content = await readFile10(filepath, "utf8");
3575
+ const content = await readFile9(filepath, "utf8");
3488
3576
  const newHash = createHash2("sha256").update(content).digest("hex");
3489
3577
  let mutated = false;
3490
3578
  for (const pillar of dossier.pillars) {
3491
3579
  for (const card of pillar.decisions) {
3492
3580
  if (!card.primaryFile)
3493
3581
  continue;
3494
- const absMatch = filepath === join11(projectRoot, card.primaryFile) || filepath.endsWith("/" + card.primaryFile);
3582
+ const absMatch = filepath === join12(projectRoot, card.primaryFile) || filepath.endsWith("/" + card.primaryFile);
3495
3583
  if (absMatch && card.lastScannedHash !== newHash) {
3496
3584
  card.status = "stale";
3497
3585
  const rel = card.primaryFile;
@@ -3501,8 +3589,11 @@ function startWatcher(projectRoot, watchedPaths) {
3501
3589
  }
3502
3590
  }
3503
3591
  }
3504
- if (mutated)
3505
- await writeDossier(projectRoot, dossier);
3592
+ if (mutated) {
3593
+ const orchestrator = new ExportOrchestrator(projectRoot);
3594
+ await orchestrator.writeBundle(dossier);
3595
+ console.error(`[vibe-splain] File changed: ${filepath}. Dossier artifacts updated.`);
3596
+ }
3506
3597
  } catch (err) {
3507
3598
  console.error("[vibe-splain] Watcher error:", err);
3508
3599
  }
@@ -3525,7 +3616,7 @@ var scanProjectTool = {
3525
3616
  required: ["projectRoot"]
3526
3617
  }
3527
3618
  };
3528
- async function handleScanProject(args) {
3619
+ async function handleScanProject(args, options = {}) {
3529
3620
  const projectRoot = args.projectRoot;
3530
3621
  if (!projectRoot)
3531
3622
  throw new Error("projectRoot is required");
@@ -3537,7 +3628,15 @@ async function handleScanProject(args) {
3537
3628
  version: "2.0.0",
3538
3629
  scannedAt: (/* @__PURE__ */ new Date()).toISOString(),
3539
3630
  projectRoot,
3540
- map: { ...result.map, brief },
3631
+ map: {
3632
+ ...result.map,
3633
+ brief,
3634
+ validation: result.validation ? {
3635
+ passed: result.validation.passed,
3636
+ errors: result.validation.errors,
3637
+ warnings: result.validation.warnings
3638
+ } : void 0
3639
+ },
3541
3640
  pillars: existing?.pillars ?? [],
3542
3641
  wildDiscoveries: existing?.wildDiscoveries ?? [],
3543
3642
  stalePaths: existing?.stalePaths ?? []
@@ -3547,12 +3646,22 @@ async function handleScanProject(args) {
3547
3646
  dossier.pillars.push({ name: def.name, cardCount: 0, decisions: [] });
3548
3647
  }
3549
3648
  }
3550
- await writeDossier(projectRoot, dossier);
3649
+ const orchestrator = new ExportOrchestrator(projectRoot);
3650
+ await orchestrator.writeBundle(dossier, {
3651
+ format: options.format,
3652
+ budget: options.budget ? parseInt(options.budget, 10) : void 0,
3653
+ scope: options.scope
3654
+ }, result.store, result.graph);
3551
3655
  startWatcher(projectRoot, result.files.map((f) => f.path));
3552
3656
  console.error(`[vibe-splain] Scan complete. ${result.totalFilesScanned} files, ${result.realSourceCount} real-source, ${result.wildCandidates.length} wild candidates.`);
3553
3657
  const validation = result.validation ?? { passed: true, errors: 0, warnings: 0, reportPath: ".vibe-splainer/validation_report.json" };
3658
+ let statusMsg = "Scan complete.";
3659
+ if (!validation.passed) {
3660
+ statusMsg = `SCAN QUALITY WARNING: ${validation.errors} errors and ${validation.warnings} warnings found in validation report. Delta Engine automation may be blocked.`;
3661
+ }
3554
3662
  return {
3555
3663
  ok: true,
3664
+ message: statusMsg,
3556
3665
  validation: {
3557
3666
  passed: validation.passed,
3558
3667
  errors: validation.errors,
@@ -3636,7 +3745,7 @@ var setProjectBriefTool = {
3636
3745
  required: ["projectRoot", "brief"]
3637
3746
  }
3638
3747
  };
3639
- async function handleSetProjectBrief(args) {
3748
+ async function handleSetProjectBrief(args, options = {}) {
3640
3749
  const projectRoot = args.projectRoot;
3641
3750
  const brief = args.brief;
3642
3751
  if (!projectRoot || !brief)
@@ -3646,7 +3755,12 @@ async function handleSetProjectBrief(args) {
3646
3755
  return { error: "No project map found. Run scan_project first." };
3647
3756
  }
3648
3757
  dossier.map.brief = brief;
3649
- await writeDossier(projectRoot, dossier);
3758
+ const orchestrator = new ExportOrchestrator(projectRoot);
3759
+ await orchestrator.writeBundle(dossier, {
3760
+ format: options.format,
3761
+ budget: options.budget ? parseInt(options.budget, 10) : void 0,
3762
+ scope: options.scope
3763
+ });
3650
3764
  const documented = new Set([...dossier.pillars.flatMap((p) => p.decisions), ...dossier.wildDiscoveries].map((c) => c.primaryFile).filter(Boolean));
3651
3765
  const startHere = dossier.map.topGravity.filter((f) => !documented.has(f));
3652
3766
  const wild = dossier.map.topHeat.filter((f) => !documented.has(f));
@@ -3661,8 +3775,8 @@ async function handleSetProjectBrief(args) {
3661
3775
  }
3662
3776
 
3663
3777
  // dist/mcp/tools/get_file_context.js
3664
- import { readFile as readFile11 } from "fs/promises";
3665
- import { join as join12, relative as relative3, isAbsolute } from "path";
3778
+ import { readFile as readFile10 } from "fs/promises";
3779
+ import { join as join13, relative as relative4, isAbsolute } from "path";
3666
3780
  var getFileContextTool = {
3667
3781
  name: "get_file_context",
3668
3782
  description: "Returns PRE-EXTRACTED evidence for a file so you do not have to read the whole thing and paraphrase its header comment. Returns: gravity/heat scores + signals, importedBy (named fan-in \u2014 use this for blastRadius), hotSpans (the gnarliest function bodies, comment-stripped, each with a reason), smellSpans (located tech debt with \xB13 lines of context), and signature (the exported API surface). Base your evidence on hotSpans/smellSpans \u2014 NEVER on header comments. Pass { full: true } only if you truly need the raw source.",
@@ -3682,8 +3796,8 @@ async function handleGetFileContext(args) {
3682
3796
  const full = args.full === true;
3683
3797
  if (!projectRoot || !filePath)
3684
3798
  throw new Error("projectRoot and filePath are required");
3685
- const fullPath = isAbsolute(filePath) ? filePath : join12(projectRoot, filePath);
3686
- const relPath = relative3(projectRoot, fullPath);
3799
+ const fullPath = isAbsolute(filePath) ? filePath : join13(projectRoot, filePath);
3800
+ const relPath = relative4(projectRoot, fullPath);
3687
3801
  const evidence = await getFileAnalysis(fullPath);
3688
3802
  if (!evidence) {
3689
3803
  throw new Error(`Could not analyze ${relPath} (unsupported language or parse failure).`);
@@ -3707,7 +3821,7 @@ async function handleGetFileContext(args) {
3707
3821
  smellSpans: evidence.smellSpans
3708
3822
  };
3709
3823
  if (full) {
3710
- result.source = await readFile11(fullPath, "utf8");
3824
+ result.source = await readFile10(fullPath, "utf8");
3711
3825
  }
3712
3826
  return result;
3713
3827
  }
@@ -3715,8 +3829,8 @@ async function handleGetFileContext(args) {
3715
3829
  // dist/mcp/tools/write_decision_card.js
3716
3830
  import { v4 as uuidv4 } from "uuid";
3717
3831
  import { createHash as createHash3 } from "crypto";
3718
- import { readFile as readFile12 } from "fs/promises";
3719
- import { join as join13 } from "path";
3832
+ import { readFile as readFile11 } from "fs/promises";
3833
+ import { join as join14 } from "path";
3720
3834
  var CATEGORIES = ["Bottleneck", "Hack", "Smart-Move", "Risk", "Convention", "Dead-Weight"];
3721
3835
  function normalizeSnippet(s) {
3722
3836
  let out = (s ?? "").replace(/\r\n/g, "\n");
@@ -3761,7 +3875,7 @@ var writeDecisionCardTool = {
3761
3875
  required: ["projectRoot", "pillar", "primaryFile", "title", "thesis", "category", "severity", "narrative", "confidence", "evidence"]
3762
3876
  }
3763
3877
  };
3764
- async function handleWriteDecisionCard(args) {
3878
+ async function handleWriteDecisionCard(args, options = {}) {
3765
3879
  const projectRoot = args.projectRoot;
3766
3880
  const pillar = args.pillar;
3767
3881
  const primaryFile = args.primaryFile;
@@ -3804,11 +3918,16 @@ async function handleWriteDecisionCard(args) {
3804
3918
  }
3805
3919
  const store = await readAnalysis(projectRoot);
3806
3920
  const persisted = store?.files[primaryFile];
3921
+ const machineConfidence = persisted?.confidence || "low";
3922
+ const confidenceOrder = { low: 0, medium: 1, high: 2 };
3923
+ if (confidenceOrder[confidence] > confidenceOrder[machineConfidence]) {
3924
+ throw new Error(`Requested confidence "${confidence}" exceeds machine-derived confidence "${machineConfidence}" for this file. Ground your narrative in the MRI evidence.`);
3925
+ }
3807
3926
  const gravity = persisted ? Math.round(persisted.gravity) : void 0;
3808
3927
  const heat = persisted ? Math.round(persisted.heat) : void 0;
3809
3928
  let primaryContent = "";
3810
3929
  try {
3811
- primaryContent = await readFile12(join13(projectRoot, primaryFile), "utf8");
3930
+ primaryContent = await readFile11(join14(projectRoot, primaryFile), "utf8");
3812
3931
  } catch {
3813
3932
  }
3814
3933
  const hash = createHash3("sha256").update(primaryContent).digest("hex");
@@ -3842,7 +3961,12 @@ async function handleWriteDecisionCard(args) {
3842
3961
  }
3843
3962
  bucket.decisions.push(card);
3844
3963
  bucket.cardCount = bucket.decisions.length;
3845
- await writeDossier(projectRoot, dossier);
3964
+ const orchestrator = new ExportOrchestrator(projectRoot);
3965
+ await orchestrator.writeBundle(dossier, {
3966
+ format: options.format,
3967
+ budget: options.budget ? parseInt(options.budget, 10) : void 0,
3968
+ scope: options.scope
3969
+ });
3846
3970
  console.error(`[vibe-splain] Card written: "${title}" [${category} sev${severity}] in "${pillar}"${isWild ? " (Wild Discovery)" : ""}`);
3847
3971
  const documented = new Set([...dossier.pillars.flatMap((p) => p.decisions), ...dossier.wildDiscoveries].map((c) => c.primaryFile).filter(Boolean));
3848
3972
  const remaining = [.../* @__PURE__ */ new Set([...dossier.map.topGravity, ...dossier.map.topHeat])].filter((f) => !documented.has(f));
@@ -3992,7 +4116,7 @@ var markStaleTool = {
3992
4116
  required: ["projectRoot", "filePaths"]
3993
4117
  }
3994
4118
  };
3995
- async function handleMarkStale(args) {
4119
+ async function handleMarkStale(args, options = {}) {
3996
4120
  const projectRoot = args.projectRoot;
3997
4121
  const filePaths = args.filePaths;
3998
4122
  if (!projectRoot || !filePaths)
@@ -4020,7 +4144,12 @@ async function handleMarkStale(args) {
4020
4144
  dossier.stalePaths.push(filePath);
4021
4145
  }
4022
4146
  }
4023
- await writeDossier(projectRoot, dossier);
4147
+ const orchestrator = new ExportOrchestrator(projectRoot);
4148
+ await orchestrator.writeBundle(dossier, {
4149
+ format: options.format,
4150
+ budget: options.budget ? parseInt(options.budget, 10) : void 0,
4151
+ scope: options.scope
4152
+ });
4024
4153
  return {
4025
4154
  success: true,
4026
4155
  staleCardsMarked: staleCount,
@@ -4102,10 +4231,10 @@ var TOOL_HANDLERS = {
4102
4231
  get_wild_discoveries: handleGetWildDiscoveries,
4103
4232
  mark_stale: handleMarkStale
4104
4233
  };
4105
- async function startMCPServer() {
4234
+ async function startMCPServer(options = {}) {
4106
4235
  await initParser2();
4107
4236
  console.error("[vibe-splain] Tree-Sitter parser initialized");
4108
- const server = new Server({ name: "vibe-splain", version: "2.0.0" }, { capabilities: { tools: {}, prompts: {} } });
4237
+ const server = new Server({ name: "vibe-splain", version: "3.0.0" }, { capabilities: { tools: {}, prompts: {} } });
4109
4238
  server.setRequestHandler(ListPromptsRequestSchema, async () => ({
4110
4239
  prompts: [
4111
4240
  {
@@ -4202,7 +4331,7 @@ When done, share the exact file:// UI link returned by scan_project. Never inven
4202
4331
  };
4203
4332
  }
4204
4333
  try {
4205
- const result = await handler(args || {});
4334
+ const result = await handler(args, options);
4206
4335
  return {
4207
4336
  content: [{ type: "text", text: JSON.stringify(result, null, 2) }]
4208
4337
  };
@@ -4221,14 +4350,33 @@ When done, share the exact file:// UI link returned by scan_project. Never inven
4221
4350
  }
4222
4351
 
4223
4352
  // dist/commands/serve.js
4224
- async function serveCommand() {
4353
+ async function serveCommand(options) {
4225
4354
  console.error("[vibe-splain] Starting MCP server...");
4226
- await startMCPServer();
4355
+ await startMCPServer(options);
4356
+ }
4357
+
4358
+ // dist/commands/export.js
4359
+ async function exportCommand(projectRoot, options) {
4360
+ const root = projectRoot || process.cwd();
4361
+ console.error(`[vibe-splain] Exporting dossier for ${root}`);
4362
+ const dossier = await readDossier(root);
4363
+ if (!dossier) {
4364
+ console.error("[vibe-splain] Dossier not found. Run scan first.");
4365
+ process.exit(1);
4366
+ }
4367
+ const orchestrator = new ExportOrchestrator(root);
4368
+ await orchestrator.writeBundle(dossier, {
4369
+ format: options.format,
4370
+ budget: options.budget ? parseInt(options.budget, 10) : void 0,
4371
+ scope: options.scope
4372
+ });
4373
+ console.error("[vibe-splain] Export complete.");
4227
4374
  }
4228
4375
 
4229
4376
  // dist/index.js
4230
4377
  var program = new Command();
4231
- program.name("vibe-splain").description("Architectural dossier engine for vibe-coded projects").version("2.5.0");
4378
+ program.name("vibe-splain").description("Architectural dossier engine for vibe-coded projects").version("3.0.0");
4232
4379
  program.command("install").description("Patch coding agent MCP config files to register vibe-splain").action(installCommand);
4233
- program.command("serve").description("Start the MCP server (called by the coding agent, not by you)").action(serveCommand);
4380
+ program.command("serve").description("Start the MCP server (called by the coding agent, not by you)").option("--format <format>", "Export format (html, markdown, etc.)").option("--budget <budget>", "Token budget for markdown").option("--scope <scope>", "Scope for export").action((options) => serveCommand(options));
4381
+ program.command("export [projectRoot]").description("Manually trigger bundle generation").option("--format <format>", "Export format (html, markdown, etc.)").option("--budget <budget>", "Token budget for markdown").option("--scope <scope>", "Scope for export").action(exportCommand);
4234
4382
  program.parse();