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.
- package/dist/commands/install.js +3 -2
- package/dist/index.js +151 -20
- package/package.json +1 -1
package/dist/commands/install.js
CHANGED
|
@@ -13,8 +13,9 @@ function expandPath(p) {
|
|
|
13
13
|
return p;
|
|
14
14
|
}
|
|
15
15
|
const AGENT_CONFIGS = [
|
|
16
|
-
{ name: 'Claude Code', path: '~/.claude/
|
|
17
|
-
{ name: '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/
|
|
23
|
-
{ name: "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
|
|
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
|
|
993
|
-
const snippet = stripLeadingComments(
|
|
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.
|
|
2310
|
-
evidenceHash: createHash("sha256").update(span.
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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
|
|
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