vibe-splain 3.4.0 → 3.4.2

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 CHANGED
@@ -1,5 +1,5 @@
1
1
  <p align="center">
2
- <strong>◈ VIBE-SPLAIN</strong>
2
+ <strong>◈ vibe-splain</strong>
3
3
  <br />
4
4
  <em>Map architectural DNA and behavioral call-chains in complex codebases.</em>
5
5
  </p>
@@ -14,13 +14,13 @@
14
14
 
15
15
  ---
16
16
 
17
- VIBE-SPLAIN is a high-fidelity **static analysis engine** and MCP server. It uses [Tree-Sitter](https://tree-sitter.github.io/tree-sitter/) to extract the structural and behavioral patterns of a codebase—identifying high-gravity components, mapping semantic actions, and tracing call-chains between entrypoints and side effects.
17
+ vibe-splain is a high-fidelity **static analysis engine** and MCP server. It uses [Tree-Sitter](https://tree-sitter.github.io/tree-sitter/) to extract the structural and behavioral patterns of a codebase—identifying high-gravity components, mapping semantic actions, and tracing call-chains between entrypoints and side effects.
18
18
 
19
- While VIBE-SPLAIN is built on a language-agnostic foundation, the current toolset is **highly optimized for TypeScript and JavaScript** (especially Next.js, Prisma, and tRPC environments).
19
+ While vibe-splain is built on a language-agnostic foundation, the current toolset is **highly optimized for TypeScript and JavaScript** (especially Next.js, Prisma, and tRPC environments).
20
20
 
21
21
  **Zero LLM calls. Zero API keys. Pure static analysis.**
22
22
 
23
- Your coding agent does all the thinking — VIBE-SPLAIN just gives it the right data.
23
+ Your coding agent does all the thinking — vibe-splain just gives it the right data.
24
24
 
25
25
  ## Install
26
26
 
@@ -28,17 +28,17 @@ Your coding agent does all the thinking — VIBE-SPLAIN just gives it the right
28
28
  npx vibe-splain install
29
29
  ```
30
30
 
31
- That's it. This patches your coding agent's MCP config so it can call VIBE-SPLAIN's tools. Restart your agent.
31
+ That's it. This patches your coding agent's MCP config so it can call vibe-splain's tools. Restart your agent.
32
32
 
33
33
  ### Running the Analysis
34
34
 
35
- You don't need to write a complex prompt. VIBE-SPLAIN provides a built-in MCP Prompt called `build_dossier` that automatically tells your agent exactly what to do.
35
+ You don't need to write a complex prompt. vibe-splain provides a built-in MCP Prompt called `build_dossier` that automatically tells your agent exactly what to do.
36
36
 
37
37
  **In Claude Code / Gemini CLI:**
38
38
  Type `/prompt build_dossier` and press enter.
39
39
 
40
40
  **In Cursor / Windsurf:**
41
- Open the MCP panel or agent chat, select the `build_dossier` prompt from the VIBE-SPLAIN server, and run it.
41
+ Open the MCP panel or agent chat, select the `build_dossier` prompt from the vibe-splain server, and run it.
42
42
 
43
43
  Your agent will loop through the high-gravity files, analyze each one, and build an **Architectural Dossier** — a structured set of **Decision Cards** explaining the technical rationale of the code.
44
44
 
@@ -71,22 +71,27 @@ Your agent will loop through the high-gravity files, analyze each one, and build
71
71
  ```
72
72
 
73
73
  ### Three Levels of Analysis
74
-
75
74
  1. **Level 0 — Semantic Classification**: Maps files to architectural "pillars" (Auth, Payments, Database, etc.) using import-path heuristics and library signatures.
75
+ 2. **Level 1 — Structural and Behavioral Gravity**: Core computes `staticGravity` using Tree-Sitter AST analysis based on link density, nesting depth, and mutation counts. **Bundled Adapters** (like Cal.com and n8n) interpret product semantics to add `behavioralLift` for critical dynamic execution paths. Final `gravity` is the combination of static evidence and adapter interpretation.
76
+ 3. **Level 2 — Behavioral Traceability**: Tree-Sitter powered call-graph analysis. It maps function-level dependencies and identifies **Critical Functions** (entrypoints, semantic actions, or high-outbound callers).
77
+
78
+ ### Claude Code PreToolUse Hook
76
79
 
77
- 2. **Level 1 — Cognitive Complexity**: Tree-Sitter AST analysis computes a complexity score per file based on link density, nesting depth, and mutation counts. Files scoring ≥ 15 are identified as **High-Gravity**.
80
+ `vibe-splain` integrates directly with Claude Code's hook system to prevent AI agents from accidentally breaking your codebase:
78
81
 
79
- 3. **Level 2 Behavioral Traceability**: Tree-Sitter powered call-graph analysis. It maps function-level dependencies and identifies **Critical Functions** (entrypoints, semantic actions, or high-outbound callers) so your agent can trace the exact ripple effect of a code change.
82
+ - **Ultra-Fast Local Gating**: Installs a standalone entrypoint (`dist/hook.js`) that runs in **< 15ms** with zero network calls, avoiding heavy WASM/tree-sitter load overhead.
83
+ - **Hybrid Blast-Radius Logic**: Combines static gravity and substance-based direct dependents to protect important files (e.g. dynamically-loaded execution nodes or central utility modules) even if they lack standard entrypoints.
84
+ - **Developer Friction Suppression**: Automatically demotes generated, minified, or vendored files to a `low` blast-radius, ensuring the agent is never blocked when editing build targets or lockfiles.
85
+ - **Warn-Once session behavior**: If `.vibe-splainer/gate.json` is missing, the hook notifies you exactly once per session to run a project scan, then gracefully gets out of the way.
80
86
 
81
87
  ## MCP Tools
82
88
 
83
- VIBE-SPLAIN exposes **8 tools** over MCP stdio:
89
+ vibe-splain exposes **8 tools** over MCP stdio:
84
90
 
85
91
  | Tool | Purpose |
86
92
  |------|---------|
87
93
  | `scan_project` | **Call first.** Scans the codebase, returns high-gravity files grouped by pillar. Starts file watcher. |
88
94
  | `get_file_context` | Returns full source + import graph neighbors for a specific file. |
89
- | `get_call_chain` | **New.** Traces function-level call chains (upstream/downstream) to map behavior paths. |
90
95
  | `write_decision_card` | Persists a Decision Card (narrative + evidence + optional Mermaid diagram). |
91
96
  | `get_strategic_overview` | Returns dossier state without evidence snippets (saves tokens). |
92
97
  | `inspect_pillar` | Returns all Decision Cards for a pillar with full evidence. |
@@ -98,8 +103,7 @@ VIBE-SPLAIN exposes **8 tools** over MCP stdio:
98
103
  ```
99
104
  1. scan_project → get high-gravity files
100
105
  2. For each file: get_file_context → read source + neighbors
101
- 3. Trace: use get_call_chain to see what calls what
102
- 4. Synthesize: "WHY does this code exist?"
106
+ 3. Synthesize: "WHY does this code exist?"
103
107
  5. write_decision_card → persist the narrative
104
108
  6. Share the file:// UI link with the user
105
109
  ```
@@ -107,10 +111,10 @@ VIBE-SPLAIN exposes **8 tools** over MCP stdio:
107
111
  ## How It Works
108
112
 
109
113
  ### Deep Analysis Pipeline
110
- Unlike simple regex scanners, VIBE-SPLAIN runs a deterministic **13-stage pipeline**—from AST inventory and alias resolution to semantic classification and function-level scoring—to ensure every Decision Card is grounded in actual code paths.
114
+ Unlike simple regex scanners, vibe-splain runs a deterministic **13-stage pipeline**—from AST inventory and alias resolution to semantic classification and function-level scoring—to ensure every Decision Card is grounded in actual code paths.
111
115
 
112
116
  ### Semantic Rulesets
113
- VIBE-SPLAIN uses specialized rulesets to understand framework-specific semantics. Current optimizations include:
117
+ vibe-splain uses specialized rulesets to understand framework-specific semantics. Current optimizations include:
114
118
  - **Next.js**: Server Actions, `cookies()`, `headers()`, and App Router conventions.
115
119
  - **Database**: Prisma model mutations and raw query patterns.
116
120
  - **API**: tRPC procedure calls (`mutate`/`query`) and standard `fetch`/`axios` patterns.
@@ -137,9 +141,11 @@ The UI features:
137
141
 
138
142
  ```
139
143
  packages/
140
- ├── brain/ # @vibe-splain/brain — analysis engine
144
+ ├── brain/ # @vibe-splain/brain — repo-agnostic analysis core + bundled adapters
141
145
  │ └── src/
142
- │ ├── scanner.ts # Tree-Sitter AST analysis (L0 + L1 + L2)
146
+ │ ├── pipeline/
147
+ │ │ └── adapters/ # Bundled product adapters (Cal.com, n8n)
148
+ │ ├── scanner.ts # Tree-Sitter AST analysis (static analysis, graph building)
143
149
  │ ├── dossier.ts # Atomic persistence + UI regeneration
144
150
  │ ├── graph.ts # Import graph read/write
145
151
  │ └── watcher.ts # Chokidar file watcher
@@ -161,7 +167,7 @@ packages/
161
167
 
162
168
  ### Key Design Decisions
163
169
 
164
- - **No LLM calls**: VIBE-SPLAIN is a pure static analysis tool. The coding agent provides all synthesis.
170
+ - **No LLM calls**: vibe-splain is a pure static analysis tool. The coding agent provides all synthesis.
165
171
  - **`async-mutex`**: All dossier writes are guarded by a mutex with atomic tmp+rename.
166
172
  - **`startOnLoad: false`**: Mermaid is initialized manually — never auto-scans the DOM.
167
173
  - **`base: './'`**: Vite builds with relative paths so the UI works from `file://` URLs.
@@ -0,0 +1,9 @@
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>;
@@ -0,0 +1,84 @@
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 +1,6 @@
1
+ /**
2
+ * Idempotently register the vibe-splain PreToolUse hook in a Claude Code settings object.
3
+ * Pure: takes a config, returns the same object mutated. Safe to run repeatedly.
4
+ */
5
+ export declare function addPreToolUseHook(config: Record<string, any>, hookPath?: string): Record<string, any>;
1
6
  export declare function installCommand(): Promise<void>;
@@ -1,7 +1,8 @@
1
1
  import { readFile, writeFile } from 'fs/promises';
2
2
  import { existsSync } from 'fs';
3
- import { join } from 'path';
3
+ import { join, dirname } from 'path';
4
4
  import { homedir } from 'os';
5
+ import { fileURLToPath } from 'url';
5
6
  function expandPath(p) {
6
7
  if (p.startsWith('~')) {
7
8
  return join(homedir(), p.slice(1));
@@ -24,8 +25,48 @@ const MCP_ENTRY = {
24
25
  command: 'npx',
25
26
  args: ['-y', 'vibe-splain', 'serve'],
26
27
  };
28
+ // Claude Code PreToolUse hook: deterministically gate edits to high-blast-radius
29
+ // files. Hooks are a Claude Code CLI feature (settings.json) — not Desktop/Cursor.
30
+ const HOOK_MATCHER = 'Edit|Write|MultiEdit';
31
+ const DEFAULT_HOOK_COMMAND = 'npx -y vibe-splain hook pretooluse';
32
+ /**
33
+ * Idempotently register the vibe-splain PreToolUse hook in a Claude Code settings object.
34
+ * Pure: takes a config, returns the same object mutated. Safe to run repeatedly.
35
+ */
36
+ export function addPreToolUseHook(config, hookPath) {
37
+ if (!config.hooks)
38
+ config.hooks = {};
39
+ if (!Array.isArray(config.hooks.PreToolUse))
40
+ config.hooks.PreToolUse = [];
41
+ const hookCommand = hookPath ? `node "${hookPath}"` : DEFAULT_HOOK_COMMAND;
42
+ const already = config.hooks.PreToolUse.some((entry) => Array.isArray(entry?.hooks) &&
43
+ entry.hooks.some((h) => typeof h?.command === 'string' && (h.command.includes('vibe-splain hook pretooluse') || h.command.includes('hook.js'))));
44
+ if (already) {
45
+ // Update existing hook command to the resolved hook.js path
46
+ config.hooks.PreToolUse = config.hooks.PreToolUse.map((entry) => {
47
+ if (Array.isArray(entry?.hooks)) {
48
+ return {
49
+ ...entry,
50
+ hooks: entry.hooks.map((h) => {
51
+ if (typeof h?.command === 'string' && (h.command.includes('vibe-splain hook pretooluse') || h.command.includes('hook.js'))) {
52
+ return { ...h, command: hookCommand };
53
+ }
54
+ return h;
55
+ })
56
+ };
57
+ }
58
+ return entry;
59
+ });
60
+ return config;
61
+ }
62
+ config.hooks.PreToolUse.push({
63
+ matcher: HOOK_MATCHER,
64
+ hooks: [{ type: 'command', command: hookCommand }],
65
+ });
66
+ return config;
67
+ }
27
68
  export async function installCommand() {
28
- console.error('\n🔧 VIBE-SPLAIN Installer\n');
69
+ console.error('\n🔧 vibe-splain Installer\n');
29
70
  let patchedCount = 0;
30
71
  for (const agent of AGENT_CONFIGS) {
31
72
  const resolvedPath = expandPath(agent.path);
@@ -43,17 +84,25 @@ export async function installCommand() {
43
84
  console.error(` ⚠ Could not parse JSON, skipping`);
44
85
  continue;
45
86
  }
46
- // Check idempotency
47
- if (config.mcpServers?.['vibe-splain']) {
87
+ // Patch config (idempotent: only write when something actually changes,
88
+ // so existing users still pick up newly-added pieces like the hook).
89
+ const before = JSON.stringify(config);
90
+ if (!config.mcpServers)
91
+ config.mcpServers = {};
92
+ if (!config.mcpServers['vibe-splain'])
93
+ config.mcpServers['vibe-splain'] = MCP_ENTRY;
94
+ // The PreToolUse gate is a Claude Code CLI feature (settings.json hooks).
95
+ // Only register it there; Desktop/Gemini/Cursor don't run these hooks.
96
+ if (agent.name === 'Claude Code CLI') {
97
+ const __dirname = dirname(fileURLToPath(import.meta.url));
98
+ const hookPath = join(__dirname, '..', 'hook.js');
99
+ addPreToolUseHook(config, hookPath);
100
+ }
101
+ if (JSON.stringify(config) === before) {
48
102
  console.error(` ✓ Already configured, skipping`);
49
103
  patchedCount++;
50
104
  continue;
51
105
  }
52
- // Patch config
53
- if (!config.mcpServers) {
54
- config.mcpServers = {};
55
- }
56
- config.mcpServers['vibe-splain'] = MCP_ENTRY;
57
106
  await writeFile(resolvedPath, JSON.stringify(config, null, 2), 'utf8');
58
107
  console.error(` ✅ Patched successfully`);
59
108
  patchedCount++;
@@ -0,0 +1 @@
1
+ export declare function networkPlanCommand(task: string, options: any): Promise<void>;
@@ -0,0 +1,91 @@
1
+ import { join, resolve } from 'path';
2
+ import { readFile, writeFile, mkdir } from 'fs/promises';
3
+ import { buildExpertNetworkPacket } from '@vibe-splain/brain';
4
+ export async function networkPlanCommand(task, options) {
5
+ const projectRoot = options.root ? resolve(options.root) : process.cwd();
6
+ const maxFiles = options.maxFiles ? parseInt(options.maxFiles, 10) : 8;
7
+ // Try to load scan artifact
8
+ let scanArtifact = null;
9
+ const artifactPath = options.scanArtifact
10
+ ? resolve(options.scanArtifact)
11
+ : join(projectRoot, '.vibe-splainer', 'analysis.json');
12
+ try {
13
+ const raw = await readFile(artifactPath, 'utf8');
14
+ scanArtifact = JSON.parse(raw);
15
+ }
16
+ catch (err) {
17
+ if (options.scanArtifact) {
18
+ console.error(`[vibe-splain] Error reading scan artifact at ${artifactPath}:`, err.message);
19
+ }
20
+ }
21
+ // Build the packet
22
+ const packet = buildExpertNetworkPacket({
23
+ task,
24
+ projectRoot,
25
+ scanArtifact,
26
+ errorTrace: options.errorTrace,
27
+ intendedBehavior: options.intendedBehavior,
28
+ maxFiles
29
+ });
30
+ // Save artifacts
31
+ const networkDir = join(projectRoot, '.vibe-splainer', 'network');
32
+ const packetsDir = join(networkDir, 'packets');
33
+ try {
34
+ await mkdir(packetsDir, { recursive: true });
35
+ // Save latest
36
+ const latestPath = join(networkDir, 'latest_packet.json');
37
+ await writeFile(latestPath, JSON.stringify(packet, null, 2), 'utf8');
38
+ // Save timestamped
39
+ const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
40
+ const timestampedPath = join(packetsDir, `${timestamp}.json`);
41
+ await writeFile(timestampedPath, JSON.stringify(packet, null, 2), 'utf8');
42
+ if (options.json) {
43
+ process.stdout.write(JSON.stringify(packet, null, 2) + '\n');
44
+ }
45
+ else {
46
+ process.stdout.write('────────────────────────────────────────────────────────\n');
47
+ process.stdout.write(' Vibe-Splain Specialist Network — Task Plan\n');
48
+ process.stdout.write('────────────────────────────────────────────────────────\n');
49
+ process.stdout.write(`Task: "${packet.task}"\n`);
50
+ process.stdout.write(`Project Root: ${packet.projectRoot}\n`);
51
+ process.stdout.write(`Primary Specialist: ${packet.route.primarySpecialist}\n`);
52
+ process.stdout.write(`Panel: ${packet.route.panel.join(', ')}\n`);
53
+ process.stdout.write(`Confidence: ${packet.route.confidence}\n`);
54
+ process.stdout.write(`Reason: ${packet.route.reason}\n`);
55
+ process.stdout.write('\nSelected Files:\n');
56
+ if (packet.selectedFiles.length > 0) {
57
+ packet.selectedFiles.forEach(f => {
58
+ process.stdout.write(` • ${f.path} [blastRadius: ${f.blastRadius}]\n`);
59
+ process.stdout.write(` Reason: ${f.reason}\n`);
60
+ });
61
+ }
62
+ else {
63
+ process.stdout.write(' (No files selected)\n');
64
+ }
65
+ process.stdout.write('\nRisk Warnings:\n');
66
+ if (packet.riskWarnings.length > 0) {
67
+ packet.riskWarnings.forEach(w => {
68
+ const levelLabel = w.level === 'critical' ? '🔴 CRITICAL' : (w.level === 'warning' ? '🟡 WARNING' : 'ℹ️ INFO');
69
+ process.stdout.write(` • [${levelLabel}] ${w.message}\n`);
70
+ process.stdout.write(` Reason: ${w.reason}\n`);
71
+ });
72
+ }
73
+ else {
74
+ process.stdout.write(' (No risks identified)\n');
75
+ }
76
+ process.stdout.write('\nSmallest Safe Change Policy:\n');
77
+ process.stdout.write(` ${packet.smallestSafeChangePolicy.summary}\n`);
78
+ process.stdout.write('\nSaved Artifacts:\n');
79
+ process.stdout.write(` • Latest: ${latestPath}\n`);
80
+ process.stdout.write(` • Packet: ${timestampedPath}\n`);
81
+ process.stdout.write('────────────────────────────────────────────────────────\n');
82
+ }
83
+ }
84
+ catch (err) {
85
+ console.error('[vibe-splain] Error saving network artifacts:', err.message);
86
+ if (options.json) {
87
+ process.stdout.write(JSON.stringify(packet, null, 2) + '\n');
88
+ }
89
+ }
90
+ }
91
+ //# sourceMappingURL=network.js.map
@@ -0,0 +1,13 @@
1
+ interface ScanOptions {
2
+ root?: string;
3
+ out?: string;
4
+ json?: boolean;
5
+ }
6
+ /**
7
+ * Headless one-shot scan. Reuses the exact MCP scan pipeline (performScan),
8
+ * relocates the artifact bundle to --out, and prints a summary. The gravity
9
+ * formula is selected by the ROZE_GRAVITY_V2 env var, never a CLI flag — v2 is
10
+ * still an experimental candidate (see CONTEXT.md / Phase 0).
11
+ */
12
+ export declare function scanCommand(options?: ScanOptions): Promise<void>;
13
+ export {};
@@ -0,0 +1,97 @@
1
+ import { resolve, join, dirname } from 'path';
2
+ import { rm, rename, mkdir, writeFile } from 'fs/promises';
3
+ import { performScan } from '../mcp/tools/scan_project.js';
4
+ /**
5
+ * Headless one-shot scan. Reuses the exact MCP scan pipeline (performScan),
6
+ * relocates the artifact bundle to --out, and prints a summary. The gravity
7
+ * formula is selected by the ROZE_GRAVITY_V2 env var, never a CLI flag — v2 is
8
+ * still an experimental candidate (see CONTEXT.md / Phase 0).
9
+ */
10
+ export async function scanCommand(options = {}) {
11
+ const root = resolve(options.root || '.');
12
+ const gravityMode = process.env.ROZE_GRAVITY_V2 === '1' ? 'v2_candidate' : 'v1_default';
13
+ console.error(`[roze] Scanning ${root} (gravity mode: ${gravityMode})...`);
14
+ const { result, scanId } = await performScan(root);
15
+ // The pipeline always writes to <root>/.roze. Relocate to --out if different
16
+ // so v1 and v2 runs can coexist for comparison.
17
+ const defaultDir = join(root, '.roze');
18
+ const outDir = resolve(options.out || defaultDir);
19
+ if (outDir !== defaultDir) {
20
+ await rm(outDir, { recursive: true, force: true });
21
+ await mkdir(dirname(outDir), { recursive: true });
22
+ await rename(defaultDir, outDir);
23
+ }
24
+ // Persist the gravity mode so v1/v2 scans are self-identifying.
25
+ const meta = {
26
+ gravityMode,
27
+ scannedAt: new Date().toISOString(),
28
+ root,
29
+ scanId,
30
+ totalFilesScanned: result.totalFilesScanned,
31
+ realSourceCount: result.realSourceCount,
32
+ wildCandidateCount: result.wildCandidates.length,
33
+ };
34
+ await writeFile(join(outDir, 'scan_meta.json'), JSON.stringify(meta, null, 2));
35
+ // Build gravity/heat lookups from the scored files for display.
36
+ const gravityByPath = new Map(result.files.map(f => [f.relativePath, f.gravity]));
37
+ const heatByPath = new Map(result.files.map(f => [f.relativePath, f.heat]));
38
+ const topGravity = result.map.topGravity.slice(0, 12).map(p => ({
39
+ path: p,
40
+ gravity: Math.round(gravityByPath.get(p) ?? 0),
41
+ }));
42
+ const topHeat = result.map.topHeat.slice(0, 12).map(p => ({
43
+ path: p,
44
+ heat: Math.round(heatByPath.get(p) ?? 0),
45
+ }));
46
+ const wildDiscovery = result.wildCandidates.slice(0, 12).map(f => ({
47
+ path: f.relativePath,
48
+ heat: Math.round(f.heat),
49
+ gravity: Math.round(f.gravity),
50
+ topSmells: f.smells.filter(s => s.severity >= 3).slice(0, 3).map(s => s.note),
51
+ }));
52
+ const specialistMap = result.map.pillars.map(p => ({
53
+ name: p.name,
54
+ fileCount: p.memberFiles.length,
55
+ }));
56
+ const summary = {
57
+ gravityMode,
58
+ root,
59
+ outDir,
60
+ dossier: join(outDir, 'dossier.json'),
61
+ analysis: join(outDir, 'analysis.json'),
62
+ stack: result.map.stack,
63
+ totalFilesScanned: result.totalFilesScanned,
64
+ realSourceCount: result.realSourceCount,
65
+ topGravity,
66
+ topHeat,
67
+ wildDiscovery,
68
+ specialistMap,
69
+ };
70
+ if (options.json) {
71
+ process.stdout.write(JSON.stringify(summary, null, 2) + '\n');
72
+ return;
73
+ }
74
+ const lines = [];
75
+ lines.push('');
76
+ lines.push(`roze scan — gravity mode: ${gravityMode}`);
77
+ lines.push(` root: ${root}`);
78
+ lines.push(` stack: ${result.map.stack.join(', ') || '(none detected)'}`);
79
+ lines.push(` files: ${result.totalFilesScanned} scanned, ${result.realSourceCount} real-source`);
80
+ lines.push('');
81
+ lines.push('Top Gravity (Start Here):');
82
+ topGravity.forEach((f, i) => lines.push(` ${String(i + 1).padStart(2)}. [${String(f.gravity).padStart(3)}] ${f.path}`));
83
+ lines.push('');
84
+ lines.push('Top Heat (Wild Discovery):');
85
+ topHeat.forEach((f, i) => lines.push(` ${String(i + 1).padStart(2)}. [${String(f.heat).padStart(3)}] ${f.path}`));
86
+ lines.push('');
87
+ lines.push('Specialist Map (pillars):');
88
+ specialistMap.forEach(p => lines.push(` - ${p.name} (${p.fileCount} files)`));
89
+ lines.push('');
90
+ lines.push(`Artifacts written to: ${outDir}`);
91
+ lines.push(` dossier: ${join(outDir, 'dossier.json')}`);
92
+ lines.push(` analysis: ${join(outDir, 'analysis.json')}`);
93
+ lines.push(` meta: ${join(outDir, 'scan_meta.json')} (gravityMode: ${gravityMode})`);
94
+ lines.push('');
95
+ process.stdout.write(lines.join('\n'));
96
+ }
97
+ //# sourceMappingURL=scan.js.map
@@ -1,8 +1,7 @@
1
- import { readAnalysis, readActionBindings, RecommendationEngine } from '@vibe-splain/brain';
1
+ import { readAnalysis, RecommendationEngine, buildGateIndex } from '@vibe-splain/brain';
2
2
  import { ArtifactBundleWriter } from './ArtifactBundleWriter.js';
3
3
  import { JsonRenderer } from './renderers/JsonRenderer.js';
4
4
  import { HtmlRenderer } from './renderers/HtmlRenderer.js';
5
- import { DeltaRenderer } from './renderers/DeltaRenderer.js';
6
5
  import { AgentMarkdownRenderer } from './renderers/AgentMarkdownRenderer.js';
7
6
  import { ValidationRenderer } from './renderers/ValidationRenderer.js';
8
7
  import { RawAnalysisRenderer } from './renderers/RawAnalysisRenderer.js';
@@ -20,22 +19,25 @@ export class ExportOrchestrator {
20
19
  if (!finalStore) {
21
20
  throw new Error('Analysis store not found. Scan the project first.');
22
21
  }
23
- const bindings = await readActionBindings(this.projectRoot);
24
22
  // Aggressive Boilerplate Culling
25
23
  for (const p of dossier.pillars) {
26
24
  p.decisions = p.decisions.filter(c => !(c.severity === 1 && c.category === 'Convention'));
27
25
  p.cardCount = p.decisions.length;
28
26
  }
29
27
  const viewModel = this.buildViewModel(dossier, finalStore);
30
- // Always render JSON, Delta, Validation, and Raw Analysis.
31
28
  const artifacts = [];
32
29
  artifacts.push(...await new JsonRenderer().render(viewModel, finalStore));
33
- artifacts.push(...await new DeltaRenderer().render(viewModel, finalStore));
34
30
  artifacts.push(...await new ValidationRenderer().render(viewModel, finalStore));
35
31
  artifacts.push(...await new RawAnalysisRenderer().render(viewModel, finalStore));
36
32
  if (graph) {
37
33
  artifacts.push(...await new GraphRenderer(graph).render(viewModel, finalStore));
38
34
  }
35
+ const gateIndex = buildGateIndex(finalStore);
36
+ artifacts.push({
37
+ type: 'gate',
38
+ path: 'gate.json',
39
+ content: JSON.stringify(gateIndex, null, 2),
40
+ });
39
41
  // Determine additional formats
40
42
  const formats = ['html', 'markdown']; // default
41
43
  if (options.format && options.format !== 'json' && options.format !== 'delta') {
@@ -46,7 +48,7 @@ export class ExportOrchestrator {
46
48
  artifacts.push(...await new HtmlRenderer().render(viewModel, finalStore));
47
49
  }
48
50
  if (formats.includes('markdown')) {
49
- artifacts.push(...await new AgentMarkdownRenderer(options.budget, bindings).render(viewModel, finalStore));
51
+ artifacts.push(...await new AgentMarkdownRenderer(options.budget).render(viewModel, finalStore));
50
52
  }
51
53
  const writer = new ArtifactBundleWriter(this.projectRoot);
52
54
  await writer.writeBundle(artifacts);
@@ -3,7 +3,6 @@ import type { Renderer } from './Renderer.js';
3
3
  import type { Artifact } from '../ArtifactBundleWriter.js';
4
4
  export declare class AgentMarkdownRenderer implements Renderer {
5
5
  private budget;
6
- private bindings;
7
- constructor(budget?: number, bindings?: any | null);
6
+ constructor(budget?: number);
8
7
  render(viewModel: DossierViewModel, store: AnalysisStore): Artifact[];
9
8
  }
@@ -1,9 +1,7 @@
1
1
  export class AgentMarkdownRenderer {
2
2
  budget;
3
- bindings;
4
- constructor(budget = 8000, bindings = null) {
3
+ constructor(budget = 8000) {
5
4
  this.budget = budget;
6
- this.bindings = bindings;
7
5
  }
8
6
  render(viewModel, store) {
9
7
  let md = `# Architectural Dossier: ${viewModel.projectRoot}\n\n`;
@@ -54,20 +52,6 @@ export class AgentMarkdownRenderer {
54
52
  md += `**Severity**: ${card.severity} | **Category**: ${card.category}\n`;
55
53
  md += `**Narrative**: ${card.narrative}\n`;
56
54
  }
57
- // Add Function-Level Action Bindings for Tier 1
58
- if (this.bindings && this.bindings.files[path]) {
59
- const fileBinding = this.bindings.files[path];
60
- const criticalFunctions = fileBinding.functions.filter((fn) => fn.semanticActions.length > 0 || fn.isEntrypoint);
61
- if (criticalFunctions.length > 0) {
62
- md += `\n**Critical Functions**:\n`;
63
- for (const fn of criticalFunctions) {
64
- md += `- \`${fn.displayName}\` (lines ${fn.startLine}-${fn.endLine})${fn.isEntrypoint ? ' [Entrypoint]' : ''}\n`;
65
- for (const action of fn.semanticActions) {
66
- md += ` - **${action.actionKind}**${action.targetModel ? ` on ${action.targetModel}` : ''}: \`${action.calleeText}\` (line ${action.sourceLine})\n`;
67
- }
68
- }
69
- }
70
- }
71
55
  if (recs.length > 0) {
72
56
  md += `\n**Safe Patch Strategies**:\n`;
73
57
  for (const r of recs) {
@@ -0,0 +1,30 @@
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;