vibe-splain 2.7.3 → 3.1.0

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