vibe-splain 3.5.0 → 4.0.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +110 -158
- package/dist/commands/install.d.ts +0 -5
- package/dist/commands/install.js +5 -55
- package/dist/commands/serve.js +1 -1
- package/dist/export/ArtifactBundleWriter.js +3 -3
- package/dist/export/ExportOrchestrator.d.ts +2 -20
- package/dist/export/ExportOrchestrator.js +5 -116
- package/dist/export/Watcher.js +5 -5
- package/dist/export/renderers/AgentMarkdownRenderer.d.ts +1 -1
- package/dist/export/renderers/GraphRenderer.d.ts +2 -2
- package/dist/export/renderers/HtmlRenderer.d.ts +1 -1
- package/dist/export/renderers/HtmlRenderer.js +1 -1
- package/dist/export/renderers/JsonRenderer.d.ts +1 -1
- package/dist/export/renderers/RawAnalysisRenderer.d.ts +1 -1
- package/dist/export/renderers/Renderer.d.ts +1 -1
- package/dist/export/renderers/ValidationRenderer.d.ts +1 -1
- package/dist/index.js +441 -3787
- package/dist/mcp/server.js +6 -23
- package/dist/mcp/tools/get_file_context.js +1 -1
- package/dist/mcp/tools/get_project_map.js +1 -1
- package/dist/mcp/tools/get_strategic_overview.js +1 -1
- package/dist/mcp/tools/get_wild_discoveries.js +1 -1
- package/dist/mcp/tools/inspect_pillar.js +1 -1
- package/dist/mcp/tools/mark_stale.js +1 -1
- package/dist/mcp/tools/scan_project.d.ts +2 -3
- package/dist/mcp/tools/scan_project.js +12 -13
- package/dist/mcp/tools/set_project_brief.js +1 -1
- package/dist/mcp/tools/write_decision_card.d.ts +1 -1
- package/dist/mcp/tools/write_decision_card.js +2 -2
- package/dist/ui/index.html +1 -1
- package/package.json +9 -14
- package/dist/commands/hook.d.ts +0 -9
- package/dist/commands/hook.js +0 -84
- package/dist/hook/preToolUse.d.ts +0 -30
- package/dist/hook/preToolUse.js +0 -81
- package/dist/hook.d.ts +0 -1
- package/dist/hook.js +0 -337
- package/dist/mcp/BudgetGuard.d.ts +0 -13
- package/dist/mcp/BudgetGuard.js +0 -55
- package/dist/mcp/SessionScope.d.ts +0 -26
- package/dist/mcp/SessionScope.js +0 -56
- package/dist/mcp/tools/get_file_skeleton.d.ts +0 -23
- package/dist/mcp/tools/get_file_skeleton.js +0 -124
- package/dist/mcp/tools/hydration/get_evidence_slice.d.ts +0 -31
- package/dist/mcp/tools/hydration/get_evidence_slice.js +0 -57
- package/dist/mcp/tools/hydration/get_project_summary.d.ts +0 -23
- package/dist/mcp/tools/hydration/get_project_summary.js +0 -58
- package/dist/mcp/tools/hydration/get_start_here.d.ts +0 -23
- package/dist/mcp/tools/hydration/get_start_here.js +0 -52
- package/dist/mcp/tools/read_file.d.ts +0 -31
- package/dist/mcp/tools/read_file.js +0 -90
- package/dist/store/BlobStore.d.ts +0 -22
- package/dist/store/BlobStore.js +0 -96
- package/dist/store/PointerStore.d.ts +0 -52
- package/dist/store/PointerStore.js +0 -154
package/package.json
CHANGED
|
@@ -1,18 +1,18 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "vibe-splain",
|
|
3
|
-
"version": "
|
|
4
|
-
"description": "Static-analysis
|
|
3
|
+
"version": "4.0.0",
|
|
4
|
+
"description": "Static-analysis dossier engine for codebases: Tree-Sitter powered architectural analysis and interactive UI. Language-agnostic core, optimized for TypeScript/JavaScript.",
|
|
5
5
|
"type": "module",
|
|
6
6
|
"license": "MIT",
|
|
7
7
|
"author": "abp2204",
|
|
8
|
-
"homepage": "https://github.com/abp2204/
|
|
8
|
+
"homepage": "https://github.com/abp2204/vibesplain#readme",
|
|
9
9
|
"repository": {
|
|
10
10
|
"type": "git",
|
|
11
|
-
"url": "git+https://github.com/abp2204/
|
|
11
|
+
"url": "git+https://github.com/abp2204/vibesplain.git",
|
|
12
12
|
"directory": "packages/cli"
|
|
13
13
|
},
|
|
14
14
|
"bugs": {
|
|
15
|
-
"url": "https://github.com/abp2204/
|
|
15
|
+
"url": "https://github.com/abp2204/vibesplain/issues"
|
|
16
16
|
},
|
|
17
17
|
"keywords": [
|
|
18
18
|
"mcp",
|
|
@@ -30,33 +30,28 @@
|
|
|
30
30
|
"node": ">=18"
|
|
31
31
|
},
|
|
32
32
|
"bin": {
|
|
33
|
-
"
|
|
33
|
+
"vibesplain": "dist/index.js"
|
|
34
34
|
},
|
|
35
35
|
"scripts": {
|
|
36
36
|
"build": "tsc && node build.mjs"
|
|
37
37
|
},
|
|
38
38
|
"dependencies": {
|
|
39
39
|
"@modelcontextprotocol/sdk": "^1.0.0",
|
|
40
|
-
"better-sqlite3": "^12.10.0",
|
|
41
40
|
"chokidar": "^3.6.0",
|
|
42
41
|
"commander": "^12.0.0",
|
|
43
42
|
"fs-extra": "^11.0.0",
|
|
44
43
|
"tar": "^7.5.16",
|
|
45
44
|
"tree-sitter-wasms": "^0.1.11",
|
|
46
|
-
"web-tree-sitter": "^0.22.0"
|
|
45
|
+
"web-tree-sitter": "^0.22.0",
|
|
46
|
+
"uuid": "^9.0.0"
|
|
47
47
|
},
|
|
48
48
|
"devDependencies": {
|
|
49
|
-
"@types/better-sqlite3": "^7.6.13",
|
|
50
49
|
"@types/fs-extra": "^11.0.0",
|
|
51
|
-
"@types/minimatch": "^5.1.2",
|
|
52
50
|
"@types/tar": "^6.1.13",
|
|
53
51
|
"@types/uuid": "^9.0.0",
|
|
54
|
-
"async-mutex": "^0.5.0",
|
|
55
52
|
"esbuild": "^0.28.0",
|
|
56
|
-
"minimatch": "^10.2.5",
|
|
57
53
|
"tsx": "^4.22.4",
|
|
58
|
-
"typescript": "^5.4.0"
|
|
59
|
-
"uuid": "^9.0.0"
|
|
54
|
+
"typescript": "^5.4.0"
|
|
60
55
|
},
|
|
61
56
|
"files": [
|
|
62
57
|
"dist/",
|
package/dist/commands/hook.d.ts
DELETED
|
@@ -1,9 +0,0 @@
|
|
|
1
|
-
export interface HookCommandResult {
|
|
2
|
-
/** JSON to print to stdout, or null to print nothing (defer / exit 0). */
|
|
3
|
-
stdout: string | null;
|
|
4
|
-
}
|
|
5
|
-
export declare function findProjectRoot(start: string | undefined): string | null;
|
|
6
|
-
/** Pure-ish core: raw stdin string in, hook decision (as a string or null) out. */
|
|
7
|
-
export declare function runHookCommand(rawStdin: string): Promise<HookCommandResult>;
|
|
8
|
-
/** CLI entrypoint: read all of stdin, run the gate, emit the result. */
|
|
9
|
-
export declare function hookPreToolUseCommand(): Promise<void>;
|
package/dist/commands/hook.js
DELETED
|
@@ -1,84 +0,0 @@
|
|
|
1
|
-
// `vibe-splain hook pretooluse` — the deterministic PreToolUse gate, wired to stdio.
|
|
2
|
-
//
|
|
3
|
-
// Claude Code pipes a JSON describing the tool call about to run onto stdin; we
|
|
4
|
-
// decide whether to escalate and, if so, write hook JSON to stdout. On a safe
|
|
5
|
-
// edit (or any non-edit tool, unscanned repo, etc.) we print nothing and exit 0,
|
|
6
|
-
// deferring to normal permission flow.
|
|
7
|
-
//
|
|
8
|
-
// NOTE on the console.log ban: that rule protects the MCP *server's* stdout.
|
|
9
|
-
// This is a separate, short-lived process whose stdout IS the hook protocol —
|
|
10
|
-
// writing JSON here is correct. Diagnostics still go to stderr.
|
|
11
|
-
import { existsSync, writeFileSync, readFileSync } from 'fs';
|
|
12
|
-
import { dirname, join } from 'path';
|
|
13
|
-
import { tmpdir } from 'os';
|
|
14
|
-
import { decidePreToolUse } from '../hook/preToolUse.js';
|
|
15
|
-
// Walk up from `start` looking for the directory that holds a `.vibe-splainer` scan.
|
|
16
|
-
// Returns null if none is found — an unscanned repo means the gate is inert.
|
|
17
|
-
export function findProjectRoot(start) {
|
|
18
|
-
let dir = start || process.cwd();
|
|
19
|
-
// Bound the walk so a stray cwd can't traverse the whole filesystem.
|
|
20
|
-
for (let i = 0; i < 64; i++) {
|
|
21
|
-
if (existsSync(join(dir, '.vibe-splainer', 'analysis.json')))
|
|
22
|
-
return dir;
|
|
23
|
-
const parent = dirname(dir);
|
|
24
|
-
if (parent === dir)
|
|
25
|
-
break;
|
|
26
|
-
dir = parent;
|
|
27
|
-
}
|
|
28
|
-
return null;
|
|
29
|
-
}
|
|
30
|
-
/** Pure-ish core: raw stdin string in, hook decision (as a string or null) out. */
|
|
31
|
-
export async function runHookCommand(rawStdin) {
|
|
32
|
-
let input;
|
|
33
|
-
try {
|
|
34
|
-
input = JSON.parse(rawStdin);
|
|
35
|
-
}
|
|
36
|
-
catch {
|
|
37
|
-
return { stdout: null }; // malformed input → never block the agent
|
|
38
|
-
}
|
|
39
|
-
const root = findProjectRoot(input.cwd);
|
|
40
|
-
const gatePath = root ? join(root, '.vibe-splainer', 'gate.json') : null;
|
|
41
|
-
const gateExists = gatePath ? existsSync(gatePath) : false;
|
|
42
|
-
let gateIndex = null;
|
|
43
|
-
if (gateExists) {
|
|
44
|
-
try {
|
|
45
|
-
gateIndex = JSON.parse(readFileSync(gatePath, 'utf8'));
|
|
46
|
-
}
|
|
47
|
-
catch { }
|
|
48
|
-
}
|
|
49
|
-
const sessionId = input.session_id || 'default';
|
|
50
|
-
const warnFile = join(tmpdir(), `vibe-splain-warn-${sessionId}`);
|
|
51
|
-
const warningShown = existsSync(warnFile);
|
|
52
|
-
const result = decidePreToolUse(input, gateIndex, { warningShown });
|
|
53
|
-
if (result.action === 'defer')
|
|
54
|
-
return { stdout: null };
|
|
55
|
-
if (!gateIndex && result.action === 'emit') {
|
|
56
|
-
try {
|
|
57
|
-
writeFileSync(warnFile, '1');
|
|
58
|
-
}
|
|
59
|
-
catch { }
|
|
60
|
-
}
|
|
61
|
-
return { stdout: JSON.stringify(result.output) };
|
|
62
|
-
}
|
|
63
|
-
/** CLI entrypoint: read all of stdin, run the gate, emit the result. */
|
|
64
|
-
export async function hookPreToolUseCommand() {
|
|
65
|
-
const raw = await readStdin();
|
|
66
|
-
const { stdout } = await runHookCommand(raw);
|
|
67
|
-
if (stdout)
|
|
68
|
-
process.stdout.write(stdout);
|
|
69
|
-
// No stdout on defer; exit code stays 0.
|
|
70
|
-
}
|
|
71
|
-
function readStdin() {
|
|
72
|
-
return new Promise((resolve) => {
|
|
73
|
-
let data = '';
|
|
74
|
-
if (process.stdin.isTTY) {
|
|
75
|
-
resolve('');
|
|
76
|
-
return;
|
|
77
|
-
}
|
|
78
|
-
process.stdin.setEncoding('utf8');
|
|
79
|
-
process.stdin.on('data', (chunk) => { data += chunk; });
|
|
80
|
-
process.stdin.on('end', () => resolve(data));
|
|
81
|
-
process.stdin.on('error', () => resolve(data));
|
|
82
|
-
});
|
|
83
|
-
}
|
|
84
|
-
//# sourceMappingURL=hook.js.map
|
|
@@ -1,30 +0,0 @@
|
|
|
1
|
-
import type { EscalationContext } from '@vibe-splain/brain/dist/network/escalation.js';
|
|
2
|
-
import type { GateIndex } from '@vibe-splain/brain/dist/network/gateIndex.js';
|
|
3
|
-
export interface PreToolUseInput {
|
|
4
|
-
tool_name?: string;
|
|
5
|
-
tool_input?: {
|
|
6
|
-
file_path?: string;
|
|
7
|
-
[k: string]: unknown;
|
|
8
|
-
};
|
|
9
|
-
cwd?: string;
|
|
10
|
-
session_id?: string;
|
|
11
|
-
}
|
|
12
|
-
export interface HookOutput {
|
|
13
|
-
hookSpecificOutput: {
|
|
14
|
-
hookEventName: 'PreToolUse';
|
|
15
|
-
permissionDecision: 'allow' | 'ask';
|
|
16
|
-
permissionDecisionReason?: string;
|
|
17
|
-
additionalContext?: string;
|
|
18
|
-
systemMessage?: string;
|
|
19
|
-
};
|
|
20
|
-
}
|
|
21
|
-
export type HookResult = {
|
|
22
|
-
action: 'defer';
|
|
23
|
-
} | {
|
|
24
|
-
action: 'emit';
|
|
25
|
-
output: HookOutput;
|
|
26
|
-
};
|
|
27
|
-
export declare function decidePreToolUse(input: PreToolUseInput, gateIndex: GateIndex | null, sessionState?: {
|
|
28
|
-
warningShown?: boolean;
|
|
29
|
-
}): HookResult;
|
|
30
|
-
export declare function formatEscalation(ctx: EscalationContext): string;
|
package/dist/hook/preToolUse.js
DELETED
|
@@ -1,81 +0,0 @@
|
|
|
1
|
-
// PreToolUse hook decision logic — the deterministic gate.
|
|
2
|
-
//
|
|
3
|
-
// Pure: given the agent's intended tool call and the scan artifact, decide
|
|
4
|
-
// whether to escalate and how. No stdin/stdout, no filesystem — the CLI command
|
|
5
|
-
// wraps this with I/O. This separation is what makes the gate testable against
|
|
6
|
-
// real scanned stores.
|
|
7
|
-
import { buildEscalationContext } from '@vibe-splain/brain/dist/network/escalation.js';
|
|
8
|
-
const DEFER = { action: 'defer' };
|
|
9
|
-
// Only file-editing tools can have a blast radius. Anything else defers.
|
|
10
|
-
const EDIT_TOOLS = new Set(['Edit', 'Write', 'MultiEdit']);
|
|
11
|
-
export function decidePreToolUse(input, gateIndex, sessionState) {
|
|
12
|
-
if (!EDIT_TOOLS.has(input.tool_name ?? ''))
|
|
13
|
-
return DEFER;
|
|
14
|
-
if (!gateIndex) {
|
|
15
|
-
if (sessionState && !sessionState.warningShown) {
|
|
16
|
-
const warnMsg = "vibe-splain: .vibe-splainer/gate.json is missing. Please run 'vibe-splain scan' to generate the scan architecture and enable tool guarding.";
|
|
17
|
-
return {
|
|
18
|
-
action: 'emit',
|
|
19
|
-
output: {
|
|
20
|
-
hookSpecificOutput: {
|
|
21
|
-
hookEventName: 'PreToolUse',
|
|
22
|
-
permissionDecision: 'allow',
|
|
23
|
-
additionalContext: warnMsg,
|
|
24
|
-
systemMessage: warnMsg,
|
|
25
|
-
},
|
|
26
|
-
},
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
return DEFER;
|
|
30
|
-
}
|
|
31
|
-
const filePath = input.tool_input?.file_path;
|
|
32
|
-
if (!filePath)
|
|
33
|
-
return DEFER;
|
|
34
|
-
const ctx = buildEscalationContext(filePath, gateIndex);
|
|
35
|
-
if (!ctx || ctx.blastRadius === 'low')
|
|
36
|
-
return DEFER;
|
|
37
|
-
const block = formatEscalation(ctx);
|
|
38
|
-
// high → stop-and-reconsider (human gate). medium → non-blocking awareness:
|
|
39
|
-
// the agent sees the context on its next request without being interrupted.
|
|
40
|
-
if (ctx.blastRadius === 'high') {
|
|
41
|
-
return {
|
|
42
|
-
action: 'emit',
|
|
43
|
-
output: {
|
|
44
|
-
hookSpecificOutput: {
|
|
45
|
-
hookEventName: 'PreToolUse',
|
|
46
|
-
permissionDecision: 'ask',
|
|
47
|
-
permissionDecisionReason: block,
|
|
48
|
-
additionalContext: block,
|
|
49
|
-
},
|
|
50
|
-
},
|
|
51
|
-
};
|
|
52
|
-
}
|
|
53
|
-
return {
|
|
54
|
-
action: 'emit',
|
|
55
|
-
output: {
|
|
56
|
-
hookSpecificOutput: {
|
|
57
|
-
hookEventName: 'PreToolUse',
|
|
58
|
-
permissionDecision: 'allow',
|
|
59
|
-
additionalContext: block,
|
|
60
|
-
},
|
|
61
|
-
},
|
|
62
|
-
};
|
|
63
|
-
}
|
|
64
|
-
export function formatEscalation(ctx) {
|
|
65
|
-
const lines = [];
|
|
66
|
-
lines.push(`vibe-splain: ${ctx.blastRadius} blast radius — ${ctx.targetFile} (gravity ${ctx.gravity}).`);
|
|
67
|
-
if (ctx.dependentCount > 0) {
|
|
68
|
-
const shown = ctx.dependents.slice(0, 8);
|
|
69
|
-
const more = ctx.dependentCount - shown.length;
|
|
70
|
-
lines.push(`${ctx.dependentCount} file(s) depend on it:`);
|
|
71
|
-
for (const d of shown)
|
|
72
|
-
lines.push(` - ${d}`);
|
|
73
|
-
if (more > 0)
|
|
74
|
-
lines.push(` … (+${more} more)`);
|
|
75
|
-
}
|
|
76
|
-
for (const w of ctx.riskWarnings)
|
|
77
|
-
lines.push(`[${w.level}] ${w.message}`);
|
|
78
|
-
lines.push(ctx.smallestSafeChange.summary);
|
|
79
|
-
return lines.join('\n');
|
|
80
|
-
}
|
|
81
|
-
//# sourceMappingURL=preToolUse.js.map
|
package/dist/hook.d.ts
DELETED
|
@@ -1 +0,0 @@
|
|
|
1
|
-
export declare function findProjectRoot(start: string | undefined): string | null;
|
package/dist/hook.js
DELETED
|
@@ -1,337 +0,0 @@
|
|
|
1
|
-
#!/usr/bin/env node
|
|
2
|
-
// dist/hook.js
|
|
3
|
-
import { existsSync, readFileSync, writeFileSync } from "fs";
|
|
4
|
-
import { dirname, join } from "path";
|
|
5
|
-
import { tmpdir } from "os";
|
|
6
|
-
|
|
7
|
-
// ../brain/dist/network/gateIndex.js
|
|
8
|
-
function isDenoisedImporter(relPath) {
|
|
9
|
-
if (!relPath)
|
|
10
|
-
return false;
|
|
11
|
-
const norm = relPath.replace(/\\/g, "/");
|
|
12
|
-
const segs = norm.split("/");
|
|
13
|
-
const excludedDirs = /* @__PURE__ */ new Set([
|
|
14
|
-
"node_modules",
|
|
15
|
-
"vendor",
|
|
16
|
-
"vendored",
|
|
17
|
-
"site-packages",
|
|
18
|
-
"third_party",
|
|
19
|
-
"third-party",
|
|
20
|
-
".yarn",
|
|
21
|
-
"bower_components",
|
|
22
|
-
"venv",
|
|
23
|
-
".venv",
|
|
24
|
-
"env",
|
|
25
|
-
"virtualenv",
|
|
26
|
-
".git",
|
|
27
|
-
".vibe-splainer",
|
|
28
|
-
"dist",
|
|
29
|
-
"build",
|
|
30
|
-
"out",
|
|
31
|
-
"target",
|
|
32
|
-
".next",
|
|
33
|
-
".nuxt",
|
|
34
|
-
".docusaurus",
|
|
35
|
-
"coverage",
|
|
36
|
-
".nyc_output",
|
|
37
|
-
".cache"
|
|
38
|
-
]);
|
|
39
|
-
for (const s of segs) {
|
|
40
|
-
const sLower = s.toLowerCase();
|
|
41
|
-
if (excludedDirs.has(sLower)) {
|
|
42
|
-
return false;
|
|
43
|
-
}
|
|
44
|
-
if (sLower.endsWith(".venv")) {
|
|
45
|
-
return false;
|
|
46
|
-
}
|
|
47
|
-
}
|
|
48
|
-
const fileName = segs[segs.length - 1];
|
|
49
|
-
const fileNameLower = fileName.toLowerCase();
|
|
50
|
-
if (/\.min\.[a-z]+$/.test(fileNameLower) || fileNameLower.includes(".min.")) {
|
|
51
|
-
return false;
|
|
52
|
-
}
|
|
53
|
-
if (/\.generated\./.test(fileNameLower) || fileNameLower.includes("__generated__")) {
|
|
54
|
-
return false;
|
|
55
|
-
}
|
|
56
|
-
if (fileNameLower.endsWith(".lock")) {
|
|
57
|
-
return false;
|
|
58
|
-
}
|
|
59
|
-
if (norm.startsWith("virtual:") || norm.startsWith("__virtual:") || norm.startsWith("webpack:")) {
|
|
60
|
-
return false;
|
|
61
|
-
}
|
|
62
|
-
return true;
|
|
63
|
-
}
|
|
64
|
-
|
|
65
|
-
// ../brain/dist/network/escalation.js
|
|
66
|
-
var SECURITY_PATH = /\b(auth|credential|secret|token|webhook|payment|password|oauth|session)\b/i;
|
|
67
|
-
var SENSITIVE_EFFECTS = /* @__PURE__ */ new Set([
|
|
68
|
-
"database_write",
|
|
69
|
-
"server_action",
|
|
70
|
-
"trpc_mutation",
|
|
71
|
-
"email_send",
|
|
72
|
-
"external_api_call"
|
|
73
|
-
]);
|
|
74
|
-
var NOTABLE_RISKS = /* @__PURE__ */ new Set([
|
|
75
|
-
"state_machine",
|
|
76
|
-
"mutation_orchestration",
|
|
77
|
-
"side_effect_coupling",
|
|
78
|
-
"async_race_risk",
|
|
79
|
-
"registry_bottleneck",
|
|
80
|
-
"error_swallowing"
|
|
81
|
-
]);
|
|
82
|
-
function buildEscalationContext(targetFile, gateIndex) {
|
|
83
|
-
const file = lookupFile(targetFile, gateIndex);
|
|
84
|
-
if (!file)
|
|
85
|
-
return null;
|
|
86
|
-
const rel = file.relativePath;
|
|
87
|
-
const gravity = file.gravity;
|
|
88
|
-
const dependents = file.dependents;
|
|
89
|
-
const dependentsCount = dependents.length;
|
|
90
|
-
const sideEffects = file.sideEffects;
|
|
91
|
-
const riskTypes = file.riskTypes;
|
|
92
|
-
const severity = file.severity;
|
|
93
|
-
const gravity_tier = gravity > 70 ? "high" : gravity > 40 ? "medium" : "low";
|
|
94
|
-
const raw_substance_tier = file.hasBehavioralSubstance && dependentsCount >= 10 ? "high" : file.hasBehavioralSubstance && dependentsCount >= 4 ? "medium" : "low";
|
|
95
|
-
const tierOrder = { low: 1, medium: 2, high: 3 };
|
|
96
|
-
let blastRadius = tierOrder[gravity_tier] >= tierOrder[raw_substance_tier] ? gravity_tier : raw_substance_tier;
|
|
97
|
-
const isGeneratedOrVendored = !isDenoisedImporter(targetFile) || file.demoteReason !== null && file.demoteReason !== "no inbound references from application code";
|
|
98
|
-
if (isGeneratedOrVendored) {
|
|
99
|
-
blastRadius = "low";
|
|
100
|
-
}
|
|
101
|
-
const riskWarnings = buildRiskWarnings(rel, gravity, dependentsCount, severity, sideEffects, riskTypes);
|
|
102
|
-
return {
|
|
103
|
-
targetFile: rel,
|
|
104
|
-
gravity,
|
|
105
|
-
blastRadius,
|
|
106
|
-
dependents,
|
|
107
|
-
dependentCount: dependentsCount,
|
|
108
|
-
fanIn: file.fanIn,
|
|
109
|
-
fanOut: file.fanOut,
|
|
110
|
-
centrality: file.centrality,
|
|
111
|
-
severity,
|
|
112
|
-
sideEffects,
|
|
113
|
-
riskTypes,
|
|
114
|
-
riskWarnings,
|
|
115
|
-
smallestSafeChange: buildSafeChangePolicy(blastRadius)
|
|
116
|
-
};
|
|
117
|
-
}
|
|
118
|
-
function lookupFile(targetFile, gateIndex) {
|
|
119
|
-
const files = gateIndex?.files;
|
|
120
|
-
if (!files)
|
|
121
|
-
return null;
|
|
122
|
-
const norm = targetFile.replace(/\\/g, "/");
|
|
123
|
-
if (files[norm])
|
|
124
|
-
return files[norm];
|
|
125
|
-
const suffixHits = [];
|
|
126
|
-
for (const [key, rec] of Object.entries(files)) {
|
|
127
|
-
if (norm === key || norm.endsWith("/" + key) || key.endsWith("/" + norm)) {
|
|
128
|
-
suffixHits.push(rec);
|
|
129
|
-
}
|
|
130
|
-
}
|
|
131
|
-
return suffixHits.length === 1 ? suffixHits[0] : null;
|
|
132
|
-
}
|
|
133
|
-
function buildRiskWarnings(rel, gravity, dependentCount, severity, sideEffects, riskTypes) {
|
|
134
|
-
const warnings = [];
|
|
135
|
-
if (gravity > 70) {
|
|
136
|
-
warnings.push({
|
|
137
|
-
id: `rw_${rel}_blast_radius`,
|
|
138
|
-
level: "critical",
|
|
139
|
-
message: `Central file \u2014 editing it has a large blast radius. ${dependentCount} file(s) depend on it.`,
|
|
140
|
-
reason: `Gravity ${gravity}/100; fan-in ${dependentCount}.`
|
|
141
|
-
});
|
|
142
|
-
}
|
|
143
|
-
if (severity >= 4) {
|
|
144
|
-
warnings.push({
|
|
145
|
-
id: `rw_${rel}_severity`,
|
|
146
|
-
level: "warning",
|
|
147
|
-
message: "High-severity smells already present here. Avoid making them worse.",
|
|
148
|
-
reason: `Canonical severity ${severity}/5.`
|
|
149
|
-
});
|
|
150
|
-
}
|
|
151
|
-
if (SECURITY_PATH.test(rel)) {
|
|
152
|
-
warnings.push({
|
|
153
|
-
id: `rw_${rel}_security_path`,
|
|
154
|
-
level: "critical",
|
|
155
|
-
message: "Security-sensitive file (auth/credential/webhook/payment). Do not alter auth or credential handling unless that is the explicit task.",
|
|
156
|
-
reason: `Path matches security-sensitive pattern.`
|
|
157
|
-
});
|
|
158
|
-
}
|
|
159
|
-
const sensitive = sideEffects.filter((e) => SENSITIVE_EFFECTS.has(e));
|
|
160
|
-
if (sensitive.length > 0) {
|
|
161
|
-
warnings.push({
|
|
162
|
-
id: `rw_${rel}_side_effects`,
|
|
163
|
-
level: "warning",
|
|
164
|
-
message: `This file performs side effects (${sensitive.join(", ")}). Preserve existing behavior; do not drop or reorder them.`,
|
|
165
|
-
reason: `Side-effect profile: ${sensitive.join(", ")}.`
|
|
166
|
-
});
|
|
167
|
-
}
|
|
168
|
-
const notable = riskTypes.filter((r) => NOTABLE_RISKS.has(r));
|
|
169
|
-
if (notable.length > 0) {
|
|
170
|
-
warnings.push({
|
|
171
|
-
id: `rw_${rel}_risk_types`,
|
|
172
|
-
level: "info",
|
|
173
|
-
message: `Structural risk patterns detected (${notable.join(", ")}). Trace the affected paths before editing.`,
|
|
174
|
-
reason: `Risk types: ${notable.join(", ")}.`
|
|
175
|
-
});
|
|
176
|
-
}
|
|
177
|
-
return warnings;
|
|
178
|
-
}
|
|
179
|
-
function buildSafeChangePolicy(blastRadius) {
|
|
180
|
-
const rules = [
|
|
181
|
-
"Locate the exact failure site before editing.",
|
|
182
|
-
"Make the smallest localized change that addresses the task.",
|
|
183
|
-
"Do not perform general cleanup or refactoring.",
|
|
184
|
-
"Do not modify unrelated files, credentials, or auth configuration."
|
|
185
|
-
];
|
|
186
|
-
if (blastRadius === "high") {
|
|
187
|
-
rules.push("This file is load-bearing: verify each dependent still type-checks against the changed surface.");
|
|
188
|
-
}
|
|
189
|
-
return {
|
|
190
|
-
summary: "Make the smallest localized change that addresses the requested task. Do not modify unrelated files, credentials, auth configuration, or neighboring modules unless the task explicitly requires it.",
|
|
191
|
-
rules,
|
|
192
|
-
forbiddenEditClasses: ["credentials", "auth_configuration", "global_refactoring"]
|
|
193
|
-
};
|
|
194
|
-
}
|
|
195
|
-
|
|
196
|
-
// dist/hook/preToolUse.js
|
|
197
|
-
var DEFER = { action: "defer" };
|
|
198
|
-
var EDIT_TOOLS = /* @__PURE__ */ new Set(["Edit", "Write", "MultiEdit"]);
|
|
199
|
-
function decidePreToolUse(input, gateIndex, sessionState) {
|
|
200
|
-
if (!EDIT_TOOLS.has(input.tool_name ?? ""))
|
|
201
|
-
return DEFER;
|
|
202
|
-
if (!gateIndex) {
|
|
203
|
-
if (sessionState && !sessionState.warningShown) {
|
|
204
|
-
const warnMsg = "vibe-splain: .vibe-splainer/gate.json is missing. Please run 'vibe-splain scan' to generate the scan architecture and enable tool guarding.";
|
|
205
|
-
return {
|
|
206
|
-
action: "emit",
|
|
207
|
-
output: {
|
|
208
|
-
hookSpecificOutput: {
|
|
209
|
-
hookEventName: "PreToolUse",
|
|
210
|
-
permissionDecision: "allow",
|
|
211
|
-
additionalContext: warnMsg,
|
|
212
|
-
systemMessage: warnMsg
|
|
213
|
-
}
|
|
214
|
-
}
|
|
215
|
-
};
|
|
216
|
-
}
|
|
217
|
-
return DEFER;
|
|
218
|
-
}
|
|
219
|
-
const filePath = input.tool_input?.file_path;
|
|
220
|
-
if (!filePath)
|
|
221
|
-
return DEFER;
|
|
222
|
-
const ctx = buildEscalationContext(filePath, gateIndex);
|
|
223
|
-
if (!ctx || ctx.blastRadius === "low")
|
|
224
|
-
return DEFER;
|
|
225
|
-
const block = formatEscalation(ctx);
|
|
226
|
-
if (ctx.blastRadius === "high") {
|
|
227
|
-
return {
|
|
228
|
-
action: "emit",
|
|
229
|
-
output: {
|
|
230
|
-
hookSpecificOutput: {
|
|
231
|
-
hookEventName: "PreToolUse",
|
|
232
|
-
permissionDecision: "ask",
|
|
233
|
-
permissionDecisionReason: block,
|
|
234
|
-
additionalContext: block
|
|
235
|
-
}
|
|
236
|
-
}
|
|
237
|
-
};
|
|
238
|
-
}
|
|
239
|
-
return {
|
|
240
|
-
action: "emit",
|
|
241
|
-
output: {
|
|
242
|
-
hookSpecificOutput: {
|
|
243
|
-
hookEventName: "PreToolUse",
|
|
244
|
-
permissionDecision: "allow",
|
|
245
|
-
additionalContext: block
|
|
246
|
-
}
|
|
247
|
-
}
|
|
248
|
-
};
|
|
249
|
-
}
|
|
250
|
-
function formatEscalation(ctx) {
|
|
251
|
-
const lines = [];
|
|
252
|
-
lines.push(`vibe-splain: ${ctx.blastRadius} blast radius \u2014 ${ctx.targetFile} (gravity ${ctx.gravity}).`);
|
|
253
|
-
if (ctx.dependentCount > 0) {
|
|
254
|
-
const shown = ctx.dependents.slice(0, 8);
|
|
255
|
-
const more = ctx.dependentCount - shown.length;
|
|
256
|
-
lines.push(`${ctx.dependentCount} file(s) depend on it:`);
|
|
257
|
-
for (const d of shown)
|
|
258
|
-
lines.push(` - ${d}`);
|
|
259
|
-
if (more > 0)
|
|
260
|
-
lines.push(` \u2026 (+${more} more)`);
|
|
261
|
-
}
|
|
262
|
-
for (const w of ctx.riskWarnings)
|
|
263
|
-
lines.push(`[${w.level}] ${w.message}`);
|
|
264
|
-
lines.push(ctx.smallestSafeChange.summary);
|
|
265
|
-
return lines.join("\n");
|
|
266
|
-
}
|
|
267
|
-
|
|
268
|
-
// dist/hook.js
|
|
269
|
-
function findProjectRoot(start) {
|
|
270
|
-
let dir = start || process.cwd();
|
|
271
|
-
for (let i = 0; i < 64; i++) {
|
|
272
|
-
if (existsSync(join(dir, ".vibe-splainer")))
|
|
273
|
-
return dir;
|
|
274
|
-
const parent = dirname(dir);
|
|
275
|
-
if (parent === dir)
|
|
276
|
-
break;
|
|
277
|
-
dir = parent;
|
|
278
|
-
}
|
|
279
|
-
return null;
|
|
280
|
-
}
|
|
281
|
-
function readStdin() {
|
|
282
|
-
return new Promise((resolve) => {
|
|
283
|
-
let data = "";
|
|
284
|
-
if (process.stdin.isTTY) {
|
|
285
|
-
resolve("");
|
|
286
|
-
return;
|
|
287
|
-
}
|
|
288
|
-
process.stdin.setEncoding("utf8");
|
|
289
|
-
process.stdin.on("data", (chunk) => {
|
|
290
|
-
data += chunk;
|
|
291
|
-
});
|
|
292
|
-
process.stdin.on("end", () => resolve(data));
|
|
293
|
-
process.stdin.on("error", () => resolve(data));
|
|
294
|
-
});
|
|
295
|
-
}
|
|
296
|
-
async function main() {
|
|
297
|
-
const rawStdin = await readStdin();
|
|
298
|
-
let input;
|
|
299
|
-
try {
|
|
300
|
-
input = JSON.parse(rawStdin);
|
|
301
|
-
} catch {
|
|
302
|
-
process.exit(0);
|
|
303
|
-
}
|
|
304
|
-
const root = findProjectRoot(input.cwd);
|
|
305
|
-
const gatePath = root ? join(root, ".vibe-splainer", "gate.json") : null;
|
|
306
|
-
const gateExists = gatePath ? existsSync(gatePath) : false;
|
|
307
|
-
let result;
|
|
308
|
-
if (!gateExists) {
|
|
309
|
-
const sessionId = input.session_id || "default";
|
|
310
|
-
const warnFile = join(tmpdir(), `vibe-splain-warn-${sessionId}`);
|
|
311
|
-
const warningShown = existsSync(warnFile);
|
|
312
|
-
result = decidePreToolUse(input, null, { warningShown });
|
|
313
|
-
if (result.action === "emit") {
|
|
314
|
-
try {
|
|
315
|
-
writeFileSync(warnFile, "1");
|
|
316
|
-
} catch {
|
|
317
|
-
}
|
|
318
|
-
}
|
|
319
|
-
} else {
|
|
320
|
-
let gateIndex = null;
|
|
321
|
-
try {
|
|
322
|
-
gateIndex = JSON.parse(readFileSync(gatePath, "utf8"));
|
|
323
|
-
} catch {
|
|
324
|
-
}
|
|
325
|
-
result = decidePreToolUse(input, gateIndex);
|
|
326
|
-
}
|
|
327
|
-
if (result.action === "emit") {
|
|
328
|
-
process.stdout.write(JSON.stringify(result.output));
|
|
329
|
-
}
|
|
330
|
-
process.exit(0);
|
|
331
|
-
}
|
|
332
|
-
main().catch(() => {
|
|
333
|
-
process.exit(0);
|
|
334
|
-
});
|
|
335
|
-
export {
|
|
336
|
-
findProjectRoot
|
|
337
|
-
};
|
|
@@ -1,13 +0,0 @@
|
|
|
1
|
-
export interface BudgetExceededResult {
|
|
2
|
-
pointerId: string;
|
|
3
|
-
contentHash: string;
|
|
4
|
-
sizeBytes: number;
|
|
5
|
-
summary: string;
|
|
6
|
-
hydrators: string[];
|
|
7
|
-
}
|
|
8
|
-
export declare function applyBudgetGuard(projectRoot: string, scanId: string, artifactName: string, output: unknown): Promise<unknown>;
|
|
9
|
-
/** Verify a pointer exists, is unexpired, and its blob hash matches */
|
|
10
|
-
export declare function hydratePointer(projectRoot: string, pointerId: string): Promise<{
|
|
11
|
-
content: Buffer;
|
|
12
|
-
row: import('../store/PointerStore.js').PointerRow;
|
|
13
|
-
}>;
|
package/dist/mcp/BudgetGuard.js
DELETED
|
@@ -1,55 +0,0 @@
|
|
|
1
|
-
import { BlobStore } from '../store/BlobStore.js';
|
|
2
|
-
import { PointerStore } from '../store/PointerStore.js';
|
|
3
|
-
import { v4 as uuidv4 } from 'uuid';
|
|
4
|
-
/** ~2000 tokens ≈ 8000 chars */
|
|
5
|
-
const BUDGET_CHARS = 8000;
|
|
6
|
-
export async function applyBudgetGuard(projectRoot, scanId, artifactName, output) {
|
|
7
|
-
const serialized = JSON.stringify(output, null, 2);
|
|
8
|
-
if (serialized.length <= BUDGET_CHARS)
|
|
9
|
-
return output;
|
|
10
|
-
const blobStore = new BlobStore(projectRoot);
|
|
11
|
-
const pointerStore = PointerStore.open(projectRoot);
|
|
12
|
-
const { contentHash, blobPath } = await blobStore.writeAtomic(serialized);
|
|
13
|
-
const pointerId = `ptr_${uuidv4().replace(/-/g, '').slice(0, 16)}`;
|
|
14
|
-
await pointerStore.insertPointer({
|
|
15
|
-
pointerId,
|
|
16
|
-
scanId,
|
|
17
|
-
artifactName,
|
|
18
|
-
contentHash,
|
|
19
|
-
blobPath,
|
|
20
|
-
schemaVersion: '1.0.0',
|
|
21
|
-
createdAt: Date.now(),
|
|
22
|
-
expiresAt: null,
|
|
23
|
-
});
|
|
24
|
-
const result = {
|
|
25
|
-
pointerId,
|
|
26
|
-
contentHash,
|
|
27
|
-
sizeBytes: serialized.length,
|
|
28
|
-
summary: `Output exceeded context budget (${serialized.length} chars). Written to artifact blob.`,
|
|
29
|
-
hydrators: ['get_evidence_slice', 'get_start_here', 'get_project_summary'],
|
|
30
|
-
};
|
|
31
|
-
return result;
|
|
32
|
-
}
|
|
33
|
-
/** Verify a pointer exists, is unexpired, and its blob hash matches */
|
|
34
|
-
export async function hydratePointer(projectRoot, pointerId) {
|
|
35
|
-
const pointerStore = PointerStore.open(projectRoot);
|
|
36
|
-
const row = pointerStore.getPointer(pointerId);
|
|
37
|
-
if (!row) {
|
|
38
|
-
throw new Error(`ArtifactNotFound: pointer ${pointerId} does not exist`);
|
|
39
|
-
}
|
|
40
|
-
if (row.expiresAt !== null && row.expiresAt < Date.now()) {
|
|
41
|
-
throw new Error(`ArtifactCollectedError: pointer ${pointerId} has expired`);
|
|
42
|
-
}
|
|
43
|
-
const SUPPORTED_VERSIONS = ['1.0.0', '2.0.0'];
|
|
44
|
-
if (!SUPPORTED_VERSIONS.includes(row.schemaVersion)) {
|
|
45
|
-
throw new Error(`UnsupportedSchema: pointer ${pointerId} has schema version ${row.schemaVersion}`);
|
|
46
|
-
}
|
|
47
|
-
const blobStore = new BlobStore(projectRoot);
|
|
48
|
-
const content = await blobStore.readBlob(row.blobPath);
|
|
49
|
-
const valid = await blobStore.verifyIntegrity(row.blobPath, row.contentHash);
|
|
50
|
-
if (!valid) {
|
|
51
|
-
throw new Error(`IntegrityError: blob for pointer ${pointerId} failed hash verification`);
|
|
52
|
-
}
|
|
53
|
-
return { content, row };
|
|
54
|
-
}
|
|
55
|
-
//# sourceMappingURL=BudgetGuard.js.map
|