vibe-splain 2.5.0 → 2.6.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.
@@ -13,8 +13,9 @@ function expandPath(p) {
13
13
  return p;
14
14
  }
15
15
  const AGENT_CONFIGS = [
16
- { name: 'Claude Code', path: '~/.claude/claude_desktop_config.json', format: 'claude' },
17
- { name: 'Claude Code (Windows)', path: '%APPDATA%/Claude/claude_desktop_config.json', format: 'claude' },
16
+ { name: 'Claude Code CLI', path: '~/.claude/settings.json', format: 'claude' },
17
+ { name: 'Claude Desktop', path: '~/.claude/claude_desktop_config.json', format: 'claude' },
18
+ { name: 'Claude Desktop (Windows)', path: '%APPDATA%/Claude/claude_desktop_config.json', format: 'claude' },
18
19
  { name: 'Gemini CLI', path: '~/.gemini/settings.json', format: 'gemini' },
19
20
  { name: 'Cursor', path: '~/.cursor/mcp.json', format: 'cursor' },
20
21
  { name: 'Windsurf', path: '~/.codeium/windsurf/mcp_config.json', format: 'cursor' },
package/dist/index.js CHANGED
@@ -19,8 +19,9 @@ function expandPath(p) {
19
19
  return p;
20
20
  }
21
21
  var AGENT_CONFIGS = [
22
- { name: "Claude Code", path: "~/.claude/claude_desktop_config.json", format: "claude" },
23
- { name: "Claude Code (Windows)", path: "%APPDATA%/Claude/claude_desktop_config.json", format: "claude" },
22
+ { name: "Claude Code CLI", path: "~/.claude/settings.json", format: "claude" },
23
+ { name: "Claude Desktop", path: "~/.claude/claude_desktop_config.json", format: "claude" },
24
+ { name: "Claude Desktop (Windows)", path: "%APPDATA%/Claude/claude_desktop_config.json", format: "claude" },
24
25
  { name: "Gemini CLI", path: "~/.gemini/settings.json", format: "gemini" },
25
26
  { name: "Cursor", path: "~/.cursor/mcp.json", format: "cursor" },
26
27
  { name: "Windsurf", path: "~/.codeium/windsurf/mcp_config.json", format: "cursor" }
@@ -88,7 +89,7 @@ import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema
88
89
 
89
90
  // ../brain/dist/scanner.js
90
91
  import { extname as extname4 } from "path";
91
- import { readFile as readFile6 } from "fs/promises";
92
+ import { readFile as readFile7 } from "fs/promises";
92
93
 
93
94
  // ../brain/dist/pipeline/orchestrator.js
94
95
  import { join as join8 } from "path";
@@ -989,11 +990,12 @@ function analyzeAst(source, lang, tree) {
989
990
  }
990
991
  scored.sort((a, b) => b.score - a.score);
991
992
  const hotSpans = scored.slice(0, 3).filter((s) => s.bodyLOC >= 4).map((s) => {
992
- const raw = source.split("\n").slice(s.node.startPosition.row, s.node.endPosition.row + 1).join("\n");
993
- const snippet = stripLeadingComments(raw).slice(0, 2e3);
993
+ const rawExcerpt = source.split("\n").slice(s.node.startPosition.row, s.node.endPosition.row + 1).join("\n");
994
+ const snippet = stripLeadingComments(rawExcerpt).slice(0, 2e3);
994
995
  return {
995
996
  startLine: s.node.startPosition.row + 1,
996
997
  endLine: s.node.endPosition.row + 1,
998
+ rawExcerpt,
997
999
  snippet,
998
1000
  reason: `high complexity: ${s.decisions} decision branches across ${s.bodyLOC} lines`
999
1001
  };
@@ -2051,7 +2053,7 @@ async function runClassification(projectRoot, inv, res) {
2051
2053
 
2052
2054
  // ../brain/dist/pipeline/scoring.js
2053
2055
  import { join as join7 } from "path";
2054
- import { writeFile as writeFile7, mkdir as mkdir6 } from "fs/promises";
2056
+ import { writeFile as writeFile7, mkdir as mkdir6, readFile as readFile6 } from "fs/promises";
2055
2057
  import { createHash } from "crypto";
2056
2058
  function computeSeverity(sideEffectProfile, productDomain, gravity, heat, maxNesting, hasLongFunctions, swallowedCatches, runtimeEntrypoints) {
2057
2059
  let score = 0;
@@ -2113,6 +2115,11 @@ function applyCorrections(file) {
2113
2115
  }
2114
2116
  if (file.canonicalSeverity === 5)
2115
2117
  file.canonicalLoadBearing = true;
2118
+ if (file.riskTypes.includes("registry_bottleneck")) {
2119
+ if (file.canonicalSeverity < 4)
2120
+ file.canonicalSeverity = 4;
2121
+ file.canonicalLoadBearing = true;
2122
+ }
2116
2123
  }
2117
2124
  function inferObservableOutputs(frameworkRole, productDomain, sideEffectProfile) {
2118
2125
  const outputs = [];
@@ -2141,6 +2148,9 @@ function inferObservableOutputs(frameworkRole, productDomain, sideEffectProfile)
2141
2148
  outputs.push("sdk_event_name");
2142
2149
  if (frameworkRole === "hook" || frameworkRole === "store")
2143
2150
  outputs.push("ui_state_transition");
2151
+ if (productDomain === "data_table" && frameworkRole === "provider") {
2152
+ outputs.push("ui_state_transition", "filter_state", "selected_segment");
2153
+ }
2144
2154
  return [...new Set(outputs)];
2145
2155
  }
2146
2156
  function inferPatchRisk(productDomain, riskTypes, sideEffectProfile, importedByCount, loadBearingScore) {
@@ -2157,9 +2167,21 @@ function inferPatchRisk(productDomain, riskTypes, sideEffectProfile, importedByC
2157
2167
  reason: `${productDomain} writes to external state (${external.join(", ") || "database"}). Changes require integration testing.`
2158
2168
  };
2159
2169
  }
2170
+ if (riskTypes.includes("registry_bottleneck")) {
2171
+ return {
2172
+ level: "high",
2173
+ reason: "registry_bottleneck: central dispatch point \u2014 blast radius not measurable by fan-in alone."
2174
+ };
2175
+ }
2160
2176
  if (loadBearingScore >= 5 || importedByCount >= 5) {
2161
2177
  return { level: "medium", reason: `Imported by ${importedByCount} files. Interface changes will cascade.` };
2162
2178
  }
2179
+ if (productDomain === "data_table" && riskTypes.includes("state_machine")) {
2180
+ return {
2181
+ level: "medium",
2182
+ reason: "data_table state machine: controls user-visible workflow state (filters, segments, pagination) \u2014 regression risk not captured by mutation scoring."
2183
+ };
2184
+ }
2163
2185
  return { level: "low", reason: "Locally contained \u2014 limited blast radius." };
2164
2186
  }
2165
2187
  function inferSafePatchStrategy(riskTypes, sideEffectProfile) {
@@ -2306,8 +2328,15 @@ async function runScoring(projectRoot, cr) {
2306
2328
  file: pf.relativePath,
2307
2329
  startLine: span.startLine,
2308
2330
  endLine: span.endLine,
2309
- rawSourceExcerpt: span.snippet,
2310
- evidenceHash: createHash("sha256").update(span.snippet).digest("hex").slice(0, 12)
2331
+ rawSourceExcerpt: span.rawExcerpt,
2332
+ evidenceHash: createHash("sha256").update(span.rawExcerpt).digest("hex").slice(0, 12)
2333
+ }));
2334
+ const displayEvidence = pf.hotSpans.map((span) => ({
2335
+ file: pf.relativePath,
2336
+ startLine: span.startLine,
2337
+ endLine: span.endLine,
2338
+ excerpt: span.snippet,
2339
+ isTruncated: span.rawExcerpt.length > 2e3
2311
2340
  }));
2312
2341
  return {
2313
2342
  path: pf.relativePath,
@@ -2332,6 +2361,7 @@ async function runScoring(projectRoot, cr) {
2332
2361
  doNotTouch: inferDoNotTouch(pf.sideEffectProfile, pf.productDomain),
2333
2362
  testProbes: inferTestProbes(pf.writeIntents, observableOutputs),
2334
2363
  rawEvidence,
2364
+ displayEvidence,
2335
2365
  analysisAnnotation: `${pf.frameworkRole} in ${pf.productDomain} domain. fanIn=${pf.gravitySignals.fanIn} cyclomatic=${pf.gravitySignals.cyclomatic} loc=${pf.gravitySignals.loc}`,
2336
2366
  hashes: { fileHash, evidenceHash: rawEvidence.map((e) => e.evidenceHash).join("-") }
2337
2367
  };
@@ -2341,7 +2371,7 @@ async function runScoring(projectRoot, cr) {
2341
2371
  await writeFile7(tmp, JSON.stringify(deltaTargets, null, 2), "utf8");
2342
2372
  const { rename } = await import("fs/promises");
2343
2373
  await rename(tmp, dest);
2344
- const validationReport = buildValidationReport(store, deltaTargets);
2374
+ const validationReport = await buildValidationReport(store, deltaTargets, projectRoot);
2345
2375
  await writeFile7(join7(dir, "validation_report.json"), JSON.stringify(validationReport, null, 2), "utf8");
2346
2376
  for (const e of validationReport.errors) {
2347
2377
  console.error(`[vibe-splain] VALIDATION ERROR [${e.rule}] ${e.file}: ${e.detail}`);
@@ -2351,7 +2381,7 @@ async function runScoring(projectRoot, cr) {
2351
2381
  }
2352
2382
  return { store, deltaTargets, validationReport };
2353
2383
  }
2354
- function buildValidationReport(store, deltaTargets) {
2384
+ async function buildValidationReport(store, deltaTargets, projectRoot) {
2355
2385
  const errors = [];
2356
2386
  const warnings = [];
2357
2387
  let passCount = 0;
@@ -2421,8 +2451,109 @@ function buildValidationReport(store, deltaTargets) {
2421
2451
  detail: `Entrypoints found but domain surface mismatch for ${pf.productDomain}. Found: ${foundPaths}`
2422
2452
  });
2423
2453
  }
2454
+ if (pf.riskTypes.includes("registry_bottleneck")) {
2455
+ if (pf.canonicalSeverity < 4)
2456
+ errors.push({
2457
+ file: pf.relativePath,
2458
+ rule: "registry_bottleneck_severity",
2459
+ detail: "registry_bottleneck file must have severity >= 4",
2460
+ expected: ">=4",
2461
+ actual: String(pf.canonicalSeverity)
2462
+ });
2463
+ if (!pf.canonicalLoadBearing)
2464
+ errors.push({
2465
+ file: pf.relativePath,
2466
+ rule: "registry_bottleneck_load_bearing",
2467
+ detail: "registry_bottleneck file must be load-bearing",
2468
+ expected: "true",
2469
+ actual: "false"
2470
+ });
2471
+ if (delta && delta.patchRisk.level !== "high" && delta.patchRisk.level !== "critical")
2472
+ errors.push({
2473
+ file: pf.relativePath,
2474
+ rule: "registry_bottleneck_patch_risk",
2475
+ detail: "registry_bottleneck file must have patch risk high or critical",
2476
+ expected: "high|critical",
2477
+ actual: delta?.patchRisk.level ?? "unknown"
2478
+ });
2479
+ }
2480
+ if (pf.productDomain === "data_table" && pf.riskTypes.includes("state_machine") && delta?.patchRisk.level === "low") {
2481
+ warnings.push({
2482
+ file: pf.relativePath,
2483
+ rule: "data_table_state_machine_risk",
2484
+ detail: "data_table state machine should have at least medium patch risk"
2485
+ });
2486
+ }
2424
2487
  passCount++;
2425
2488
  }
2489
+ const PAYMENT_PROVIDER_PATH_TERMS = ["stripe", "paypal", "btcpay", "btcpayserver", "alby", "hitpay", "payment"];
2490
+ const PAYMENT_CONTENT_TERMS = [
2491
+ "constructEvent",
2492
+ "checkoutSession",
2493
+ "paymentIntent",
2494
+ "stripe-signature",
2495
+ "webhook-signature",
2496
+ "payment_mutation",
2497
+ "paymentStatus",
2498
+ "invoicePaid",
2499
+ "chargeSucceeded"
2500
+ ];
2501
+ for (const [rel, pf] of Object.entries(store.files)) {
2502
+ if (!pf.isRealSource)
2503
+ continue;
2504
+ const pathLower = rel.toLowerCase();
2505
+ if (!pathLower.includes("webhook"))
2506
+ continue;
2507
+ const primaryTrigger = PAYMENT_PROVIDER_PATH_TERMS.some((t) => pathLower.includes(t));
2508
+ let secondaryTrigger = false;
2509
+ if (!primaryTrigger && pf.productDomain !== "payments_webhooks") {
2510
+ try {
2511
+ const src = await readFile6(join7(projectRoot, rel), "utf8");
2512
+ secondaryTrigger = PAYMENT_CONTENT_TERMS.some((t) => src.includes(t));
2513
+ } catch {
2514
+ }
2515
+ }
2516
+ if (!primaryTrigger && !secondaryTrigger)
2517
+ continue;
2518
+ const delta = deltaByPath.get(rel);
2519
+ const triggerLabel = primaryTrigger ? "path" : "content";
2520
+ const webhookChecks = [
2521
+ [
2522
+ pf.productDomain !== "payments_webhooks",
2523
+ "webhook_domain",
2524
+ `Payment webhook (${triggerLabel} trigger) not classified as payments_webhooks`
2525
+ ],
2526
+ [
2527
+ !pf.sideEffectProfile.includes("webhook_ingress"),
2528
+ "webhook_ingress_missing",
2529
+ `Payment webhook (${triggerLabel} trigger) missing webhook_ingress side effect`
2530
+ ],
2531
+ [
2532
+ !pf.sideEffectProfile.includes("payment_mutation"),
2533
+ "webhook_payment_mutation_missing",
2534
+ `Payment webhook (${triggerLabel} trigger) missing payment_mutation side effect`
2535
+ ],
2536
+ [
2537
+ !pf.writeIntents.includes("handle_payment_webhook"),
2538
+ "webhook_write_intent_missing",
2539
+ `Payment webhook (${triggerLabel} trigger) missing handle_payment_webhook write intent`
2540
+ ],
2541
+ [
2542
+ !!delta && delta.patchRisk.level !== "high" && delta.patchRisk.level !== "critical",
2543
+ "webhook_patch_risk",
2544
+ `Payment webhook (${triggerLabel} trigger) patchRisk must be high or critical`
2545
+ ],
2546
+ [
2547
+ !pf.canonicalLoadBearing,
2548
+ "webhook_load_bearing",
2549
+ `Payment webhook (${triggerLabel} trigger) must be load-bearing`
2550
+ ]
2551
+ ];
2552
+ for (const [condition, rule, detail] of webhookChecks) {
2553
+ if (condition)
2554
+ errors.push({ file: rel, rule, detail });
2555
+ }
2556
+ }
2426
2557
  return {
2427
2558
  timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2428
2559
  passed: errors.length === 0,
@@ -2505,7 +2636,7 @@ async function getFileAnalysis(absPath) {
2505
2636
  return null;
2506
2637
  let source;
2507
2638
  try {
2508
- source = await readFile6(absPath, "utf8");
2639
+ source = await readFile7(absPath, "utf8");
2509
2640
  } catch {
2510
2641
  return null;
2511
2642
  }
@@ -2546,14 +2677,14 @@ async function getFileAnalysis(absPath) {
2546
2677
  import { Mutex } from "async-mutex";
2547
2678
  import { join as join9, dirname as dirname3 } from "path";
2548
2679
  import { fileURLToPath as fileURLToPath2 } from "url";
2549
- import { readFile as readFile7, writeFile as writeFile8, mkdir as mkdir7 } from "fs/promises";
2680
+ import { readFile as readFile8, writeFile as writeFile8, mkdir as mkdir7 } from "fs/promises";
2550
2681
  import { existsSync as existsSync4, cpSync } from "fs";
2551
2682
  var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
2552
2683
  var dossierMutex = new Mutex();
2553
2684
  async function readDossier(projectRoot) {
2554
2685
  const dossierPath = join9(projectRoot, ".vibe-splainer", "dossier.json");
2555
2686
  try {
2556
- const raw = await readFile7(dossierPath, "utf8");
2687
+ const raw = await readFile8(dossierPath, "utf8");
2557
2688
  return JSON.parse(raw);
2558
2689
  } catch {
2559
2690
  return null;
@@ -2587,7 +2718,7 @@ async function regenerateUI(projectRoot, dossier) {
2587
2718
  return;
2588
2719
  }
2589
2720
  cpSync(templateDir, uiDir, { recursive: true });
2590
- let html = await readFile7(join9(templateDir, "index.html"), "utf8");
2721
+ let html = await readFile8(join9(templateDir, "index.html"), "utf8");
2591
2722
  const injection = `<script>window.__VIBE_DOSSIER__ = ${JSON.stringify(dossier)};</script>`;
2592
2723
  html = html.replace("<!-- VIBE_DOSSIER_INJECTION_POINT -->", injection);
2593
2724
  await writeFile8(join9(uiDir, "index.html"), html, "utf8");
@@ -2611,7 +2742,7 @@ function validateMermaidNodeCount(diagram) {
2611
2742
  // ../brain/dist/watcher.js
2612
2743
  import chokidar from "chokidar";
2613
2744
  import { createHash as createHash2 } from "crypto";
2614
- import { readFile as readFile8 } from "fs/promises";
2745
+ import { readFile as readFile9 } from "fs/promises";
2615
2746
  import { join as join10 } from "path";
2616
2747
  function startWatcher(projectRoot, watchedPaths) {
2617
2748
  const watcher = chokidar.watch(watchedPaths.length > 0 ? watchedPaths : projectRoot, {
@@ -2624,7 +2755,7 @@ function startWatcher(projectRoot, watchedPaths) {
2624
2755
  const dossier = await readDossier(projectRoot);
2625
2756
  if (!dossier)
2626
2757
  return;
2627
- const content = await readFile8(filepath, "utf8");
2758
+ const content = await readFile9(filepath, "utf8");
2628
2759
  const newHash = createHash2("sha256").update(content).digest("hex");
2629
2760
  let mutated = false;
2630
2761
  for (const pillar of dossier.pillars) {
@@ -2801,7 +2932,7 @@ async function handleSetProjectBrief(args) {
2801
2932
  }
2802
2933
 
2803
2934
  // dist/mcp/tools/get_file_context.js
2804
- import { readFile as readFile9 } from "fs/promises";
2935
+ import { readFile as readFile10 } from "fs/promises";
2805
2936
  import { join as join11, relative as relative3, isAbsolute } from "path";
2806
2937
  var getFileContextTool = {
2807
2938
  name: "get_file_context",
@@ -2847,7 +2978,7 @@ async function handleGetFileContext(args) {
2847
2978
  smellSpans: evidence.smellSpans
2848
2979
  };
2849
2980
  if (full) {
2850
- result.source = await readFile9(fullPath, "utf8");
2981
+ result.source = await readFile10(fullPath, "utf8");
2851
2982
  }
2852
2983
  return result;
2853
2984
  }
@@ -2855,7 +2986,7 @@ async function handleGetFileContext(args) {
2855
2986
  // dist/mcp/tools/write_decision_card.js
2856
2987
  import { v4 as uuidv4 } from "uuid";
2857
2988
  import { createHash as createHash3 } from "crypto";
2858
- import { readFile as readFile10 } from "fs/promises";
2989
+ import { readFile as readFile11 } from "fs/promises";
2859
2990
  import { join as join12 } from "path";
2860
2991
  var CATEGORIES = ["Bottleneck", "Hack", "Smart-Move", "Risk", "Convention", "Dead-Weight"];
2861
2992
  function normalizeSnippet(s) {
@@ -2948,7 +3079,7 @@ async function handleWriteDecisionCard(args) {
2948
3079
  const heat = persisted ? Math.round(persisted.heat) : void 0;
2949
3080
  let primaryContent = "";
2950
3081
  try {
2951
- primaryContent = await readFile10(join12(projectRoot, primaryFile), "utf8");
3082
+ primaryContent = await readFile11(join12(projectRoot, primaryFile), "utf8");
2952
3083
  } catch {
2953
3084
  }
2954
3085
  const hash = createHash3("sha256").update(primaryContent).digest("hex");
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "vibe-splain",
3
- "version": "2.5.0",
3
+ "version": "2.6.0",
4
4
  "description": "Architectural dossier engine for vibe-coded TypeScript/JavaScript projects. Runs as an MCP server inside your coding agent.",
5
5
  "type": "module",
6
6
  "license": "MIT",