vibe-splain 2.4.1 → 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/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" }
@@ -87,12 +88,11 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
87
88
  import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema } from "@modelcontextprotocol/sdk/types.js";
88
89
 
89
90
  // ../brain/dist/scanner.js
90
- import Parser from "web-tree-sitter";
91
- import { join as join4, dirname, relative, extname, basename, sep } from "path";
92
- import { fileURLToPath } from "url";
93
- import { createRequire } from "module";
94
- import { readFile as readFile4, readdir } from "fs/promises";
95
- import { existsSync as existsSync2 } from "fs";
91
+ import { extname as extname4 } from "path";
92
+ import { readFile as readFile7 } from "fs/promises";
93
+
94
+ // ../brain/dist/pipeline/orchestrator.js
95
+ import { join as join8 } from "path";
96
96
 
97
97
  // ../brain/dist/graph.js
98
98
  import { join as join2 } from "path";
@@ -106,7 +106,6 @@ async function writeGraph(projectRoot, graph) {
106
106
 
107
107
  // ../brain/dist/analysis.js
108
108
  import { join as join3 } from "path";
109
- import { createHash } from "crypto";
110
109
  import { readFile as readFile3, writeFile as writeFile3, mkdir as mkdir2 } from "fs/promises";
111
110
  async function readAnalysis(projectRoot) {
112
111
  const p = join3(projectRoot, ".vibe-splainer", "analysis.json");
@@ -126,575 +125,279 @@ async function writeAnalysis(projectRoot, store) {
126
125
  const { rename } = await import("fs/promises");
127
126
  await rename(tmp, dest);
128
127
  }
129
- var ENTRYPOINT_ROLES = /* @__PURE__ */ new Set([
130
- "app_route_page",
131
- "app_route_handler",
132
- "pages_route",
133
- "pages_api_route",
134
- "trpc_api_route"
128
+
129
+ // ../brain/dist/pipeline/inventory.js
130
+ import Parser from "web-tree-sitter";
131
+ import { join as join4, dirname, relative, extname, basename, sep } from "path";
132
+ import { fileURLToPath } from "url";
133
+ import { createRequire } from "module";
134
+ import { readFile as readFile4, readdir, writeFile as writeFile4, mkdir as mkdir3 } from "fs/promises";
135
+ import { existsSync as existsSync2 } from "fs";
136
+ var __dirname = dirname(fileURLToPath(import.meta.url));
137
+ var require2 = createRequire(import.meta.url);
138
+ var _parser = null;
139
+ var langCache = /* @__PURE__ */ new Map();
140
+ var EXT_LANG = {
141
+ ".ts": "typescript",
142
+ ".tsx": "tsx",
143
+ ".js": "javascript",
144
+ ".jsx": "tsx",
145
+ ".mjs": "javascript",
146
+ ".cjs": "javascript",
147
+ ".py": "python",
148
+ ".go": "go",
149
+ ".rs": "rust",
150
+ ".java": "java"
151
+ };
152
+ var LANG_WASM = {
153
+ typescript: "tree-sitter-typescript.wasm",
154
+ tsx: "tree-sitter-tsx.wasm",
155
+ javascript: "tree-sitter-javascript.wasm",
156
+ python: "tree-sitter-python.wasm",
157
+ go: "tree-sitter-go.wasm",
158
+ rust: "tree-sitter-rust.wasm",
159
+ java: "tree-sitter-java.wasm"
160
+ };
161
+ var SUPPORTED_EXTENSIONS = new Set(Object.keys(EXT_LANG));
162
+ function resolveWasm(file) {
163
+ try {
164
+ const wasmsDir = dirname(require2.resolve("tree-sitter-wasms/package.json"));
165
+ const p = join4(wasmsDir, "out", file);
166
+ if (existsSync2(p))
167
+ return p;
168
+ } catch {
169
+ }
170
+ const local = join4(__dirname, "../../wasm", file);
171
+ return existsSync2(local) ? local : null;
172
+ }
173
+ async function getLanguage(lang) {
174
+ const cached = langCache.get(lang);
175
+ if (cached)
176
+ return cached;
177
+ const wasm = resolveWasm(LANG_WASM[lang]);
178
+ if (!wasm) {
179
+ console.error(`[vibe-splain] grammar missing for ${lang} (${LANG_WASM[lang]}); skipping`);
180
+ return null;
181
+ }
182
+ try {
183
+ const loaded = await Parser.Language.load(wasm);
184
+ langCache.set(lang, loaded);
185
+ return loaded;
186
+ } catch (err) {
187
+ console.error(`[vibe-splain] failed to load grammar for ${lang}:`, err instanceof Error ? err.message : err);
188
+ return null;
189
+ }
190
+ }
191
+ async function initParser() {
192
+ if (_parser)
193
+ return _parser;
194
+ await Parser.init();
195
+ _parser = new Parser();
196
+ const ts = await getLanguage("typescript");
197
+ if (ts)
198
+ _parser.setLanguage(ts);
199
+ return _parser;
200
+ }
201
+ async function parseAs(lang, source) {
202
+ const p = await initParser();
203
+ const language = await getLanguage(lang);
204
+ if (!language)
205
+ return null;
206
+ p.setLanguage(language);
207
+ try {
208
+ return p.parse(source);
209
+ } catch {
210
+ return null;
211
+ }
212
+ }
213
+ var EXCLUDE_DIRS = /* @__PURE__ */ new Set([
214
+ "node_modules",
215
+ "dist",
216
+ "build",
217
+ ".next",
218
+ "out",
219
+ ".vibe-splainer",
220
+ ".git",
221
+ ".venv",
222
+ "venv",
223
+ "env",
224
+ "__pycache__",
225
+ ".idea",
226
+ ".vscode",
227
+ ".cache",
228
+ "site-packages",
229
+ "target",
230
+ ".tox",
231
+ ".mypy_cache",
232
+ ".pytest_cache"
135
233
  ]);
136
- function findRuntimeEntrypoints(relPath, files, maxDepth = 8) {
137
- const results = [];
138
- const seen = /* @__PURE__ */ new Set();
139
- const queue = [{ path: relPath, depth: 0 }];
140
- while (queue.length > 0) {
141
- const current = queue.shift();
142
- if (seen.has(current.path))
143
- continue;
144
- seen.add(current.path);
145
- if (current.path !== relPath) {
146
- const meta2 = files[current.path];
147
- if (meta2 && ENTRYPOINT_ROLES.has(meta2.frameworkRole)) {
148
- results.push({
149
- path: current.path,
150
- frameworkRole: meta2.frameworkRole,
151
- productDomain: meta2.productDomain,
152
- distance: current.depth
153
- });
154
- if (results.length >= 8)
155
- break;
234
+ var EXCLUDE_FILE_PATTERNS = [/\.lock$/, /\.min\.[a-z]+$/, /\.d\.ts$/];
235
+ var DEMOTE_SEGMENTS = /* @__PURE__ */ new Set([
236
+ "docs",
237
+ "doc",
238
+ "examples",
239
+ "example",
240
+ "samples",
241
+ "sample",
242
+ "mockup",
243
+ "mockups",
244
+ "fixtures",
245
+ "fixture",
246
+ "__generated__",
247
+ "__mocks__",
248
+ "playwright",
249
+ "e2e",
250
+ "__tests__",
251
+ "cypress",
252
+ "storybook",
253
+ "stories",
254
+ ".storybook"
255
+ ]);
256
+ var VENDOR_SEGMENTS = /* @__PURE__ */ new Set([
257
+ "node_modules",
258
+ "vendor",
259
+ "vendored",
260
+ "site-packages",
261
+ "third_party",
262
+ "third-party"
263
+ ]);
264
+ async function collectFiles(dir, projectRoot, acc) {
265
+ let entries;
266
+ try {
267
+ entries = await readdir(dir, { withFileTypes: true });
268
+ } catch {
269
+ return;
270
+ }
271
+ for (const entry of entries) {
272
+ if (entry.name.startsWith(".") && entry.name !== ".") {
273
+ if (entry.isDirectory())
156
274
  continue;
157
- }
158
275
  }
159
- if (current.depth >= maxDepth)
160
- continue;
161
- const meta = files[current.path];
162
- if (!meta)
276
+ if (EXCLUDE_DIRS.has(entry.name))
163
277
  continue;
164
- for (const importer of meta.importedBy) {
165
- if (!seen.has(importer)) {
166
- queue.push({ path: importer, depth: current.depth + 1 });
167
- }
278
+ const fullPath = join4(dir, entry.name);
279
+ if (entry.isDirectory()) {
280
+ await collectFiles(fullPath, projectRoot, acc);
281
+ } else if (entry.isFile()) {
282
+ const ext = extname(entry.name);
283
+ if (!SUPPORTED_EXTENSIONS.has(ext))
284
+ continue;
285
+ if (EXCLUDE_FILE_PATTERNS.some((p) => p.test(entry.name)))
286
+ continue;
287
+ acc.push(fullPath);
168
288
  }
169
289
  }
170
- const byPath = /* @__PURE__ */ new Map();
171
- for (const r of results) {
172
- const existing = byPath.get(r.path);
173
- if (!existing || r.distance < existing.distance)
174
- byPath.set(r.path, r);
290
+ }
291
+ function pathDemoteReason(relPath) {
292
+ const segs = relPath.split(sep);
293
+ for (const s of segs) {
294
+ if (VENDOR_SEGMENTS.has(s))
295
+ return `vendored code (${s})`;
296
+ if (s.endsWith(".venv") || s === "venv" || s === "env")
297
+ return "virtual environment";
175
298
  }
176
- return [...byPath.values()].sort((a, b) => a.distance - b.distance);
299
+ for (const s of segs) {
300
+ if (DEMOTE_SEGMENTS.has(s.toLowerCase()))
301
+ return `non-application path segment (${s})`;
302
+ }
303
+ const b = basename(relPath);
304
+ if (/\.min\./.test(b))
305
+ return "minified bundle";
306
+ if (/\.generated\./.test(b))
307
+ return "generated file";
308
+ return null;
177
309
  }
178
- function deriveEntrypointTraceStatus(entrypoints, unresolved) {
179
- if (entrypoints.length > 0 && unresolved.length === 0)
180
- return "complete";
181
- if (entrypoints.length > 0)
182
- return "partial";
183
- if (unresolved.length > 0)
184
- return "blocked_by_alias_resolution";
185
- return "no_runtime_entrypoint_found";
310
+ function inferFrameworkRole(relPath) {
311
+ const p = relPath.replace(/\\/g, "/");
312
+ if (/\.test\.|\.spec\./.test(p))
313
+ return "test";
314
+ if (/\.generated\.|__generated__|\.prisma\//.test(p))
315
+ return "generated";
316
+ if (/(?:^|\/)app\/.*\/page\.tsx?$/.test(p))
317
+ return "app_route_page";
318
+ if (/(?:^|\/)app\/.*\/layout\.tsx?$/.test(p))
319
+ return "app_route_layout";
320
+ if (/(?:^|\/)app\/.*\/route\.tsx?$/.test(p))
321
+ return "app_route_handler";
322
+ if (/(?:^|\/)app\/.*\/loading\.tsx?$/.test(p))
323
+ return "app_loading_boundary";
324
+ if (/(?:^|\/)app\/.*\/error\.tsx?$/.test(p))
325
+ return "app_error_boundary";
326
+ if (/(?:^|\/)pages\/api\/trpc\//.test(p))
327
+ return "trpc_api_route";
328
+ if (/(?:^|\/)pages\/api\//.test(p))
329
+ return "pages_api_route";
330
+ if (/(?:^|\/)pages\//.test(p))
331
+ return "pages_route";
332
+ if (/\/hooks\/|\/use[A-Z][^/]*\.(ts|tsx)$/.test(p))
333
+ return "hook";
334
+ if (/\/stores?\/|[Ss]tore\.(ts|tsx)$/.test(p))
335
+ return "store";
336
+ if (/[Pp]rovider\.(tsx?|jsx?)$|\/providers?\//.test(p))
337
+ return "provider";
338
+ if (/\.types\.ts$|\/types\.ts$|\/types\/[^/]+\.ts$/.test(p))
339
+ return "type_definition";
340
+ if (/\.(tsx|jsx)$/.test(p))
341
+ return "component";
342
+ if (/\.(ts|js|mjs|cjs)$/.test(p))
343
+ return "utility";
344
+ return "unknown";
186
345
  }
187
- function computeLoadBearingScore(f, entrypoints) {
188
- let score = 0;
189
- if (f.gravity >= 85)
190
- score += 2;
191
- if (f.heat >= 60)
192
- score += 1;
193
- if (entrypoints.length >= 2)
194
- score += 2;
195
- if (f.importedBy.length >= 3)
196
- score += 1;
197
- if (f.sideEffectProfile.includes("database_write"))
198
- score += 3;
199
- if (f.sideEffectProfile.includes("booking_mutation"))
200
- score += 3;
201
- if (f.sideEffectProfile.includes("payment_mutation"))
202
- score += 3;
203
- if (f.sideEffectProfile.includes("auth_token_mutation"))
204
- score += 3;
205
- if (f.sideEffectProfile.includes("webhook_delivery"))
206
- score += 2;
207
- if (f.sideEffectProfile.includes("webhook_ingress"))
208
- score += 2;
209
- if (f.sideEffectProfile.includes("calendar_mutation"))
210
- score += 2;
211
- if (f.sideEffectProfile.includes("redirect"))
212
- score += 1;
213
- if (f.sideEffectProfile.includes("analytics_event"))
214
- score += 1;
215
- const highImpactDomains = [
216
- "booking_creation",
217
- "payments",
218
- "auth_oauth",
219
- "webhooks",
220
- "payments_webhooks"
221
- ];
222
- if (highImpactDomains.includes(f.productDomain))
223
- score += 2;
224
- const maxSeverity = f.smells.length > 0 ? Math.max(...f.smells.map((s) => s.severity)) : 0;
225
- if (maxSeverity === 5)
226
- score += 3;
227
- return score;
228
- }
229
- function computeSeverity(f, entrypoints) {
230
- let score = 0;
231
- if (f.sideEffectProfile.includes("database_write"))
232
- score += 3;
233
- if (f.sideEffectProfile.includes("booking_mutation"))
234
- score += 4;
235
- if (f.sideEffectProfile.includes("payment_mutation"))
236
- score += 4;
237
- if (f.sideEffectProfile.includes("auth_token_mutation"))
238
- score += 4;
239
- if (f.sideEffectProfile.includes("webhook_delivery"))
240
- score += 3;
241
- if (f.sideEffectProfile.includes("webhook_ingress"))
242
- score += 3;
243
- if (f.sideEffectProfile.includes("calendar_mutation"))
244
- score += 3;
245
- if (f.productDomain === "booking_creation")
246
- score += 3;
247
- if (f.productDomain === "payments" || f.productDomain === "payments_webhooks")
248
- score += 3;
249
- if (f.productDomain === "auth_oauth")
250
- score += 3;
251
- if (f.productDomain === "webhooks")
252
- score += 2;
253
- if (f.gravity >= 85)
254
- score += 2;
255
- if (f.heat >= 70)
256
- score += 2;
257
- if (f.heatSignals.maxNesting >= 4)
258
- score += 1;
259
- if (f.heatSignals.longFunctions >= 1)
260
- score += 1;
261
- if (f.heatSignals.swallowedCatches >= 1)
262
- score += 1;
263
- if (entrypoints.length >= 2)
264
- score += 2;
265
- if (score >= 10)
266
- return 5;
267
- if (score >= 7)
268
- return 4;
269
- if (score >= 4)
270
- return 3;
271
- if (score >= 2)
272
- return 2;
273
- return 1;
274
- }
275
- function inferRiskTypes(f) {
276
- const types = [];
277
- const kinds = new Set(f.smells.map((s) => s.kind));
278
- if (f.gravitySignals.cyclomatic > 20)
279
- types.push("state_machine");
280
- if (kinds.has("god-file")) {
281
- if (f.frameworkRole === "hook")
282
- types.push("god_hook");
283
- else
284
- types.push("god_component");
285
- }
286
- if (f.sideEffectProfile.length > 3 && !f.sideEffectProfile.includes("none_detected")) {
287
- types.push("side_effect_coupling");
288
- }
289
- if (f.productDomain === "forms" && f.gravitySignals.fanIn > 5 && f.gravitySignals.publicSurface > 8)
290
- types.push("registry_bottleneck");
291
- if (f.sideEffectProfile.some((s) => ["booking_mutation", "payment_mutation", "auth_token_mutation"].includes(s)) && f.gravitySignals.cyclomatic > 10)
292
- types.push("mutation_orchestration");
293
- if (ENTRYPOINT_ROLES.has(f.frameworkRole) && f.sideEffectProfile.includes("database_write"))
294
- types.push("route_handler_write_path");
295
- if (kinds.has("swallowed-catch"))
296
- types.push("error_swallowing");
297
- if (f.sideEffectProfile.includes("local_storage") || f.sideEffectProfile.includes("indexed_db"))
298
- types.push("storage_persistence_risk");
299
- if (types.length === 0)
300
- types.push("complexity_hotspot");
301
- return types;
302
- }
303
- function inferObservableOutputs(f) {
304
- const outputs = [];
305
- if (f.sideEffectProfile.includes("redirect"))
306
- outputs.push("redirect_url");
307
- if (ENTRYPOINT_ROLES.has(f.frameworkRole))
308
- outputs.push("http_status");
309
- if (f.frameworkRole === "app_route_handler" || f.frameworkRole === "pages_api_route") {
310
- outputs.push("json_response_shape");
311
- }
312
- if (f.productDomain === "booking_creation" || f.productDomain === "booking_management") {
313
- outputs.push("booking_uid");
314
- }
315
- if (f.productDomain === "payments" || f.productDomain === "payments_webhooks") {
316
- outputs.push("payment_status");
317
- }
318
- if (f.productDomain === "auth_oauth") {
319
- outputs.push("auth_token");
320
- }
321
- if (f.sideEffectProfile.includes("webhook_delivery") || f.sideEffectProfile.includes("webhook_ingress")) {
322
- outputs.push("webhook_payload");
323
- }
324
- if (f.sideEffectProfile.includes("calendar_mutation")) {
325
- outputs.push("calendar_event_id");
326
- }
327
- if (f.sideEffectProfile.includes("email_send")) {
328
- outputs.push("email_payload");
329
- }
330
- if (f.sideEffectProfile.includes("analytics_event")) {
331
- outputs.push("sdk_event_name");
332
- }
333
- if (f.frameworkRole === "hook" || f.frameworkRole === "store") {
334
- outputs.push("ui_state_transition");
335
- }
336
- return [...new Set(outputs)];
337
- }
338
- function inferWriteIntents(f) {
339
- const intents = [];
340
- if (f.productDomain === "booking_creation") {
341
- intents.push("create_booking");
342
- if (f.relativePath.includes("reschedule") || f.relativePath.includes("Reschedule"))
343
- intents.push("reschedule_booking");
344
- if (f.relativePath.includes("recurring") || f.relativePath.includes("Recurring")) {
345
- intents.push("create_recurring_booking");
346
- }
347
- }
348
- if (f.productDomain === "booking_management") {
349
- intents.push("cancel_booking");
350
- }
351
- if (f.productDomain === "event_type_configuration") {
352
- intents.push("update_event_type");
353
- }
354
- if (f.productDomain === "availability") {
355
- intents.push("update_availability");
356
- }
357
- if (f.productDomain === "payments") {
358
- intents.push("create_payment");
359
- }
360
- if (f.productDomain === "payments_webhooks") {
361
- intents.push("handle_payment_webhook");
362
- }
363
- if (f.productDomain === "auth_oauth") {
364
- intents.push("issue_auth_token");
365
- intents.push("refresh_auth_token");
366
- }
367
- if (f.sideEffectProfile.includes("webhook_delivery")) {
368
- intents.push("send_webhook");
369
- }
370
- if (f.productDomain === "settings") {
371
- intents.push("update_user_settings");
372
- }
373
- if (f.sideEffectProfile.includes("local_storage") || f.sideEffectProfile.includes("indexed_db")) {
374
- intents.push("persist_local_state");
375
- }
376
- return intents.length > 0 ? intents : ["none_detected"];
377
- }
378
- function inferPatchRisk(f, score, riskTypes) {
379
- if (score >= 12 || f.productDomain === "booking_creation" && riskTypes.includes("mutation_orchestration")) {
380
- return {
381
- level: "critical",
382
- reason: `${f.productDomain} domain with ${riskTypes.join(", ")} \u2014 any patch risks breaking live booking, payment, or auth flows.`
383
- };
384
- }
385
- if (score >= 8 || f.sideEffectProfile.includes("payment_mutation") || f.sideEffectProfile.includes("auth_token_mutation")) {
386
- return {
387
- level: "high",
388
- reason: `${f.productDomain} writes to external state (${f.sideEffectProfile.filter((s) => ["payment_mutation", "auth_token_mutation", "database_write", "webhook_delivery"].includes(s)).join(", ") || "database"}). Changes require integration testing.`
389
- };
390
- }
391
- if (score >= 5 || f.importedBy.length >= 5) {
392
- return {
393
- level: "medium",
394
- reason: `Imported by ${f.importedBy.length} files. Interface changes will cascade.`
395
- };
396
- }
397
- return {
398
- level: "low",
399
- reason: "Locally contained \u2014 limited blast radius."
400
- };
401
- }
402
- function inferSafePatchStrategy(f, riskTypes) {
403
- if (riskTypes.includes("mutation_orchestration")) {
404
- return "Do not rewrite inline. Extract pure decision logic into a tested reducer or state machine first. Preserve all side-effect call sites (redirect URLs, SDK event names, response shapes) as invariants.";
405
- }
406
- if (riskTypes.includes("registry_bottleneck")) {
407
- return "Add new entries without removing existing keys. Treat the registry map as append-only until all consumers are verified.";
408
- }
409
- if (riskTypes.includes("route_handler_write_path")) {
410
- return "Add integration tests covering success and failure paths before modifying. Verify HTTP status codes and response shapes are preserved.";
411
- }
412
- if (riskTypes.includes("god_component") || riskTypes.includes("god_hook")) {
413
- return "Extract sub-concerns into separate modules first. Only refactor the extraction points after tests confirm equivalence.";
414
- }
415
- if (f.sideEffectProfile.includes("database_write")) {
416
- return "Wrap changes in a transaction or use a feature flag. Run against a staging database before production.";
417
- }
418
- return "Review importedBy before patching. Run affected integration tests.";
419
- }
420
- function inferDoNotTouch(f) {
421
- const items = [];
422
- if (f.sideEffectProfile.includes("payment_mutation"))
423
- items.push("payment flow branch");
424
- if (f.sideEffectProfile.includes("auth_token_mutation"))
425
- items.push("token issuance / refresh branch");
426
- if (f.sideEffectProfile.includes("webhook_delivery") || f.sideEffectProfile.includes("webhook_ingress")) {
427
- items.push("webhook payload shape");
428
- }
429
- if (f.sideEffectProfile.includes("redirect"))
430
- items.push("redirect URL strings");
431
- if (f.sideEffectProfile.includes("analytics_event"))
432
- items.push("SDK event names");
433
- if (f.sideEffectProfile.includes("booking_mutation")) {
434
- items.push("booking success response shape", "recurring booking branch");
435
- }
436
- if (f.productDomain === "auth_oauth")
437
- items.push("OAuth callback URLs", "token scopes");
438
- return items;
439
- }
440
- function inferTestProbes(f, writeIntents, observableOutputs) {
441
- const probes = [];
442
- if (writeIntents.includes("create_booking")) {
443
- probes.push({
444
- name: "standard booking success",
445
- scenario: "create a standard booking and assert success redirect and booking uid",
446
- expectedObservable: ["booking_uid", "redirect_url", "sdk_event_name"].filter((o) => observableOutputs.includes(o))
447
- });
448
- }
449
- if (writeIntents.includes("reschedule_booking")) {
450
- probes.push({
451
- name: "reschedule booking",
452
- scenario: "reschedule an existing booking and assert reschedule event path",
453
- expectedObservable: ["booking_uid", "redirect_url"].filter((o) => observableOutputs.includes(o))
454
- });
455
- }
456
- if (writeIntents.includes("create_recurring_booking")) {
457
- probes.push({
458
- name: "recurring booking",
459
- scenario: "create recurring booking and assert recurring success behavior",
460
- expectedObservable: ["booking_uid", "redirect_url"].filter((o) => observableOutputs.includes(o))
461
- });
462
- }
463
- if (writeIntents.includes("handle_payment_webhook")) {
464
- probes.push({
465
- name: "payment webhook ingestion",
466
- scenario: "send a valid payment webhook and assert booking/payment state updated",
467
- expectedObservable: ["payment_status", "booking_uid", "http_status"].filter((o) => observableOutputs.includes(o))
468
- });
469
- }
470
- if (writeIntents.includes("issue_auth_token")) {
471
- probes.push({
472
- name: "token issuance",
473
- scenario: "complete OAuth flow and assert access token issued with correct scopes",
474
- expectedObservable: ["auth_token", "http_status"].filter((o) => observableOutputs.includes(o))
475
- });
476
- }
477
- return probes;
478
- }
479
- function buildRawEvidence(f) {
480
- return f.hotSpans.map((span) => ({
481
- file: f.relativePath,
482
- startLine: span.startLine,
483
- endLine: span.endLine,
484
- rawSourceExcerpt: span.snippet,
485
- evidenceHash: createHash("sha256").update(span.snippet).digest("hex").slice(0, 12)
486
- }));
487
- }
488
- function deriveConfidence(f) {
489
- if (f.gravitySignals.fanIn >= 10 && f.gravity >= 40)
490
- return "high";
491
- if (f.gravitySignals.fanIn >= 5 || f.gravity >= 25)
492
- return "medium";
493
- return "low";
494
- }
495
- function validateTarget(target) {
496
- const warn = (msg) => console.error(`[vibe-splain] WARN ${target.path}: ${msg}`);
497
- const err = (msg) => console.error(`[vibe-splain] ERR ${target.path}: ${msg}`);
498
- if (target.severity >= 4 && target.runtimeEntrypoints.length === 0) {
499
- warn("high severity target has no runtime entrypoints \u2014 check alias resolution");
500
- }
501
- if (target.severity === 5 && !target.isLoadBearing) {
502
- err("severity 5 target must be load bearing");
503
- }
504
- if (target.productDomain === "routing_infrastructure" && !target.path.includes("middleware") && !target.path.includes("router") && !target.path.includes("Navigation")) {
505
- warn("possible over-classification as routing_infrastructure");
506
- }
507
- if ((target.path.includes("payment") || target.path.includes("stripe") || target.path.includes("paypal")) && !target.sideEffectProfile.includes("payment_mutation") && !target.sideEffectProfile.includes("webhook_ingress")) {
508
- warn("payment file missing payment side effect classification");
509
- }
510
- if (target.rawEvidence.some((e) => e.rawSourceExcerpt.includes("// ..") || e.rawSourceExcerpt.includes("/* ..."))) {
511
- err("raw evidence appears summarized or annotated");
512
- }
513
- }
514
- async function writeDeltaTargets(projectRoot, store, _entrypoints = /* @__PURE__ */ new Set()) {
515
- const targets = Object.values(store.files).filter((f) => f.isRealSource).sort((a, b) => b.gravity - a.gravity).map((f) => {
516
- const runtimeEntrypoints = findRuntimeEntrypoints(f.relativePath, store.files);
517
- const entrypointTraceStatus = deriveEntrypointTraceStatus(runtimeEntrypoints, f.importsUnresolved);
518
- const loadBearingScore = computeLoadBearingScore(f, runtimeEntrypoints);
519
- const severity = computeSeverity(f, runtimeEntrypoints);
520
- const riskTypes = inferRiskTypes(f);
521
- const observableOutputs = inferObservableOutputs(f);
522
- const writeIntents = inferWriteIntents(f);
523
- const patchRisk = inferPatchRisk(f, loadBearingScore, riskTypes);
524
- const rawEvidence = buildRawEvidence(f);
525
- const confidence = deriveConfidence(f);
526
- const fileHashInput = f.hotSpans.map((h) => h.snippet).join("");
527
- const fileHash = createHash("sha256").update(fileHashInput || f.relativePath).digest("hex").slice(0, 12);
528
- const target = {
529
- path: f.relativePath,
530
- frameworkRole: f.frameworkRole,
531
- productDomain: f.productDomain,
532
- gravity: Math.round(f.gravity),
533
- heat: Math.round(f.heat),
534
- severity,
535
- confidence,
536
- isLoadBearing: loadBearingScore >= 5,
537
- loadBearingScore,
538
- riskTypes,
539
- sideEffectProfile: f.sideEffectProfile,
540
- blastRadius: f.importedBy,
541
- runtimeEntrypoints,
542
- entrypointTraceStatus,
543
- blockedImports: f.importsUnresolved,
544
- observableOutputs,
545
- writeIntents,
546
- patchRisk,
547
- safePatchStrategy: inferSafePatchStrategy(f, riskTypes),
548
- doNotTouch: inferDoNotTouch(f),
549
- testProbes: inferTestProbes(f, writeIntents, observableOutputs),
550
- rawEvidence,
551
- analysisAnnotation: `${f.frameworkRole} in ${f.productDomain} domain. fanIn=${f.gravitySignals.fanIn} cyclomatic=${f.gravitySignals.cyclomatic} loc=${f.gravitySignals.loc}`,
552
- hashes: {
553
- fileHash,
554
- evidenceHash: rawEvidence.map((e) => e.evidenceHash).join("-")
555
- }
556
- };
557
- validateTarget(target);
558
- return target;
559
- });
560
- const dir = join3(projectRoot, ".vibe-splainer");
561
- await mkdir2(dir, { recursive: true });
562
- const dest = join3(dir, "delta_targets.json");
563
- const tmp = dest + ".tmp";
564
- await writeFile3(tmp, JSON.stringify(targets, null, 2), "utf8");
565
- const { rename } = await import("fs/promises");
566
- await rename(tmp, dest);
567
- }
568
-
569
- // ../brain/dist/scanner.js
570
- var __dirname = dirname(fileURLToPath(import.meta.url));
571
- var require2 = createRequire(import.meta.url);
572
- var parser = null;
573
- var langCache = /* @__PURE__ */ new Map();
574
- var EXT_LANG = {
575
- ".ts": "typescript",
576
- ".tsx": "tsx",
577
- ".js": "javascript",
578
- ".jsx": "tsx",
579
- ".mjs": "javascript",
580
- ".cjs": "javascript",
581
- ".py": "python",
582
- ".go": "go",
583
- ".rs": "rust",
584
- ".java": "java"
585
- };
586
- var LANG_WASM = {
587
- typescript: "tree-sitter-typescript.wasm",
588
- tsx: "tree-sitter-tsx.wasm",
589
- javascript: "tree-sitter-javascript.wasm",
590
- python: "tree-sitter-python.wasm",
591
- go: "tree-sitter-go.wasm",
592
- rust: "tree-sitter-rust.wasm",
593
- java: "tree-sitter-java.wasm"
594
- };
595
- var SUPPORTED_EXTENSIONS = new Set(Object.keys(EXT_LANG));
596
- function resolveWasm(file) {
597
- try {
598
- const wasmsDir = dirname(require2.resolve("tree-sitter-wasms/package.json"));
599
- const p = join4(wasmsDir, "out", file);
600
- if (existsSync2(p))
601
- return p;
602
- } catch {
603
- }
604
- const local = join4(__dirname, "../wasm", file);
605
- return existsSync2(local) ? local : null;
606
- }
607
- async function getLanguage(lang) {
608
- const cached = langCache.get(lang);
609
- if (cached)
610
- return cached;
611
- const wasm = resolveWasm(LANG_WASM[lang]);
612
- if (!wasm) {
613
- console.error(`[vibe-splain] grammar missing for ${lang} (${LANG_WASM[lang]}); skipping language`);
614
- return null;
615
- }
616
- try {
617
- const loaded = await Parser.Language.load(wasm);
618
- langCache.set(lang, loaded);
619
- return loaded;
620
- } catch (err) {
621
- console.error(`[vibe-splain] failed to load grammar for ${lang}:`, err instanceof Error ? err.message : err);
622
- return null;
623
- }
624
- }
625
- async function initParser() {
626
- if (parser)
627
- return parser;
628
- await Parser.init();
629
- parser = new Parser();
630
- const ts = await getLanguage("typescript");
631
- if (ts)
632
- parser.setLanguage(ts);
633
- return parser;
634
- }
635
- async function parseAs(lang, source) {
636
- const p = await initParser();
637
- const language = await getLanguage(lang);
638
- if (!language)
639
- return null;
640
- p.setLanguage(language);
641
- try {
642
- return p.parse(source);
643
- } catch {
644
- return null;
346
+ function inferProductDomain(relPath, importSpecs) {
347
+ const p = relPath.toLowerCase().replace(/\\/g, "/");
348
+ if (/\.test\.|\.spec\.|__tests__|\/e2e\/|\/playwright\/|\/cypress\//.test(p)) {
349
+ return "test_infrastructure";
350
+ }
351
+ if (/\.generated\.|__generated__|\.prisma\//.test(p)) {
352
+ return "generated_noise";
353
+ }
354
+ if (p.includes("booking-audit") || p.includes("bookingaudit"))
355
+ return "booking_audit";
356
+ if (p.includes("bookeventform") || p.includes("availabletimes") || p.includes("availabletimeslots") || p.includes("usebookings") || p.includes("/pages/api/book/") || p.includes("/api/book/") || p.includes("/booking-successful/") || p.includes("/reschedule/") || p.includes("booking-page-wrapper") || p.includes("/book/") && !p.includes("booking-audit"))
357
+ return "booking_creation";
358
+ if (p.includes("modules/bookings") || p.includes("components/booking/actions") || p.includes("/bookings/[status]") || p.includes("/booking/[uid]") || p.includes("/bookings/"))
359
+ return "booking_management";
360
+ if (p.includes("event-types") || p.includes("eventtypes") || p.includes("eventavailabilitytab") || p.includes("eventadvancedtab") || p.includes("eventlimits") || p.includes("eventrecurring"))
361
+ return "event_type_configuration";
362
+ if (p.includes("availability") || p.includes("/schedules/") || p.includes("/slots/")) {
363
+ return "availability";
645
364
  }
365
+ if (p.includes("oauth") || p.includes("nextauth") || p.includes("/auth/oauth") || p.includes("/api/auth/") || importSpecs.some((s) => s.includes("arctic") || s.includes("@auth/core")))
366
+ return "auth_oauth";
367
+ if (p.includes("/auth/") || p.includes("signup") || p.includes("login") || p.includes("forgot-password") || p.includes("reset-password") || p.includes("two-factor") || p.includes("verify-email") || importSpecs.some((s) => s.includes("next-auth") || s.includes("@clerk/")))
368
+ return "auth";
369
+ if ((p.includes("stripe") || p.includes("paypal") || p.includes("btcpay") || p.includes("alby") || p.includes("payment")) && (p.includes("webhook") || p.includes("hook")))
370
+ return "payments_webhooks";
371
+ if (p.includes("stripe") || p.includes("paypal") || p.includes("btcpay") || p.includes("alby") || p.includes("payment") || p.includes("billing") || p.includes("checkout") || p.includes("subscription") || importSpecs.some((s) => s.includes("stripe") || s.includes("@stripe/")))
372
+ return "payments";
373
+ if (p.includes("webhook"))
374
+ return "webhooks";
375
+ if (p.includes("app-store") || p.includes("appstore") || p.includes("/apps/") || p.includes("modules/apps"))
376
+ return "apps_marketplace";
377
+ if (p.includes("calendar") || p.includes("selected-calendars") || importSpecs.some((s) => s.includes("googleapis") || s.includes("@google-cloud/")))
378
+ return "calendar_integrations";
379
+ if (p.includes("video") || p.includes("calvideo") || p.includes("daily.co"))
380
+ return "video";
381
+ if (p.includes("onboarding") || p.includes("getting-started"))
382
+ return "onboarding";
383
+ if (p.includes("/settings/") || p.includes("/settings."))
384
+ return "settings";
385
+ if (p.includes("/admin/") || p.includes("/admin."))
386
+ return "admin";
387
+ if (p.includes("data-table") || p.includes("datatable") || p.includes("datasegment") || p.includes("segment"))
388
+ return "data_table";
389
+ if (p.includes("shell/navigation") || p.includes("navigationitem") || p.includes("/shell/") || p.includes("sidebar") || p.includes("topnav") || p.includes("mainnav"))
390
+ return "shell_navigation";
391
+ if (p.includes("form-builder") || p.includes("formbuilder") || p.includes("/forms/") || p.includes("routingforms"))
392
+ return "forms";
393
+ if (p.includes("embed"))
394
+ return "embed";
395
+ if (p.includes("notification") || p.includes("/email/") || p.includes("/emails/") || importSpecs.some((s) => s.includes("nodemailer") || s.includes("resend") || s.includes("@sendgrid/")))
396
+ return "notifications";
397
+ if (p.includes("middleware") && !p.includes("pages/api/") || p.includes("/router.") || p.includes("routerconfig"))
398
+ return "routing_infrastructure";
399
+ return "unknown";
646
400
  }
647
- var EXCLUDE_DIRS = /* @__PURE__ */ new Set([
648
- "node_modules",
649
- "dist",
650
- "build",
651
- ".next",
652
- "out",
653
- ".vibe-splainer",
654
- ".git",
655
- ".venv",
656
- "venv",
657
- "env",
658
- "__pycache__",
659
- ".idea",
660
- ".vscode",
661
- ".cache",
662
- "site-packages",
663
- "target",
664
- ".tox",
665
- ".mypy_cache",
666
- ".pytest_cache"
667
- ]);
668
- var EXCLUDE_FILE_PATTERNS = [/\.lock$/, /\.min\.[a-z]+$/, /\.d\.ts$/];
669
- var DEMOTE_SEGMENTS = /* @__PURE__ */ new Set([
670
- "docs",
671
- "doc",
672
- "examples",
673
- "example",
674
- "samples",
675
- "sample",
676
- "mockup",
677
- "mockups",
678
- "fixtures",
679
- "fixture",
680
- "__generated__",
681
- "__mocks__",
682
- "playwright",
683
- "e2e",
684
- "__tests__",
685
- "cypress",
686
- "storybook",
687
- "stories",
688
- ".storybook"
689
- ]);
690
- var VENDOR_SEGMENTS = /* @__PURE__ */ new Set([
691
- "node_modules",
692
- "vendor",
693
- "vendored",
694
- "site-packages",
695
- "third_party",
696
- "third-party"
697
- ]);
698
401
  var PILLAR_KEYWORDS = {
699
402
  "Auth": [
700
403
  "passport",
@@ -769,29 +472,9 @@ var PILLAR_KEYWORDS = {
769
472
  "sharp",
770
473
  "imagekit"
771
474
  ],
772
- "Config": [
773
- "dotenv",
774
- "convict",
775
- "env-var",
776
- "@t3-oss/env",
777
- "envalid"
778
- ],
779
- "Email": [
780
- "nodemailer",
781
- "resend",
782
- "@sendgrid/",
783
- "postmark",
784
- "@resend/",
785
- "mailgun"
786
- ],
787
- "Realtime": [
788
- "socket.io",
789
- "ws",
790
- "pusher",
791
- "ably",
792
- "@supabase/realtime",
793
- "socket.io-client"
794
- ]
475
+ "Config": ["dotenv", "convict", "env-var", "@t3-oss/env", "envalid"],
476
+ "Email": ["nodemailer", "resend", "@sendgrid/", "postmark", "@resend/", "mailgun"],
477
+ "Realtime": ["socket.io", "ws", "pusher", "ably", "@supabase/realtime", "socket.io-client"]
795
478
  };
796
479
  var PILLAR_PATH_PATTERNS = {
797
480
  "Auth": /(?:^|[\/\\])(?:auth|login|signup|register|session|oauth)(?:[\/\\]|$)/i,
@@ -817,264 +500,711 @@ var MEANINGLESS_SEGMENTS = /* @__PURE__ */ new Set([
817
500
  "pkg",
818
501
  "packages"
819
502
  ]);
820
- function matchPillarByImports(importSpecs) {
821
- const scores = /* @__PURE__ */ new Map();
822
- for (const spec of importSpecs) {
823
- for (const [pillar, keywords] of Object.entries(PILLAR_KEYWORDS)) {
824
- if (keywords.some((kw) => spec === kw || spec.startsWith(kw + "/"))) {
825
- scores.set(pillar, (scores.get(pillar) || 0) + 1);
826
- }
503
+ function matchPillarByImports(importSpecs) {
504
+ const scores = /* @__PURE__ */ new Map();
505
+ for (const spec of importSpecs) {
506
+ for (const [pillar, keywords] of Object.entries(PILLAR_KEYWORDS)) {
507
+ if (keywords.some((kw) => spec === kw || spec.startsWith(kw + "/"))) {
508
+ scores.set(pillar, (scores.get(pillar) || 0) + 1);
509
+ }
510
+ }
511
+ }
512
+ if (scores.size === 0)
513
+ return null;
514
+ return [...scores.entries()].sort((a, b) => b[1] - a[1])[0][0];
515
+ }
516
+ function matchPillarByPath(relPath) {
517
+ for (const [pillar, pattern] of Object.entries(PILLAR_PATH_PATTERNS)) {
518
+ if (pattern.test(relPath))
519
+ return pillar;
520
+ }
521
+ return null;
522
+ }
523
+ function extractImports(source, lang) {
524
+ const specs = [];
525
+ if (lang === "python") {
526
+ const re2 = /^[ \t]*(?:from[ \t]+([.\w]+)[ \t]+import|import[ \t]+([.\w][.\w ,]*))/gm;
527
+ let m2;
528
+ while ((m2 = re2.exec(source)) !== null) {
529
+ if (m2[1]) {
530
+ specs.push(m2[1]);
531
+ } else if (m2[2]) {
532
+ for (const part of m2[2].split(",")) {
533
+ const name = part.trim().split(/\s+as\s+/)[0].trim();
534
+ if (name)
535
+ specs.push(name);
536
+ }
537
+ }
538
+ }
539
+ return specs;
540
+ }
541
+ if (lang === "go") {
542
+ const re2 = /"([^"]+)"/g;
543
+ const importBlock = source.match(/import\s*\(([\s\S]*?)\)/g) || [];
544
+ for (const block of importBlock) {
545
+ let m3;
546
+ while ((m3 = re2.exec(block)) !== null)
547
+ specs.push(m3[1]);
548
+ }
549
+ const single = /import\s+(?:\w+\s+)?"([^"]+)"/g;
550
+ let m2;
551
+ while ((m2 = single.exec(source)) !== null)
552
+ specs.push(m2[1]);
553
+ return specs;
554
+ }
555
+ if (lang === "rust") {
556
+ const re2 = /\b(?:use|mod)\s+([\w:]+)/g;
557
+ let m2;
558
+ while ((m2 = re2.exec(source)) !== null)
559
+ specs.push(m2[1]);
560
+ return specs;
561
+ }
562
+ if (lang === "java") {
563
+ const re2 = /import\s+(?:static\s+)?([\w.]+)/g;
564
+ let m2;
565
+ while ((m2 = re2.exec(source)) !== null)
566
+ specs.push(m2[1]);
567
+ return specs;
568
+ }
569
+ const re = /(?:import|export)\s[^;]*?from\s*['"]([^'"]+)['"]|(?:import|require)\s*\(\s*['"]([^'"]+)['"]/g;
570
+ let m;
571
+ while ((m = re.exec(source)) !== null)
572
+ specs.push(m[1] || m[2]);
573
+ return specs;
574
+ }
575
+ async function detectStackAndEntrypoints(projectRoot, files) {
576
+ const stack = /* @__PURE__ */ new Set();
577
+ const entrypoints = /* @__PURE__ */ new Set();
578
+ const rel = (abs) => relative(projectRoot, abs);
579
+ const pkgPath = join4(projectRoot, "package.json");
580
+ if (existsSync2(pkgPath)) {
581
+ try {
582
+ const pkg = JSON.parse(await readFile4(pkgPath, "utf8"));
583
+ stack.add("Node.js");
584
+ const deps = { ...pkg.dependencies || {}, ...pkg.devDependencies || {} };
585
+ for (const known of ["react", "next", "vue", "svelte", "express", "fastify", "typescript", "vite"]) {
586
+ if (deps[known])
587
+ stack.add(known === "next" ? "Next.js" : known[0].toUpperCase() + known.slice(1));
588
+ }
589
+ const addEntry = (p) => {
590
+ if (!p)
591
+ return;
592
+ const abs = join4(projectRoot, p);
593
+ const r = relative(projectRoot, abs);
594
+ if (files.includes(abs))
595
+ entrypoints.add(r);
596
+ };
597
+ addEntry(pkg.main);
598
+ if (typeof pkg.bin === "string")
599
+ addEntry(pkg.bin);
600
+ else if (pkg.bin)
601
+ for (const v of Object.values(pkg.bin))
602
+ addEntry(v);
603
+ } catch {
604
+ }
605
+ }
606
+ const pyproject = join4(projectRoot, "pyproject.toml");
607
+ const setupPy = join4(projectRoot, "setup.py");
608
+ const requirements = join4(projectRoot, "requirements.txt");
609
+ if (existsSync2(pyproject) || existsSync2(setupPy) || existsSync2(requirements)) {
610
+ stack.add("Python");
611
+ let reqText = "";
612
+ for (const f of [pyproject, requirements]) {
613
+ if (existsSync2(f)) {
614
+ try {
615
+ reqText += await readFile4(f, "utf8");
616
+ } catch {
617
+ }
618
+ }
619
+ }
620
+ for (const known of ["pygame", "PySide6", "PyQt5", "PyQt6", "flask", "django", "fastapi", "numpy", "pandas", "torch", "tensorflow"]) {
621
+ if (new RegExp(known, "i").test(reqText))
622
+ stack.add(known);
623
+ }
624
+ }
625
+ if (existsSync2(join4(projectRoot, "go.mod")))
626
+ stack.add("Go");
627
+ if (existsSync2(join4(projectRoot, "Cargo.toml")))
628
+ stack.add("Rust");
629
+ if (existsSync2(join4(projectRoot, "pom.xml")) || existsSync2(join4(projectRoot, "build.gradle")))
630
+ stack.add("Java");
631
+ for (const abs of files) {
632
+ const r = rel(abs);
633
+ const b = basename(r);
634
+ if (b === "main.py" || b === "__main__.py")
635
+ entrypoints.add(r);
636
+ if (/^index\.(ts|tsx|js|jsx|mjs|cjs)$/.test(b) && dirname(r).split(sep).length <= 2)
637
+ entrypoints.add(r);
638
+ if (b === "main.go" && r.includes("cmd" + sep))
639
+ entrypoints.add(r);
640
+ if (b === "main.go" && !r.includes(sep))
641
+ entrypoints.add(r);
642
+ if (b === "main.rs" || b === "lib.rs")
643
+ entrypoints.add(r);
644
+ }
645
+ if (stack.has("Next.js")) {
646
+ const appRouterNames = /* @__PURE__ */ new Set(["page", "layout", "route", "loading", "error", "not-found", "template", "default"]);
647
+ for (const abs of files) {
648
+ const r = rel(abs);
649
+ const stem = basename(r, extname(r));
650
+ if (/(?:^|[/\\])app[/\\]/.test(r) && appRouterNames.has(stem))
651
+ entrypoints.add(r);
652
+ if (/(?:^|[/\\])pages[/\\]/.test(r) && !stem.startsWith("_"))
653
+ entrypoints.add(r);
654
+ }
655
+ }
656
+ return { stack: [...stack], entrypoints };
657
+ }
658
+ var FUNCTION_TYPES = /* @__PURE__ */ new Set([
659
+ "function_declaration",
660
+ "function",
661
+ "function_expression",
662
+ "arrow_function",
663
+ "method_definition",
664
+ "function_definition",
665
+ "method_declaration",
666
+ "func_literal",
667
+ "function_item",
668
+ "closure_expression",
669
+ "constructor_declaration",
670
+ "generator_function_declaration",
671
+ "generator_function"
672
+ ]);
673
+ var NESTING_TYPES = /* @__PURE__ */ new Set([
674
+ "function_declaration",
675
+ "function",
676
+ "arrow_function",
677
+ "function_expression",
678
+ "method_definition",
679
+ "function_definition",
680
+ "method_declaration",
681
+ "function_item",
682
+ "class_declaration",
683
+ "class",
684
+ "class_definition",
685
+ "class_item",
686
+ "if_statement",
687
+ "if_expression",
688
+ "for_statement",
689
+ "for_in_statement",
690
+ "for_expression",
691
+ "enhanced_for_statement",
692
+ "while_statement",
693
+ "while_expression",
694
+ "do_statement",
695
+ "switch_statement",
696
+ "match_expression",
697
+ "match_arm",
698
+ "try_statement",
699
+ "catch_clause",
700
+ "except_clause",
701
+ "loop_expression",
702
+ "block"
703
+ ]);
704
+ var DECISION_TYPES = /* @__PURE__ */ new Set([
705
+ "if_statement",
706
+ "if_expression",
707
+ "elif_clause",
708
+ "for_statement",
709
+ "for_in_statement",
710
+ "for_expression",
711
+ "enhanced_for_statement",
712
+ "while_statement",
713
+ "while_expression",
714
+ "do_statement",
715
+ "loop_expression",
716
+ "case",
717
+ "switch_case",
718
+ "case_clause",
719
+ "match_arm",
720
+ "catch_clause",
721
+ "except_clause",
722
+ "communication_case",
723
+ "conditional_expression",
724
+ "ternary_expression"
725
+ ]);
726
+ var CATCH_TYPES = /* @__PURE__ */ new Set(["catch_clause", "except_clause"]);
727
+ var LONG_FN_LOC = 60;
728
+ var DEEP_NESTING = 5;
729
+ var GOD_FILE_LOC = 400;
730
+ var GOD_FILE_EXPORTS = 8;
731
+ function nodeLOC(node) {
732
+ return node.endPosition.row - node.startPosition.row + 1;
733
+ }
734
+ function countDecisions(node) {
735
+ let count = 0;
736
+ const walk = (n) => {
737
+ if (DECISION_TYPES.has(n.type))
738
+ count++;
739
+ if (n.type === "binary_expression") {
740
+ const op = n.children.find((c) => c.type === "&&" || c.type === "||");
741
+ if (op)
742
+ count++;
827
743
  }
744
+ if (n.type === "boolean_operator")
745
+ count++;
746
+ for (const c of n.children)
747
+ walk(c);
748
+ };
749
+ walk(node);
750
+ return count;
751
+ }
752
+ function computeNesting(node, depth) {
753
+ let maxDepth = depth;
754
+ for (const child of node.children) {
755
+ const nextDepth = NESTING_TYPES.has(child.type) ? depth + 1 : depth;
756
+ maxDepth = Math.max(maxDepth, computeNesting(child, nextDepth));
828
757
  }
829
- if (scores.size === 0)
830
- return null;
831
- return [...scores.entries()].sort((a, b) => b[1] - a[1])[0][0];
758
+ return maxDepth;
832
759
  }
833
- function matchPillarByPath(relPath) {
834
- for (const [pillar, pattern] of Object.entries(PILLAR_PATH_PATTERNS)) {
835
- if (pattern.test(relPath))
836
- return pillar;
760
+ function firstLine(s) {
761
+ return s.split("\n")[0];
762
+ }
763
+ function stripLeadingComments(snippet) {
764
+ const lines = snippet.split("\n");
765
+ let i = 0;
766
+ let inBlock = false;
767
+ while (i < lines.length) {
768
+ const t = lines[i].trim();
769
+ if (inBlock) {
770
+ if (t.includes("*/"))
771
+ inBlock = false;
772
+ i++;
773
+ continue;
774
+ }
775
+ if (t === "") {
776
+ i++;
777
+ continue;
778
+ }
779
+ if (t.startsWith("//") || t.startsWith("#")) {
780
+ i++;
781
+ continue;
782
+ }
783
+ if (t.startsWith("/*")) {
784
+ inBlock = !t.includes("*/");
785
+ i++;
786
+ continue;
787
+ }
788
+ if (t.startsWith('"""') || t.startsWith("'''")) {
789
+ const q = t.slice(0, 3);
790
+ if (t.length > 3 && t.endsWith(q)) {
791
+ i++;
792
+ continue;
793
+ }
794
+ i++;
795
+ while (i < lines.length && !lines[i].includes(q))
796
+ i++;
797
+ i++;
798
+ continue;
799
+ }
800
+ break;
837
801
  }
838
- return null;
802
+ return lines.slice(i).join("\n");
839
803
  }
840
- function inferFrameworkRole(relPath) {
841
- const p = relPath.replace(/\\/g, "/");
842
- if (/\.test\.|\.spec\./.test(p))
843
- return "test";
844
- if (/\.generated\.|__generated__|\.prisma\//.test(p))
845
- return "generated";
846
- if (/(?:^|\/)app\/.*\/page\.tsx?$/.test(p))
847
- return "app_route_page";
848
- if (/(?:^|\/)app\/.*\/layout\.tsx?$/.test(p))
849
- return "app_route_layout";
850
- if (/(?:^|\/)app\/.*\/route\.tsx?$/.test(p))
851
- return "app_route_handler";
852
- if (/(?:^|\/)app\/.*\/loading\.tsx?$/.test(p))
853
- return "app_loading_boundary";
854
- if (/(?:^|\/)app\/.*\/error\.tsx?$/.test(p))
855
- return "app_error_boundary";
856
- if (/(?:^|\/)pages\/api\/trpc\//.test(p))
857
- return "trpc_api_route";
858
- if (/(?:^|\/)pages\/api\//.test(p))
859
- return "pages_api_route";
860
- if (/(?:^|\/)pages\//.test(p))
861
- return "pages_route";
862
- if (/\/hooks\/|\/use[A-Z][^/]*\.(ts|tsx)$/.test(p))
863
- return "hook";
864
- if (/\/stores?\/|[Ss]tore\.(ts|tsx)$/.test(p))
865
- return "store";
866
- if (/[Pp]rovider\.(tsx?|jsx?)$|\/providers?\//.test(p))
867
- return "provider";
868
- if (/\.types\.ts$|\/types\.ts$|\/types\/[^/]+\.ts$/.test(p))
869
- return "type_definition";
870
- if (/\.(tsx|jsx)$/.test(p))
871
- return "component";
872
- if (/\.(ts|js|mjs|cjs)$/.test(p))
873
- return "utility";
874
- return "unknown";
804
+ var TODO_RE = /\b(TODO|FIXME|HACK|XXX|KLUDGE)\b|@deprecated/;
805
+ var SUPPRESS_RE = /@ts-ignore|@ts-nocheck|eslint-disable|:\s*any\b|#\s*type:\s*ignore|type:\s*ignore|#\s*nosec/;
806
+ function collectFunctionNodes(root) {
807
+ const out = [];
808
+ const walk = (n) => {
809
+ if (FUNCTION_TYPES.has(n.type))
810
+ out.push(n);
811
+ for (const c of n.children)
812
+ walk(c);
813
+ };
814
+ walk(root);
815
+ return out;
875
816
  }
876
- function inferProductDomain(relPath, importSpecs) {
877
- const p = relPath.toLowerCase().replace(/\\/g, "/");
878
- if (/\.test\.|\.spec\.|__tests__|\/e2e\/|\/playwright\/|\/cypress\//.test(p)) {
879
- return "test_infrastructure";
817
+ function catchIsSwallowed(node) {
818
+ const bodyText = node.text;
819
+ const inner = bodyText.replace(/^[^{:]*[{:]/, "");
820
+ const meaningful = inner.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("//") && !l.startsWith("#") && l !== "}" && l !== "pass");
821
+ if (meaningful.length === 0)
822
+ return true;
823
+ return meaningful.every((l) => /^(console\.(log|error|warn|info)|print|println!?|System\.out|logger?\.)/.test(l) || l === "pass" || l === "{" || l === "});" || l === ")" || l === "`");
824
+ }
825
+ function collectExports(root, lang) {
826
+ const out = [];
827
+ const seen = /* @__PURE__ */ new Set();
828
+ const push = (name, node) => {
829
+ if (!name || seen.has(name))
830
+ return;
831
+ seen.add(name);
832
+ out.push({ name, text: firstLine(node.text).trim().slice(0, 200) });
833
+ };
834
+ if (lang === "python") {
835
+ for (const c of root.children) {
836
+ if (c.type === "function_definition" || c.type === "class_definition") {
837
+ const name = c.childForFieldName("name")?.text;
838
+ if (name && !name.startsWith("_"))
839
+ push(name, c);
840
+ }
841
+ }
842
+ return out;
880
843
  }
881
- if (/\.generated\.|__generated__|\.prisma\//.test(p)) {
882
- return "generated_noise";
844
+ if (lang === "go") {
845
+ const walk2 = (n) => {
846
+ if (n.type === "function_declaration" || n.type === "method_declaration" || n.type === "type_declaration") {
847
+ const name = n.childForFieldName("name")?.text;
848
+ if (name && /^[A-Z]/.test(name))
849
+ push(name, n);
850
+ }
851
+ for (const c of n.children)
852
+ walk2(c);
853
+ };
854
+ walk2(root);
855
+ return out;
856
+ }
857
+ if (lang === "rust") {
858
+ const walk2 = (n) => {
859
+ if (/_item$/.test(n.type) && n.children.some((c) => c.type === "visibility_modifier")) {
860
+ const name = n.childForFieldName("name")?.text;
861
+ push(name, n);
862
+ }
863
+ for (const c of n.children)
864
+ walk2(c);
865
+ };
866
+ walk2(root);
867
+ return out;
868
+ }
869
+ if (lang === "java") {
870
+ const walk2 = (n) => {
871
+ if ((n.type === "method_declaration" || n.type === "class_declaration") && /\bpublic\b/.test(firstLine(n.text))) {
872
+ const name = n.childForFieldName("name")?.text;
873
+ push(name, n);
874
+ }
875
+ for (const c of n.children)
876
+ walk2(c);
877
+ };
878
+ walk2(root);
879
+ return out;
883
880
  }
884
- if (p.includes("booking-audit") || p.includes("bookingaudit"))
885
- return "booking_audit";
886
- if (p.includes("bookeventform") || p.includes("availabletimes") || p.includes("availabletimeslots") || p.includes("usebookings") || p.includes("/pages/api/book/") || p.includes("/api/book/") || p.includes("/booking-successful/") || p.includes("/reschedule/") || p.includes("booking-page-wrapper") || p.includes("/book/") && !p.includes("booking-audit"))
887
- return "booking_creation";
888
- if (p.includes("modules/bookings") || p.includes("components/booking/actions") || p.includes("/bookings/[status]") || p.includes("/booking/[uid]") || p.includes("/bookings/"))
889
- return "booking_management";
890
- if (p.includes("event-types") || p.includes("eventtypes") || p.includes("eventavailabilitytab") || p.includes("eventadvancedtab") || p.includes("eventlimits") || p.includes("eventrecurring"))
891
- return "event_type_configuration";
892
- if (p.includes("availability") || p.includes("/schedules/") || p.includes("/slots/"))
893
- return "availability";
894
- if (p.includes("oauth") || p.includes("nextauth") || p.includes("/auth/oauth") || p.includes("/api/auth/") || importSpecs.some((s) => s.includes("arctic") || s.includes("@auth/core")))
895
- return "auth_oauth";
896
- if (p.includes("/auth/") || p.includes("signup") || p.includes("login") || p.includes("forgot-password") || p.includes("reset-password") || p.includes("two-factor") || p.includes("verify-email") || importSpecs.some((s) => s.includes("next-auth") || s.includes("@clerk/")))
897
- return "auth";
898
- if ((p.includes("stripe") || p.includes("paypal") || p.includes("btcpay") || p.includes("alby") || p.includes("payment")) && (p.includes("webhook") || p.includes("hook")))
899
- return "payments_webhooks";
900
- if (p.includes("stripe") || p.includes("paypal") || p.includes("btcpay") || p.includes("alby") || p.includes("payment") || p.includes("billing") || p.includes("checkout") || p.includes("subscription") || importSpecs.some((s) => s.includes("stripe") || s.includes("@stripe/")))
901
- return "payments";
902
- if (p.includes("webhook"))
903
- return "webhooks";
904
- if (p.includes("app-store") || p.includes("appstore") || p.includes("/apps/") || p.includes("modules/apps"))
905
- return "apps_marketplace";
906
- if (p.includes("calendar") || p.includes("selected-calendars") || importSpecs.some((s) => s.includes("googleapis") || s.includes("@google-cloud/")))
907
- return "calendar_integrations";
908
- if (p.includes("video") || p.includes("calvideo") || p.includes("daily.co"))
909
- return "video";
910
- if (p.includes("onboarding") || p.includes("getting-started"))
911
- return "onboarding";
912
- if (p.includes("/settings/") || p.includes("/settings."))
913
- return "settings";
914
- if (p.includes("/admin/") || p.includes("/admin."))
915
- return "admin";
916
- if (p.includes("data-table") || p.includes("datatable") || p.includes("datasegment") || p.includes("segment"))
917
- return "data_table";
918
- if (p.includes("shell/navigation") || p.includes("navigationitem") || p.includes("/shell/") || p.includes("sidebar") || p.includes("topnav") || p.includes("mainnav"))
919
- return "shell_navigation";
920
- if (p.includes("form-builder") || p.includes("formbuilder") || p.includes("/forms/") || p.includes("routingforms"))
921
- return "forms";
922
- if (p.includes("embed"))
923
- return "embed";
924
- if (p.includes("notification") || p.includes("/email/") || p.includes("/emails/") || importSpecs.some((s) => s.includes("nodemailer") || s.includes("resend") || s.includes("@sendgrid/")))
925
- return "notifications";
926
- if (p.includes("middleware") && !p.includes("pages/api/") || p.includes("/router.") || p.includes("routerconfig"))
927
- return "routing_infrastructure";
928
- return "unknown";
881
+ const walk = (n) => {
882
+ if (n.type === "export_statement") {
883
+ const decl = n.childForFieldName("declaration");
884
+ if (decl) {
885
+ const name = decl.childForFieldName("name")?.text;
886
+ if (name)
887
+ push(name, decl);
888
+ for (const c of decl.namedChildren) {
889
+ const dn = c.childForFieldName("name")?.text;
890
+ if (dn)
891
+ push(dn, c);
892
+ }
893
+ }
894
+ for (const spec of n.descendantsOfType("export_specifier")) {
895
+ push(spec.childForFieldName("name")?.text, spec);
896
+ }
897
+ if (n.text.includes("export default"))
898
+ push("default", n);
899
+ }
900
+ for (const c of n.children)
901
+ walk(c);
902
+ };
903
+ walk(root);
904
+ return out;
929
905
  }
930
- function inferSideEffectProfile(source, importSpecs) {
931
- const effects = /* @__PURE__ */ new Set();
932
- if (/router\.(push|replace|back)\(|redirect\(|notFound\(|permanentRedirect\(/.test(source)) {
933
- effects.add("redirect");
906
+ function analyzeAst(source, lang, tree) {
907
+ const root = tree.rootNode;
908
+ const lines = source.split("\n");
909
+ const loc = lines.length;
910
+ const cyclomatic = countDecisions(root);
911
+ const maxNesting = computeNesting(root, 0);
912
+ const smells = [];
913
+ let todos = 0, suppressions = 0;
914
+ for (let i = 0; i < lines.length; i++) {
915
+ const line = lines[i];
916
+ if (TODO_RE.test(line)) {
917
+ todos++;
918
+ smells.push({ kind: "todo", line: i + 1, endLine: i + 1, text: line.trim().slice(0, 200), severity: 2, note: "unfinished / known-bad marker" });
919
+ }
920
+ if (SUPPRESS_RE.test(line)) {
921
+ suppressions++;
922
+ smells.push({ kind: "suppression", line: i + 1, endLine: i + 1, text: line.trim().slice(0, 200), severity: 3, note: "type/lint safety suppressed" });
923
+ }
934
924
  }
935
- if (/["']use server["']/.test(source))
936
- effects.add("server_action");
937
- if (/useMutation\b|\.mutate\b|\.mutateAsync\b/.test(source))
938
- effects.add("trpc_mutation");
939
- if (/sdkActionManager\.fire|telemetry\.|posthog\.|mixpanel\.|amplitude\.|ga\(/.test(source) || importSpecs.some((s) => /analytics|telemetry|posthog|mixpanel|amplitude/.test(s)))
940
- effects.add("analytics_event");
941
- if (/prisma\s*[.?]\s*\w+\s*[.?]\s*(create|update|upsert|delete|deleteMany|updateMany|createMany|transaction|executeRaw|queryRaw)\b/.test(source)) {
942
- effects.add("database_write");
925
+ let magicNumbers = 0;
926
+ const magicWalk = (n) => {
927
+ if (n.type === "number" || n.type === "integer_literal" || n.type === "float_literal" || n.type === "int_literal") {
928
+ const v = n.text.replace(/_/g, "");
929
+ if (!["0", "1", "2", "-1", "100", "1000"].includes(v) && /^\d{2,}$/.test(v))
930
+ magicNumbers++;
931
+ }
932
+ for (const c of n.children)
933
+ magicWalk(c);
934
+ };
935
+ magicWalk(root);
936
+ if (magicNumbers > 6) {
937
+ smells.push({ kind: "magic-number", line: 1, endLine: 1, text: `${magicNumbers} unexplained numeric literals`, severity: 2, note: "many magic numbers \u2014 extract named constants" });
943
938
  }
944
- if (/prisma\s*[.?]\s*\w+\s*[.?]\s*(findMany|findUnique|findFirst|findFirstOrThrow|findUniqueOrThrow|count|aggregate|groupBy)\b/.test(source)) {
945
- effects.add("database_read");
939
+ let swallowedCatches = 0;
940
+ const catchWalk = (n) => {
941
+ if (CATCH_TYPES.has(n.type) && catchIsSwallowed(n)) {
942
+ swallowedCatches++;
943
+ smells.push({
944
+ kind: "swallowed-catch",
945
+ line: n.startPosition.row + 1,
946
+ endLine: n.endPosition.row + 1,
947
+ text: firstLine(n.text).trim().slice(0, 200),
948
+ severity: 4,
949
+ note: "catch block swallows error silently"
950
+ });
951
+ }
952
+ for (const c of n.children)
953
+ catchWalk(c);
954
+ };
955
+ catchWalk(root);
956
+ const fnNodes = collectFunctionNodes(root);
957
+ let longFunctions = 0;
958
+ const scored = [];
959
+ for (const fn of fnNodes) {
960
+ const bodyLOC = nodeLOC(fn);
961
+ const decisions = countDecisions(fn);
962
+ scored.push({ node: fn, decisions, bodyLOC, score: decisions + bodyLOC });
963
+ if (bodyLOC > LONG_FN_LOC) {
964
+ longFunctions++;
965
+ smells.push({
966
+ kind: "long-function",
967
+ line: fn.startPosition.row + 1,
968
+ endLine: fn.endPosition.row + 1,
969
+ text: firstLine(fn.text).trim().slice(0, 200),
970
+ severity: 3,
971
+ note: `function body is ${bodyLOC} lines`
972
+ });
973
+ }
946
974
  }
947
- if (/createBooking|handleNewBooking|cancelBooking|rescheduleBooking|handleBooking|createRecurring/.test(source)) {
948
- effects.add("booking_mutation");
975
+ if (maxNesting > DEEP_NESTING) {
976
+ smells.push({ kind: "deep-nesting", line: 1, endLine: 1, text: `nesting depth ${maxNesting}`, severity: 3, note: `control flow nested ${maxNesting} levels deep` });
949
977
  }
950
- if (importSpecs.some((s) => /stripe|paypal|btcpay|alby/.test(s.toLowerCase())) || /stripe\.|paymentIntent|createPaymentIntent|confirmPayment|createCharge/.test(source))
951
- effects.add("payment_mutation");
952
- if (/signIn\b|signOut\b|createSession|destroySession|issueToken|refreshToken|getToken/.test(source)) {
953
- effects.add("auth_token_mutation");
978
+ const exported = collectExports(root, lang);
979
+ const publicSurface = exported.length;
980
+ const signature = exported.map((e) => e.text).join("\n").slice(0, 4e3);
981
+ if (loc > GOD_FILE_LOC && publicSurface > GOD_FILE_EXPORTS) {
982
+ smells.push({
983
+ kind: "god-file",
984
+ line: 1,
985
+ endLine: 1,
986
+ text: `${loc} LOC, ${publicSurface} exports`,
987
+ severity: 4,
988
+ note: `god-file: ${loc} lines exporting ${publicSurface} symbols`
989
+ });
954
990
  }
955
- if (/triggerWebhook|sendWebhook|webhook\.send\b/.test(source))
956
- effects.add("webhook_delivery");
957
- if (/webhookSecret|stripe\.webhooks\.constructEvent|validateWebhook|verifyWebhook/.test(source)) {
958
- effects.add("webhook_ingress");
991
+ scored.sort((a, b) => b.score - a.score);
992
+ const hotSpans = scored.slice(0, 3).filter((s) => s.bodyLOC >= 4).map((s) => {
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);
995
+ return {
996
+ startLine: s.node.startPosition.row + 1,
997
+ endLine: s.node.endPosition.row + 1,
998
+ rawExcerpt,
999
+ snippet,
1000
+ reason: `high complexity: ${s.decisions} decision branches across ${s.bodyLOC} lines`
1001
+ };
1002
+ });
1003
+ return {
1004
+ language: lang,
1005
+ loc,
1006
+ cyclomatic,
1007
+ maxNesting,
1008
+ publicSurface,
1009
+ exportedNames: exported.map((e) => e.name),
1010
+ signature,
1011
+ longFunctions,
1012
+ magicNumbers,
1013
+ swallowedCatches,
1014
+ smells,
1015
+ hotSpans
1016
+ };
1017
+ }
1018
+ var SMELL_WEIGHT = {
1019
+ "todo": 3,
1020
+ "suppression": 5,
1021
+ "swallowed-catch": 10,
1022
+ "deep-nesting": 6,
1023
+ "long-function": 5,
1024
+ "magic-number": 3,
1025
+ "god-file": 14
1026
+ };
1027
+ function computeHeat(smells) {
1028
+ let sum = 0;
1029
+ for (const s of smells)
1030
+ sum += s.severity * SMELL_WEIGHT[s.kind];
1031
+ return Math.min(100, sum);
1032
+ }
1033
+ async function runInventory(projectRoot) {
1034
+ await initParser();
1035
+ const abs = [];
1036
+ await collectFiles(projectRoot, projectRoot, abs);
1037
+ const fileSet = new Set(abs.map((f) => relative(projectRoot, f)));
1038
+ const basenameIndex = /* @__PURE__ */ new Map();
1039
+ for (const rel of fileSet) {
1040
+ const b = basename(rel).slice(0, basename(rel).length - extname(rel).length);
1041
+ if (!basenameIndex.has(b))
1042
+ basenameIndex.set(b, []);
1043
+ basenameIndex.get(b).push(rel);
959
1044
  }
960
- if (/sendEmail|sendMail\b|mailer\./.test(source) || importSpecs.some((s) => /nodemailer|resend|sendgrid|postmark|mailgun/.test(s)))
961
- effects.add("email_send");
962
- if (/createCalendarEvent|updateCalendarEvent|deleteCalendarEvent|calendar\.events\.(insert|update|delete|patch)/.test(source)) {
963
- effects.add("calendar_mutation");
1045
+ const { stack, entrypoints } = await detectStackAndEntrypoints(projectRoot, abs);
1046
+ const work = [];
1047
+ for (const file of abs) {
1048
+ const rel = relative(projectRoot, file);
1049
+ const ext = extname(file);
1050
+ const lang = EXT_LANG[ext];
1051
+ if (!lang)
1052
+ continue;
1053
+ let source;
1054
+ try {
1055
+ source = await readFile4(file, "utf8");
1056
+ } catch {
1057
+ continue;
1058
+ }
1059
+ if (/if\s+__name__\s*==\s*['"]__main__['"]/.test(source) || /^#![^\n]*\b(node|python\d?)\b/.test(source)) {
1060
+ entrypoints.add(rel);
1061
+ }
1062
+ const tree = await parseAs(lang, source);
1063
+ if (!tree)
1064
+ continue;
1065
+ const ast = analyzeAst(source, lang, tree);
1066
+ const importSpecs = extractImports(source, lang);
1067
+ const frameworkRole = inferFrameworkRole(rel);
1068
+ const productDomain = inferProductDomain(rel, importSpecs);
1069
+ work.push({
1070
+ abs: file,
1071
+ rel,
1072
+ lang,
1073
+ source,
1074
+ ast,
1075
+ importSpecs,
1076
+ pathDemote: pathDemoteReason(rel),
1077
+ frameworkRole,
1078
+ productDomain
1079
+ });
964
1080
  }
965
- if (/revalidatePath\b|revalidateTag\b/.test(source))
966
- effects.add("cache_revalidation");
967
- if (/localStorage\.|sessionStorage\./.test(source))
968
- effects.add("local_storage");
969
- if (/indexedDB\b|new Dexie|idb\./.test(source))
970
- effects.add("indexed_db");
971
- if (/\bfetch\s*\(|axios\.(get|post|put|patch|delete)\b/.test(source)) {
972
- effects.add("external_api_call");
1081
+ const dir = join4(projectRoot, ".vibe-splainer");
1082
+ await mkdir3(dir, { recursive: true });
1083
+ const stage01 = {
1084
+ files: work.map((w) => ({
1085
+ absPath: w.abs,
1086
+ relPath: w.rel,
1087
+ language: w.lang,
1088
+ demoteReason: w.pathDemote
1089
+ })),
1090
+ totalCount: work.length,
1091
+ realSourceCount: work.filter((w) => !w.pathDemote).length
1092
+ };
1093
+ await writeFile4(join4(dir, "stage-01-inventory.json"), JSON.stringify(stage01, null, 2), "utf8");
1094
+ const stage02 = Object.fromEntries(work.map((w) => [w.rel, w.frameworkRole]));
1095
+ await writeFile4(join4(dir, "stage-02-framework-roles.json"), JSON.stringify(stage02, null, 2), "utf8");
1096
+ const stage03 = Object.fromEntries(work.map((w) => [w.rel, w.productDomain]));
1097
+ await writeFile4(join4(dir, "stage-03-domains.json"), JSON.stringify(stage03, null, 2), "utf8");
1098
+ return { projectRoot, work, stack, entrypoints, fileSet, basenameIndex };
1099
+ }
1100
+
1101
+ // ../brain/dist/pipeline/resolution.js
1102
+ import { join as join5, dirname as dirname2, relative as relative2, extname as extname2, sep as sep2 } from "path";
1103
+ import { readFile as readFile5, writeFile as writeFile5, mkdir as mkdir4 } from "fs/promises";
1104
+ import { existsSync as existsSync3 } from "fs";
1105
+ function parseJsonLenient(text) {
1106
+ const stripped = text.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
1107
+ try {
1108
+ return JSON.parse(stripped);
1109
+ } catch {
1110
+ return null;
973
1111
  }
974
- if (effects.size === 0)
975
- effects.add("none_detected");
976
- return [...effects];
977
1112
  }
978
- async function collectFiles(dir, projectRoot, acc) {
979
- let entries;
1113
+ async function extractTsConfigPaths(tsconfigPath, projectRoot, depth = 0) {
1114
+ if (depth > 3 || !existsSync3(tsconfigPath))
1115
+ return {};
1116
+ let raw;
980
1117
  try {
981
- entries = await readdir(dir, { withFileTypes: true });
1118
+ raw = await readFile5(tsconfigPath, "utf8");
982
1119
  } catch {
983
- return;
1120
+ return {};
1121
+ }
1122
+ const parsed = parseJsonLenient(raw);
1123
+ if (!parsed)
1124
+ return {};
1125
+ const result = {};
1126
+ if (typeof parsed.extends === "string") {
1127
+ const baseFile = join5(dirname2(tsconfigPath), parsed.extends);
1128
+ const base = await extractTsConfigPaths(baseFile, projectRoot, depth + 1);
1129
+ Object.assign(result, base);
1130
+ }
1131
+ const opts = parsed.compilerOptions || {};
1132
+ const baseUrl = typeof opts.baseUrl === "string" ? join5(dirname2(tsconfigPath), opts.baseUrl) : dirname2(tsconfigPath);
1133
+ const paths = opts.paths || {};
1134
+ for (const [alias, targets] of Object.entries(paths)) {
1135
+ if (!Array.isArray(targets) || targets.length === 0)
1136
+ continue;
1137
+ const first = targets[0].replace(/\/\*$/, "");
1138
+ const resolved = relative2(projectRoot, join5(baseUrl, first));
1139
+ const key = alias.replace(/\/\*$/, "");
1140
+ result[key] = resolved;
984
1141
  }
985
- for (const entry of entries) {
986
- if (entry.name.startsWith(".") && entry.name !== ".") {
987
- if (entry.isDirectory())
988
- continue;
989
- }
990
- if (EXCLUDE_DIRS.has(entry.name))
1142
+ return result;
1143
+ }
1144
+ async function discoverWorkspacePackages(projectRoot) {
1145
+ const packages = {};
1146
+ const pkgPath = join5(projectRoot, "package.json");
1147
+ if (!existsSync3(pkgPath))
1148
+ return packages;
1149
+ let rootPkg;
1150
+ try {
1151
+ rootPkg = JSON.parse(await readFile5(pkgPath, "utf8"));
1152
+ } catch {
1153
+ return packages;
1154
+ }
1155
+ const workspaces = rootPkg.workspaces;
1156
+ const globs = Array.isArray(workspaces) ? workspaces : Array.isArray(workspaces?.packages) ? workspaces.packages : [];
1157
+ for (const glob of globs) {
1158
+ const prefix = glob.replace(/\/\*$/, "");
1159
+ const absPrefix = join5(projectRoot, prefix);
1160
+ if (!existsSync3(absPrefix))
991
1161
  continue;
992
- const fullPath = join4(dir, entry.name);
993
- if (entry.isDirectory()) {
994
- await collectFiles(fullPath, projectRoot, acc);
995
- } else if (entry.isFile()) {
996
- const ext = extname(entry.name);
997
- if (!SUPPORTED_EXTENSIONS.has(ext))
1162
+ const { readdir: readdir2 } = await import("fs/promises");
1163
+ let entries = [];
1164
+ try {
1165
+ const dirents = await readdir2(absPrefix, { withFileTypes: true });
1166
+ entries = dirents.filter((d) => d.isDirectory()).map((d) => d.name);
1167
+ } catch {
1168
+ continue;
1169
+ }
1170
+ for (const entry of entries) {
1171
+ const wsPkgPath = join5(absPrefix, entry, "package.json");
1172
+ if (!existsSync3(wsPkgPath))
998
1173
  continue;
999
- if (EXCLUDE_FILE_PATTERNS.some((p) => p.test(entry.name)))
1174
+ try {
1175
+ const wsPkg = JSON.parse(await readFile5(wsPkgPath, "utf8"));
1176
+ if (typeof wsPkg.name === "string") {
1177
+ packages[wsPkg.name] = relative2(projectRoot, join5(absPrefix, entry));
1178
+ }
1179
+ } catch {
1000
1180
  continue;
1001
- acc.push(fullPath);
1181
+ }
1002
1182
  }
1003
1183
  }
1184
+ return packages;
1004
1185
  }
1005
- function pathDemoteReason(relPath) {
1006
- const segs = relPath.split(sep);
1007
- for (const s of segs) {
1008
- if (VENDOR_SEGMENTS.has(s))
1009
- return `vendored code (${s})`;
1010
- if (s.endsWith(".venv") || s === "venv" || s === "env")
1011
- return "virtual environment";
1012
- }
1013
- for (const s of segs) {
1014
- if (DEMOTE_SEGMENTS.has(s.toLowerCase()))
1015
- return `non-application path segment (${s})`;
1016
- }
1017
- const base = basename(relPath);
1018
- if (/\.min\./.test(base))
1019
- return "minified bundle";
1020
- if (/\.generated\./.test(base))
1021
- return "generated file";
1022
- return null;
1023
- }
1024
- function extractImports(source, lang) {
1025
- const specs = [];
1026
- if (lang === "python") {
1027
- const re2 = /^[ \t]*(?:from[ \t]+([.\w]+)[ \t]+import|import[ \t]+([.\w][.\w ,]*))/gm;
1028
- let m2;
1029
- while ((m2 = re2.exec(source)) !== null) {
1030
- if (m2[1]) {
1031
- specs.push(m2[1]);
1032
- } else if (m2[2]) {
1033
- for (const part of m2[2].split(",")) {
1034
- const name = part.trim().split(/\s+as\s+/)[0].trim();
1035
- if (name)
1036
- specs.push(name);
1037
- }
1186
+ async function discoverAppTsConfigPaths(projectRoot) {
1187
+ const result = {};
1188
+ const scanDirs = ["apps", "packages"];
1189
+ for (const scanDir of scanDirs) {
1190
+ const absDir = join5(projectRoot, scanDir);
1191
+ if (!existsSync3(absDir))
1192
+ continue;
1193
+ const { readdir: readdir2 } = await import("fs/promises");
1194
+ try {
1195
+ const entries = await readdir2(absDir, { withFileTypes: true });
1196
+ for (const entry of entries.filter((e) => e.isDirectory())) {
1197
+ const tsconfig = join5(absDir, entry.name, "tsconfig.json");
1198
+ const paths = await extractTsConfigPaths(tsconfig, projectRoot);
1199
+ Object.assign(result, paths);
1038
1200
  }
1201
+ } catch {
1202
+ continue;
1039
1203
  }
1040
- return specs;
1041
- }
1042
- if (lang === "go") {
1043
- const re2 = /"([^"]+)"/g;
1044
- const importBlock = source.match(/import\s*\(([\s\S]*?)\)/g) || [];
1045
- for (const block of importBlock) {
1046
- let m3;
1047
- while ((m3 = re2.exec(block)) !== null)
1048
- specs.push(m3[1]);
1049
- }
1050
- const single = /import\s+(?:\w+\s+)?"([^"]+)"/g;
1051
- let m2;
1052
- while ((m2 = single.exec(source)) !== null)
1053
- specs.push(m2[1]);
1054
- return specs;
1055
- }
1056
- if (lang === "rust") {
1057
- const re2 = /\b(?:use|mod)\s+([\w:]+)/g;
1058
- let m2;
1059
- while ((m2 = re2.exec(source)) !== null)
1060
- specs.push(m2[1]);
1061
- return specs;
1062
- }
1063
- if (lang === "java") {
1064
- const re2 = /import\s+(?:static\s+)?([\w.]+)/g;
1065
- let m2;
1066
- while ((m2 = re2.exec(source)) !== null)
1067
- specs.push(m2[1]);
1068
- return specs;
1069
- }
1070
- const re = /(?:import|export)\s[^;]*?from\s*['"]([^'"]+)['"]|(?:import|require)\s*\(\s*['"]([^'"]+)['"]/g;
1071
- let m;
1072
- while ((m = re.exec(source)) !== null) {
1073
- specs.push(m[1] || m[2]);
1074
1204
  }
1075
- return specs;
1205
+ return result;
1076
1206
  }
1077
- var MONOREPO_ALIASES = [
1207
+ var CONVENTIONAL_ALIASES = [
1078
1208
  { prefix: "~/", replacement: "" },
1079
1209
  { prefix: "@components/", replacement: "components/" },
1080
1210
  { prefix: "@lib/", replacement: "lib/" },
@@ -1087,43 +1217,28 @@ var MONOREPO_ALIASES = [
1087
1217
  { prefix: "@calcom/ui/", replacement: "../packages/ui/" },
1088
1218
  { prefix: "@calcom/emails/", replacement: "../packages/emails/" }
1089
1219
  ];
1090
- function resolveAlias(spec) {
1091
- for (const { prefix, replacement } of MONOREPO_ALIASES) {
1092
- if (spec.startsWith(prefix)) {
1093
- return replacement + spec.slice(prefix.length);
1220
+ async function buildAliasMap(projectRoot) {
1221
+ const rootPaths = await extractTsConfigPaths(join5(projectRoot, "tsconfig.json"), projectRoot);
1222
+ const workspacePackages = await discoverWorkspacePackages(projectRoot);
1223
+ const appPaths = await discoverAppTsConfigPaths(projectRoot);
1224
+ const resolvedAliases = { ...appPaths, ...rootPaths };
1225
+ for (const [pkgName, pkgDir] of Object.entries(workspacePackages)) {
1226
+ if (!(pkgName in resolvedAliases)) {
1227
+ resolvedAliases[pkgName] = pkgDir;
1094
1228
  }
1095
1229
  }
1096
- return null;
1230
+ return { resolvedAliases, workspacePackages };
1097
1231
  }
1098
1232
  var JS_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
1099
- function resolveImport(spec, fromAbs, lang, projectRoot, fileSet, basenameIndex) {
1100
- if (lang === "python") {
1101
- return { resolved: resolvePython(spec, fromAbs, projectRoot, fileSet), isAlias: false };
1102
- }
1103
- if (lang === "typescript" || lang === "tsx" || lang === "javascript") {
1104
- if (spec.startsWith(".")) {
1105
- const base = join4(dirname(fromAbs), spec);
1106
- return { resolved: tryJsCandidates(base, projectRoot, fileSet), isAlias: false };
1107
- }
1108
- const aliasResolved = resolveAlias(spec);
1109
- if (aliasResolved !== null) {
1110
- const base = join4(projectRoot, aliasResolved);
1111
- const resolved = tryJsCandidates(base, projectRoot, fileSet);
1112
- return { resolved, isAlias: true };
1113
- }
1114
- return { resolved: null, isAlias: false };
1115
- }
1116
- return { resolved: resolveGeneric(spec, projectRoot, fileSet, basenameIndex), isAlias: false };
1117
- }
1118
1233
  function tryJsCandidates(base, projectRoot, fileSet) {
1119
1234
  const candidates = [];
1235
+ candidates.unshift(base);
1120
1236
  for (const ext of JS_EXTS)
1121
1237
  candidates.push(base + ext);
1122
1238
  for (const ext of JS_EXTS)
1123
- candidates.push(join4(base, "index" + ext));
1124
- candidates.unshift(base);
1239
+ candidates.push(join5(base, "index" + ext));
1125
1240
  for (const c of candidates) {
1126
- const rel = relative(projectRoot, c);
1241
+ const rel = relative2(projectRoot, c);
1127
1242
  if (fileSet.has(rel))
1128
1243
  return rel;
1129
1244
  }
@@ -1133,19 +1248,17 @@ function resolvePython(spec, fromAbs, projectRoot, fileSet) {
1133
1248
  let modulePath;
1134
1249
  if (spec.startsWith(".")) {
1135
1250
  const dots = spec.match(/^\.+/)[0].length;
1136
- let dir = dirname(fromAbs);
1251
+ let dir = dirname2(fromAbs);
1137
1252
  for (let i = 1; i < dots; i++)
1138
- dir = dirname(dir);
1139
- const rest = spec.slice(dots).replace(/\./g, sep);
1140
- modulePath = rest ? join4(dir, rest) : dir;
1253
+ dir = dirname2(dir);
1254
+ const rest = spec.slice(dots).replace(/\./g, sep2);
1255
+ modulePath = rest ? join5(dir, rest) : dir;
1141
1256
  } else {
1142
- modulePath = join4(projectRoot, spec.replace(/\./g, sep));
1257
+ modulePath = join5(projectRoot, spec.replace(/\./g, sep2));
1143
1258
  }
1144
- const candidates = [modulePath + ".py", join4(modulePath, "__init__.py")];
1145
- for (const c of candidates) {
1146
- const rel = relative(projectRoot, c);
1147
- if (fileSet.has(rel))
1148
- return rel;
1259
+ for (const c of [modulePath + ".py", join5(modulePath, "__init__.py")]) {
1260
+ if (fileSet.has(relative2(projectRoot, c)))
1261
+ return relative2(projectRoot, c);
1149
1262
  }
1150
1263
  return null;
1151
1264
  }
@@ -1156,8 +1269,8 @@ function resolveGeneric(spec, projectRoot, fileSet, basenameIndex) {
1156
1269
  return null;
1157
1270
  const last = parts[parts.length - 1];
1158
1271
  for (const rel of fileSet) {
1159
- const noExt = rel.slice(0, rel.length - extname(rel).length);
1160
- if (noExt.endsWith(parts.join(sep)))
1272
+ const noExt = rel.slice(0, rel.length - extname2(rel).length);
1273
+ if (noExt.endsWith(parts.join(sep2)))
1161
1274
  return rel;
1162
1275
  }
1163
1276
  const byBase = basenameIndex.get(last);
@@ -1165,942 +1278,1365 @@ function resolveGeneric(spec, projectRoot, fileSet, basenameIndex) {
1165
1278
  return byBase[0];
1166
1279
  return null;
1167
1280
  }
1168
- var FUNCTION_TYPES = /* @__PURE__ */ new Set([
1169
- "function_declaration",
1170
- "function",
1171
- "function_expression",
1172
- "arrow_function",
1173
- "method_definition",
1174
- "function_definition",
1175
- "method_declaration",
1176
- "func_literal",
1177
- "function_item",
1178
- "closure_expression",
1179
- "constructor_declaration",
1180
- "generator_function_declaration",
1181
- "generator_function"
1182
- ]);
1183
- var NESTING_TYPES = /* @__PURE__ */ new Set([
1184
- "function_declaration",
1185
- "function",
1186
- "arrow_function",
1187
- "function_expression",
1188
- "method_definition",
1189
- "function_definition",
1190
- "method_declaration",
1191
- "function_item",
1192
- "class_declaration",
1193
- "class",
1194
- "class_definition",
1195
- "class_item",
1196
- "if_statement",
1197
- "if_expression",
1198
- "for_statement",
1199
- "for_in_statement",
1200
- "for_expression",
1201
- "enhanced_for_statement",
1202
- "while_statement",
1203
- "while_expression",
1204
- "do_statement",
1205
- "switch_statement",
1206
- "match_expression",
1207
- "match_arm",
1208
- "try_statement",
1209
- "catch_clause",
1210
- "except_clause",
1211
- "loop_expression",
1212
- "block"
1213
- ]);
1214
- var DECISION_TYPES = /* @__PURE__ */ new Set([
1215
- "if_statement",
1216
- "if_expression",
1217
- "elif_clause",
1218
- "for_statement",
1219
- "for_in_statement",
1220
- "for_expression",
1221
- "enhanced_for_statement",
1222
- "while_statement",
1223
- "while_expression",
1224
- "do_statement",
1225
- "loop_expression",
1226
- "case",
1227
- "switch_case",
1228
- "case_clause",
1229
- "match_arm",
1230
- "catch_clause",
1231
- "except_clause",
1232
- "communication_case",
1233
- "conditional_expression",
1234
- "ternary_expression"
1235
- ]);
1236
- var CATCH_TYPES = /* @__PURE__ */ new Set(["catch_clause", "except_clause"]);
1237
- var LONG_FN_LOC = 60;
1238
- var DEEP_NESTING = 5;
1239
- var GOD_FILE_LOC = 400;
1240
- var GOD_FILE_EXPORTS = 8;
1241
- function nodeLOC(node) {
1242
- return node.endPosition.row - node.startPosition.row + 1;
1281
+ function resolveImportWithAliasMap(spec, fromAbs, lang, projectRoot, fileSet, basenameIndex, aliasMap) {
1282
+ if (lang === "python") {
1283
+ return { resolved: resolvePython(spec, fromAbs, projectRoot, fileSet), isAlias: false };
1284
+ }
1285
+ if (lang === "typescript" || lang === "tsx" || lang === "javascript") {
1286
+ if (spec.startsWith(".")) {
1287
+ const base = join5(dirname2(fromAbs), spec);
1288
+ return { resolved: tryJsCandidates(base, projectRoot, fileSet), isAlias: false };
1289
+ }
1290
+ for (const [prefix, replacement] of Object.entries(aliasMap.resolvedAliases)) {
1291
+ if (spec === prefix || spec.startsWith(prefix + "/")) {
1292
+ const rest = spec.slice(prefix.length).replace(/^\//, "");
1293
+ const base = join5(projectRoot, replacement, rest);
1294
+ const resolved = tryJsCandidates(base, projectRoot, fileSet);
1295
+ return { resolved, isAlias: true, reason: resolved ? void 0 : `alias '${prefix}' found but path '${replacement}/${rest}' not in file set` };
1296
+ }
1297
+ }
1298
+ for (const [pkgName, pkgDir] of Object.entries(aliasMap.workspacePackages)) {
1299
+ if (spec === pkgName || spec.startsWith(pkgName + "/")) {
1300
+ const rest = spec.slice(pkgName.length).replace(/^\//, "");
1301
+ const base = join5(projectRoot, pkgDir, rest);
1302
+ const resolved = tryJsCandidates(base, projectRoot, fileSet);
1303
+ return { resolved, isAlias: true, reason: resolved ? void 0 : `workspace package '${pkgName}' found but subpath '${rest}' not in file set` };
1304
+ }
1305
+ }
1306
+ for (const { prefix, replacement } of CONVENTIONAL_ALIASES) {
1307
+ if (spec.startsWith(prefix)) {
1308
+ const rest = replacement + spec.slice(prefix.length);
1309
+ const base = join5(projectRoot, rest);
1310
+ const resolved = tryJsCandidates(base, projectRoot, fileSet);
1311
+ return { resolved, isAlias: true, reason: resolved ? void 0 : `conventional alias '${prefix}' \u2192 path not found` };
1312
+ }
1313
+ }
1314
+ return { resolved: null, isAlias: false };
1315
+ }
1316
+ return { resolved: resolveGeneric(spec, projectRoot, fileSet, basenameIndex), isAlias: false };
1243
1317
  }
1244
- function countDecisions(node) {
1245
- let count = 0;
1246
- const walk = (n) => {
1247
- if (DECISION_TYPES.has(n.type))
1248
- count++;
1249
- if (n.type === "binary_expression") {
1250
- const op = n.children.find((c) => c.type === "&&" || c.type === "||");
1251
- if (op)
1252
- count++;
1318
+ async function runResolution(projectRoot, inv) {
1319
+ const aliasMap = await buildAliasMap(projectRoot);
1320
+ const { work, fileSet, basenameIndex } = inv;
1321
+ const importedBy = /* @__PURE__ */ new Map();
1322
+ const importsResolved = /* @__PURE__ */ new Map();
1323
+ const importsUnresolved = /* @__PURE__ */ new Map();
1324
+ const fanOut = /* @__PURE__ */ new Map();
1325
+ for (const w of work) {
1326
+ importedBy.set(w.rel, /* @__PURE__ */ new Set());
1327
+ importsResolved.set(w.rel, /* @__PURE__ */ new Set());
1328
+ importsUnresolved.set(w.rel, /* @__PURE__ */ new Set());
1329
+ }
1330
+ const graph = { nodes: {}, edges: [] };
1331
+ for (const w of work) {
1332
+ graph.nodes[w.rel] = { imports: w.importSpecs };
1333
+ }
1334
+ const resolutionFailuresByFile = {};
1335
+ const resolutionFailureReasons = {};
1336
+ const unresolvedSet = /* @__PURE__ */ new Set();
1337
+ for (const w of work) {
1338
+ const distinctModules = /* @__PURE__ */ new Set();
1339
+ for (const spec of w.importSpecs) {
1340
+ distinctModules.add(spec);
1341
+ const { resolved, isAlias, reason } = resolveImportWithAliasMap(spec, w.abs, w.lang, projectRoot, fileSet, basenameIndex, aliasMap);
1342
+ if (resolved && resolved !== w.rel && importedBy.has(resolved)) {
1343
+ importedBy.get(resolved).add(w.rel);
1344
+ importsResolved.get(w.rel).add(resolved);
1345
+ graph.edges.push({ from: w.rel, to: resolved });
1346
+ } else if (resolved === null && isAlias) {
1347
+ importsUnresolved.get(w.rel).add(spec);
1348
+ unresolvedSet.add(spec);
1349
+ if (reason) {
1350
+ if (!resolutionFailuresByFile[w.rel])
1351
+ resolutionFailuresByFile[w.rel] = [];
1352
+ resolutionFailuresByFile[w.rel].push(spec);
1353
+ if (!resolutionFailureReasons[spec])
1354
+ resolutionFailureReasons[spec] = reason;
1355
+ }
1356
+ }
1253
1357
  }
1254
- if (n.type === "boolean_operator")
1255
- count++;
1256
- for (const c of n.children)
1257
- walk(c);
1358
+ fanOut.set(w.rel, distinctModules.size);
1359
+ }
1360
+ const unresolvedImports = [...unresolvedSet];
1361
+ const dir = join5(projectRoot, ".vibe-splainer");
1362
+ await mkdir4(dir, { recursive: true });
1363
+ const stage04 = {
1364
+ resolvedAliases: aliasMap.resolvedAliases,
1365
+ workspacePackages: aliasMap.workspacePackages,
1366
+ unresolvedImports,
1367
+ resolutionFailuresByFile,
1368
+ resolutionFailureReasons
1369
+ };
1370
+ await writeFile5(join5(dir, "stage-04-aliases.json"), JSON.stringify(stage04, null, 2), "utf8");
1371
+ return {
1372
+ aliasMap,
1373
+ importedBy,
1374
+ importsResolved,
1375
+ importsUnresolved,
1376
+ fanOut,
1377
+ graph,
1378
+ unresolvedImports,
1379
+ resolutionFailuresByFile,
1380
+ resolutionFailureReasons
1258
1381
  };
1259
- walk(node);
1260
- return count;
1261
1382
  }
1262
- function computeNesting(node, depth) {
1263
- let maxDepth = depth;
1264
- for (const child of node.children) {
1265
- const nextDepth = NESTING_TYPES.has(child.type) ? depth + 1 : depth;
1266
- maxDepth = Math.max(maxDepth, computeNesting(child, nextDepth));
1383
+
1384
+ // ../brain/dist/pipeline/classification.js
1385
+ import { join as join6, basename as basename2, extname as extname3, sep as sep3 } from "path";
1386
+ import { writeFile as writeFile6, mkdir as mkdir5 } from "fs/promises";
1387
+ function inferSideEffectProfile(source, importSpecs, productDomain, frameworkRole) {
1388
+ const effects = /* @__PURE__ */ new Set();
1389
+ if (/router\.(push|replace|back)\(|redirect\(|notFound\(|permanentRedirect\(/.test(source)) {
1390
+ effects.add("redirect");
1267
1391
  }
1268
- return maxDepth;
1392
+ if (/["']use server["']/.test(source))
1393
+ effects.add("server_action");
1394
+ if (/useMutation\b|\.mutate\b|\.mutateAsync\b/.test(source))
1395
+ effects.add("trpc_mutation");
1396
+ if (/sdkActionManager\.fire|telemetry\.|posthog\.|mixpanel\.|amplitude\.|ga\(/.test(source) || importSpecs.some((s) => /analytics|telemetry|posthog|mixpanel|amplitude/.test(s)))
1397
+ effects.add("analytics_event");
1398
+ if (/prisma\s*[.?]\s*\w+\s*[.?]\s*(create|update|upsert|delete|deleteMany|updateMany|createMany|transaction|executeRaw|queryRaw)\b/.test(source)) {
1399
+ effects.add("database_write");
1400
+ }
1401
+ if (/prisma\s*[.?]\s*\w+\s*[.?]\s*(findMany|findUnique|findFirst|findFirstOrThrow|findUniqueOrThrow|count|aggregate|groupBy)\b/.test(source)) {
1402
+ effects.add("database_read");
1403
+ }
1404
+ if (/createBooking|handleNewBooking|cancelBooking|rescheduleBooking|handleBooking|createRecurring/.test(source) || productDomain === "booking_creation" && /useMutation\b|\.mutate\b|\.mutateAsync\b/.test(source))
1405
+ effects.add("booking_mutation");
1406
+ if (/stripe\.webhooks\.(constructEvent|constructEventAsync)|webhookSecret|validateWebhook|verifyWebhook|verifySignature/.test(source) || productDomain === "payments_webhooks" && frameworkRole === "pages_api_route")
1407
+ effects.add("webhook_ingress");
1408
+ if (importSpecs.some((s) => /stripe|paypal|btcpay|alby/.test(s.toLowerCase())) || /stripe\.|paymentIntent|createPaymentIntent|confirmPayment|createCharge/.test(source) || productDomain === "payments_webhooks" && effects.has("webhook_ingress"))
1409
+ effects.add("payment_mutation");
1410
+ if (/signIn\b|signOut\b|createSession|destroySession|issueToken|refreshToken|getToken/.test(source)) {
1411
+ effects.add("auth_token_mutation");
1412
+ }
1413
+ if (/triggerWebhook|sendWebhook|webhook\.send\b/.test(source))
1414
+ effects.add("webhook_delivery");
1415
+ if (/sendEmail|sendMail\b|mailer\./.test(source) || importSpecs.some((s) => /nodemailer|resend|sendgrid|postmark|mailgun/.test(s)))
1416
+ effects.add("email_send");
1417
+ if (/createCalendarEvent|updateCalendarEvent|deleteCalendarEvent|calendar\.events\.(insert|update|delete|patch)/.test(source)) {
1418
+ effects.add("calendar_mutation");
1419
+ }
1420
+ if (/revalidatePath\b|revalidateTag\b/.test(source))
1421
+ effects.add("cache_revalidation");
1422
+ if (/localStorage\.|sessionStorage\./.test(source))
1423
+ effects.add("local_storage");
1424
+ if (/indexedDB\b|new Dexie|idb\./.test(source))
1425
+ effects.add("indexed_db");
1426
+ if (/\bfetch\s*\(|axios\.(get|post|put|patch|delete)\b/.test(source)) {
1427
+ effects.add("external_api_call");
1428
+ }
1429
+ if (effects.size === 0)
1430
+ effects.add("none_detected");
1431
+ return [...effects];
1269
1432
  }
1270
- function firstLine(s) {
1271
- return s.split("\n")[0];
1433
+ function inferWriteIntents(productDomain, relPath, sideEffectProfile) {
1434
+ const intents = [];
1435
+ if (productDomain === "booking_creation") {
1436
+ intents.push("create_booking");
1437
+ if (relPath.includes("reschedule") || relPath.includes("Reschedule"))
1438
+ intents.push("reschedule_booking");
1439
+ if (relPath.includes("recurring") || relPath.includes("Recurring"))
1440
+ intents.push("create_recurring_booking");
1441
+ }
1442
+ if (productDomain === "booking_management")
1443
+ intents.push("cancel_booking");
1444
+ if (productDomain === "event_type_configuration")
1445
+ intents.push("update_event_type");
1446
+ if (productDomain === "availability")
1447
+ intents.push("update_availability");
1448
+ if (productDomain === "payments")
1449
+ intents.push("create_payment");
1450
+ if (productDomain === "payments_webhooks")
1451
+ intents.push("handle_payment_webhook");
1452
+ if (productDomain === "auth_oauth") {
1453
+ intents.push("issue_auth_token");
1454
+ intents.push("refresh_auth_token");
1455
+ }
1456
+ if (sideEffectProfile.includes("webhook_delivery"))
1457
+ intents.push("send_webhook");
1458
+ if (productDomain === "settings")
1459
+ intents.push("update_user_settings");
1460
+ if (sideEffectProfile.includes("local_storage") || sideEffectProfile.includes("indexed_db")) {
1461
+ intents.push("persist_local_state");
1462
+ }
1463
+ return intents.length > 0 ? intents : ["none_detected"];
1272
1464
  }
1273
- function stripLeadingComments(snippet) {
1274
- const lines = snippet.split("\n");
1275
- let i = 0;
1276
- let inBlock = false;
1277
- while (i < lines.length) {
1278
- const t = lines[i].trim();
1279
- if (inBlock) {
1280
- if (t.includes("*/"))
1281
- inBlock = false;
1282
- i++;
1465
+ var ENTRYPOINT_ROLES = /* @__PURE__ */ new Set([
1466
+ "app_route_page",
1467
+ "app_route_handler",
1468
+ "pages_route",
1469
+ "pages_api_route",
1470
+ "trpc_api_route"
1471
+ ]);
1472
+ function inferRiskTypesPass1(rel, frameworkRole, productDomain, sideEffectProfile, gravitySignals, smellKinds) {
1473
+ const types = [];
1474
+ const smThreshold = ["provider", "store"].includes(frameworkRole) ? 8 : 20;
1475
+ if (gravitySignals.cyclomatic > smThreshold)
1476
+ types.push("state_machine");
1477
+ if (smellKinds.has("god-file")) {
1478
+ if (frameworkRole === "hook")
1479
+ types.push("god_hook");
1480
+ else
1481
+ types.push("god_component");
1482
+ }
1483
+ if (sideEffectProfile.length > 3 && !sideEffectProfile.includes("none_detected")) {
1484
+ types.push("side_effect_coupling");
1485
+ }
1486
+ if (productDomain === "forms" && (gravitySignals.fanIn > 3 || gravitySignals.publicSurface > 5))
1487
+ types.push("registry_bottleneck");
1488
+ if (sideEffectProfile.some((s) => ["booking_mutation", "payment_mutation", "auth_token_mutation"].includes(s)) && gravitySignals.cyclomatic > 10)
1489
+ types.push("mutation_orchestration");
1490
+ if (ENTRYPOINT_ROLES.has(frameworkRole) && sideEffectProfile.includes("database_write")) {
1491
+ types.push("route_handler_write_path");
1492
+ }
1493
+ if (smellKinds.has("swallowed-catch"))
1494
+ types.push("error_swallowing");
1495
+ if (sideEffectProfile.includes("local_storage") || sideEffectProfile.includes("indexed_db")) {
1496
+ types.push("storage_persistence_risk");
1497
+ }
1498
+ return types;
1499
+ }
1500
+ var DOMAIN_SURFACE_PATTERNS = {
1501
+ booking_creation: {
1502
+ expected: [/book/i, /booking/i, /reschedule/i, /booking-success/i, /api\/book/i, /create-booking/i],
1503
+ wrong: [/event-type/i, /event-types/i, /eventtypes/i, /availability/i, /schedule/i]
1504
+ },
1505
+ payments_webhooks: {
1506
+ expected: [/webhook/i, /stripe/i, /payment/i],
1507
+ wrong: [/settings/i, /onboarding/i, /profile/i]
1508
+ },
1509
+ auth_oauth: {
1510
+ expected: [/oauth/i, /callback/i, /auth/i, /signin/i, /login/i],
1511
+ wrong: [/booking/i, /payment/i, /settings/i]
1512
+ }
1513
+ };
1514
+ function findRuntimeEntrypoints(relPath, importedByMap, persisted, maxDepth = 8) {
1515
+ const results = [];
1516
+ const seen = /* @__PURE__ */ new Set();
1517
+ const queue = [{ path: relPath, depth: 0 }];
1518
+ while (queue.length > 0) {
1519
+ const current = queue.shift();
1520
+ if (seen.has(current.path))
1283
1521
  continue;
1522
+ seen.add(current.path);
1523
+ if (current.path !== relPath) {
1524
+ const meta = persisted.get(current.path);
1525
+ if (meta && ENTRYPOINT_ROLES.has(meta.frameworkRole)) {
1526
+ results.push({
1527
+ path: current.path,
1528
+ frameworkRole: meta.frameworkRole,
1529
+ productDomain: meta.productDomain,
1530
+ distance: current.depth
1531
+ });
1532
+ if (results.length >= 8)
1533
+ break;
1534
+ continue;
1535
+ }
1284
1536
  }
1285
- if (t === "") {
1286
- i++;
1537
+ if (current.depth >= maxDepth)
1287
1538
  continue;
1288
- }
1289
- if (t.startsWith("//") || t.startsWith("#")) {
1290
- i++;
1539
+ const importers = importedByMap.get(current.path);
1540
+ if (!importers)
1291
1541
  continue;
1542
+ for (const importer of importers) {
1543
+ if (!seen.has(importer))
1544
+ queue.push({ path: importer, depth: current.depth + 1 });
1292
1545
  }
1293
- if (t.startsWith("/*")) {
1294
- inBlock = !t.includes("*/");
1295
- i++;
1296
- continue;
1546
+ }
1547
+ const byPath = /* @__PURE__ */ new Map();
1548
+ for (const r of results) {
1549
+ const existing = byPath.get(r.path);
1550
+ if (!existing || r.distance < existing.distance)
1551
+ byPath.set(r.path, r);
1552
+ }
1553
+ return [...byPath.values()].sort((a, b) => a.distance - b.distance);
1554
+ }
1555
+ function deriveEntrypointTraceStatus(domain, entrypoints, unresolved) {
1556
+ if (entrypoints.length === 0 && unresolved.length > 0)
1557
+ return "blocked_by_alias_resolution";
1558
+ if (entrypoints.length === 0)
1559
+ return "no_runtime_entrypoint_found";
1560
+ const patterns = DOMAIN_SURFACE_PATTERNS[domain];
1561
+ if (patterns) {
1562
+ const allWrong = entrypoints.every((e) => patterns.wrong.some((p) => p.test(e.path)) && !patterns.expected.some((p) => p.test(e.path)));
1563
+ if (allWrong)
1564
+ return "partial_wrong_surface";
1565
+ }
1566
+ return unresolved.length === 0 ? "complete" : "partial";
1567
+ }
1568
+ function computeLoadBearingScore(gravity, heat, importedByCount, sideEffectProfile, productDomain, smellMaxSeverity, runtimeEntrypoints) {
1569
+ let score = 0;
1570
+ if (gravity >= 85)
1571
+ score += 2;
1572
+ if (heat >= 60)
1573
+ score += 1;
1574
+ if (runtimeEntrypoints.length >= 2)
1575
+ score += 2;
1576
+ if (importedByCount >= 3)
1577
+ score += 1;
1578
+ if (sideEffectProfile.includes("database_write"))
1579
+ score += 3;
1580
+ if (sideEffectProfile.includes("booking_mutation"))
1581
+ score += 3;
1582
+ if (sideEffectProfile.includes("payment_mutation"))
1583
+ score += 3;
1584
+ if (sideEffectProfile.includes("auth_token_mutation"))
1585
+ score += 3;
1586
+ if (sideEffectProfile.includes("webhook_delivery"))
1587
+ score += 2;
1588
+ if (sideEffectProfile.includes("webhook_ingress"))
1589
+ score += 2;
1590
+ if (sideEffectProfile.includes("calendar_mutation"))
1591
+ score += 2;
1592
+ if (sideEffectProfile.includes("redirect"))
1593
+ score += 1;
1594
+ if (sideEffectProfile.includes("analytics_event"))
1595
+ score += 1;
1596
+ const highImpactDomains = [
1597
+ "booking_creation",
1598
+ "payments",
1599
+ "auth_oauth",
1600
+ "webhooks",
1601
+ "payments_webhooks"
1602
+ ];
1603
+ if (highImpactDomains.includes(productDomain))
1604
+ score += 2;
1605
+ if (smellMaxSeverity === 5)
1606
+ score += 3;
1607
+ return score;
1608
+ }
1609
+ function pageRank(nodes, outEdges, damping = 0.85, iters = 20) {
1610
+ const n = nodes.length;
1611
+ const rank = /* @__PURE__ */ new Map();
1612
+ if (n === 0)
1613
+ return rank;
1614
+ for (const node of nodes)
1615
+ rank.set(node, 1 / n);
1616
+ const inEdges = /* @__PURE__ */ new Map();
1617
+ for (const node of nodes)
1618
+ inEdges.set(node, []);
1619
+ const outCount = /* @__PURE__ */ new Map();
1620
+ for (const [from, tos] of outEdges) {
1621
+ const valid = [...tos].filter((t) => rank.has(t));
1622
+ outCount.set(from, valid.length);
1623
+ for (const to of valid)
1624
+ inEdges.get(to).push(from);
1625
+ }
1626
+ for (let it = 0; it < iters; it++) {
1627
+ const next = /* @__PURE__ */ new Map();
1628
+ let dangling = 0;
1629
+ for (const node of nodes) {
1630
+ if ((outCount.get(node) || 0) === 0)
1631
+ dangling += rank.get(node);
1297
1632
  }
1298
- if (t.startsWith('"""') || t.startsWith("'''")) {
1299
- const q = t.slice(0, 3);
1300
- if (t.length > 3 && t.endsWith(q)) {
1301
- i++;
1633
+ for (const node of nodes) {
1634
+ let sum = 0;
1635
+ for (const from of inEdges.get(node)) {
1636
+ sum += rank.get(from) / (outCount.get(from) || 1);
1637
+ }
1638
+ next.set(node, (1 - damping) / n + damping * (sum + dangling / n));
1639
+ }
1640
+ for (const node of nodes)
1641
+ rank.set(node, next.get(node));
1642
+ }
1643
+ let max = 0;
1644
+ for (const v of rank.values())
1645
+ max = Math.max(max, v);
1646
+ if (max > 0)
1647
+ for (const node of nodes)
1648
+ rank.set(node, rank.get(node) / max);
1649
+ return rank;
1650
+ }
1651
+ function detectCommunities(nodes, adjacency) {
1652
+ const label = /* @__PURE__ */ new Map();
1653
+ nodes.forEach((node, i) => label.set(node, i));
1654
+ for (let pass = 0; pass < 10; pass++) {
1655
+ let changed = false;
1656
+ for (const node of nodes) {
1657
+ const neighbors = adjacency.get(node);
1658
+ if (!neighbors || neighbors.size === 0)
1302
1659
  continue;
1660
+ const counts = /* @__PURE__ */ new Map();
1661
+ for (const [nb, weight] of neighbors) {
1662
+ const l = label.get(nb);
1663
+ counts.set(l, (counts.get(l) || 0) + weight);
1664
+ }
1665
+ let best = label.get(node), bestCount = -1;
1666
+ for (const [l, c] of counts) {
1667
+ if (c > bestCount || c === bestCount && l < best) {
1668
+ best = l;
1669
+ bestCount = c;
1670
+ }
1671
+ }
1672
+ if (best !== label.get(node)) {
1673
+ label.set(node, best);
1674
+ changed = true;
1303
1675
  }
1304
- i++;
1305
- while (i < lines.length && !lines[i].includes(q))
1306
- i++;
1307
- i++;
1308
- continue;
1309
1676
  }
1310
- break;
1677
+ if (!changed)
1678
+ break;
1311
1679
  }
1312
- return lines.slice(i).join("\n");
1680
+ return label;
1313
1681
  }
1314
- var TODO_RE = /\b(TODO|FIXME|HACK|XXX|KLUDGE)\b|@deprecated/;
1315
- var SUPPRESS_RE = /@ts-ignore|@ts-nocheck|eslint-disable|:\s*any\b|#\s*type:\s*ignore|type:\s*ignore|#\s*nosec/;
1316
- function collectFunctionNodes(root) {
1317
- const out = [];
1318
- const walk = (n) => {
1319
- if (FUNCTION_TYPES.has(n.type))
1320
- out.push(n);
1321
- for (const c of n.children)
1322
- walk(c);
1323
- };
1324
- walk(root);
1325
- return out;
1682
+ function titleCase(s) {
1683
+ return s.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
1326
1684
  }
1327
- function catchIsSwallowed(node, _lang) {
1328
- const bodyText = node.text;
1329
- const inner = bodyText.replace(/^[^{:]*[{:]/, "");
1330
- const meaningful = inner.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("//") && !l.startsWith("#") && l !== "}" && l !== "pass");
1331
- if (meaningful.length === 0)
1332
- return true;
1333
- const onlyLogs = meaningful.every((l) => /^(console\.(log|error|warn|info)|print|println!?|System\.out|logger?\.)/.test(l) || l === "pass" || l === "{" || l === "});" || l === ")" || l === "`");
1334
- return onlyLogs;
1685
+ function domainToGroupLabel(domain) {
1686
+ const labels = {
1687
+ booking_creation: "Booking",
1688
+ booking_management: "Booking",
1689
+ booking_audit: "Booking Audit",
1690
+ event_type_configuration: "Event Types",
1691
+ availability: "Availability",
1692
+ auth: "Auth",
1693
+ auth_oauth: "Auth OAuth",
1694
+ payments: "Payments",
1695
+ payments_webhooks: "Payment Webhooks",
1696
+ webhooks: "Webhooks",
1697
+ apps_marketplace: "Apps",
1698
+ calendar_integrations: "Calendar",
1699
+ video: "Video",
1700
+ onboarding: "Onboarding",
1701
+ settings: "Settings",
1702
+ admin: "Admin",
1703
+ data_table: "Data Table",
1704
+ shell_navigation: "Shell",
1705
+ forms: "Forms",
1706
+ embed: "Embed",
1707
+ notifications: "Notifications"
1708
+ };
1709
+ return labels[domain] || titleCase(domain.replace(/_/g, " "));
1335
1710
  }
1336
- function analyzeAst(source, lang, tree) {
1337
- const root = tree.rootNode;
1338
- const lines = source.split("\n");
1339
- const loc = lines.length;
1340
- const cyclomatic = countDecisions(root);
1341
- const maxNesting = computeNesting(root, 0);
1342
- const smells = [];
1343
- let todos = 0, suppressions = 0;
1344
- for (let i = 0; i < lines.length; i++) {
1345
- const line = lines[i];
1346
- if (TODO_RE.test(line)) {
1347
- todos++;
1348
- smells.push({ kind: "todo", line: i + 1, endLine: i + 1, text: line.trim().slice(0, 200), severity: 2, note: "unfinished / known-bad marker" });
1711
+ function pillarNameFromCluster(files) {
1712
+ const hintCounts = /* @__PURE__ */ new Map();
1713
+ for (const f of files) {
1714
+ if (f.pillarHint && !f.pillarHint.startsWith("community-")) {
1715
+ hintCounts.set(f.pillarHint, (hintCounts.get(f.pillarHint) || 0) + 1);
1349
1716
  }
1350
- if (SUPPRESS_RE.test(line)) {
1351
- suppressions++;
1352
- smells.push({ kind: "suppression", line: i + 1, endLine: i + 1, text: line.trim().slice(0, 200), severity: 3, note: "type/lint safety suppressed" });
1717
+ }
1718
+ if (hintCounts.size > 0) {
1719
+ const best = [...hintCounts.entries()].sort((a, b) => b[1] - a[1])[0];
1720
+ if (best[1] >= files.length * 0.4)
1721
+ return best[0];
1722
+ }
1723
+ const dirs = files.map((f) => dirname_simple(f.rel)).filter((d) => d && d !== ".");
1724
+ if (dirs.length) {
1725
+ const segCounts = /* @__PURE__ */ new Map();
1726
+ for (const d of dirs) {
1727
+ const segments = d.split(sep3).filter((s) => !MEANINGLESS_SEGMENTS.has(s.toLowerCase()));
1728
+ const meaningful = segments.pop();
1729
+ if (meaningful)
1730
+ segCounts.set(meaningful, (segCounts.get(meaningful) || 0) + 1);
1353
1731
  }
1732
+ const top = [...segCounts.entries()].sort((a, b) => b[1] - a[1])[0];
1733
+ if (top)
1734
+ return titleCase(top[0]);
1354
1735
  }
1355
- let magicNumbers = 0;
1356
- const magicWalk = (n) => {
1357
- if (n.type === "number" || n.type === "integer_literal" || n.type === "float_literal" || n.type === "int_literal") {
1358
- const v = n.text.replace(/_/g, "");
1359
- if (!["0", "1", "2", "-1", "100", "1000"].includes(v) && /^\d{2,}$/.test(v)) {
1360
- magicNumbers++;
1361
- }
1736
+ const topFile = basename2(files[0].rel, extname3(files[0].rel));
1737
+ return titleCase(topFile);
1738
+ }
1739
+ function dirname_simple(p) {
1740
+ const idx = p.lastIndexOf(sep3);
1741
+ if (idx < 0)
1742
+ return ".";
1743
+ return p.slice(0, idx);
1744
+ }
1745
+ function buildPillars(classified, communities) {
1746
+ const real = classified.filter((f) => f.isRealSource);
1747
+ const keywordGroups = /* @__PURE__ */ new Map();
1748
+ const unlabeled = [];
1749
+ for (const f of real) {
1750
+ if (f.pillarHint && !f.pillarHint.startsWith("community-")) {
1751
+ if (!keywordGroups.has(f.pillarHint))
1752
+ keywordGroups.set(f.pillarHint, []);
1753
+ keywordGroups.get(f.pillarHint).push(f);
1754
+ } else {
1755
+ unlabeled.push(f);
1362
1756
  }
1363
- for (const c of n.children)
1364
- magicWalk(c);
1365
- };
1366
- magicWalk(root);
1367
- if (magicNumbers > 6) {
1368
- smells.push({ kind: "magic-number", line: 1, endLine: 1, text: `${magicNumbers} unexplained numeric literals`, severity: 2, note: "many magic numbers \u2014 extract named constants" });
1369
1757
  }
1370
- let swallowedCatches = 0;
1371
- const catchWalk = (n) => {
1372
- if (CATCH_TYPES.has(n.type) && catchIsSwallowed(n, lang)) {
1373
- swallowedCatches++;
1374
- smells.push({
1375
- kind: "swallowed-catch",
1376
- line: n.startPosition.row + 1,
1377
- endLine: n.endPosition.row + 1,
1378
- text: firstLine(n.text).trim().slice(0, 200),
1379
- severity: 4,
1380
- note: "catch block swallows error silently"
1381
- });
1758
+ const pillars = [];
1759
+ for (const [name, files] of keywordGroups) {
1760
+ const sorted = [...files].sort((a, b) => b.gravity - a.gravity);
1761
+ pillars.push({
1762
+ name,
1763
+ description: `${name} subsystem: ${files.length} file${files.length > 1 ? "s" : ""} centered on ${basename2(sorted[0].rel)}.`,
1764
+ memberFiles: sorted.map((f) => f.rel)
1765
+ });
1766
+ }
1767
+ if (unlabeled.length > 0) {
1768
+ const communityGroups = /* @__PURE__ */ new Map();
1769
+ for (const f of unlabeled) {
1770
+ const c = communities.get(f.rel);
1771
+ if (c === void 0)
1772
+ continue;
1773
+ if (!communityGroups.has(c))
1774
+ communityGroups.set(c, []);
1775
+ communityGroups.get(c).push(f);
1382
1776
  }
1383
- for (const c of n.children)
1384
- catchWalk(c);
1385
- };
1386
- catchWalk(root);
1387
- const fnNodes = collectFunctionNodes(root);
1388
- let longFunctions = 0;
1389
- const scored = [];
1390
- for (const fn of fnNodes) {
1391
- const bodyLOC = nodeLOC(fn);
1392
- const decisions = countDecisions(fn);
1393
- scored.push({ node: fn, decisions, bodyLOC, score: decisions + bodyLOC });
1394
- if (bodyLOC > LONG_FN_LOC) {
1395
- longFunctions++;
1396
- smells.push({
1397
- kind: "long-function",
1398
- line: fn.startPosition.row + 1,
1399
- endLine: fn.endPosition.row + 1,
1400
- text: firstLine(fn.text).trim().slice(0, 200),
1401
- severity: 3,
1402
- note: `function body is ${bodyLOC} lines`
1403
- });
1777
+ const remainingSlots = Math.max(0, 6 - pillars.length);
1778
+ const sorted = [...communityGroups.entries()].map(([id, files]) => ({ id, files, weight: files.reduce((s, f) => s + f.gravity, 0) })).filter((g) => g.files.length >= 2).sort((a, b) => b.weight - a.weight).slice(0, remainingSlots);
1779
+ for (const g of sorted) {
1780
+ const top = [...g.files].sort((a, b) => b.gravity - a.gravity);
1781
+ const name = pillarNameFromCluster(top.map((f) => ({ rel: f.rel, pillarHint: f.pillarHint })));
1782
+ const existing = pillars.find((p) => p.name === name);
1783
+ if (existing) {
1784
+ existing.memberFiles.push(...top.map((f) => f.rel));
1785
+ existing.description = `${name} subsystem: ${existing.memberFiles.length} files.`;
1786
+ } else {
1787
+ pillars.push({
1788
+ name,
1789
+ description: `${g.files.length} files centered on ${basename2(top[0].rel)}.`,
1790
+ memberFiles: top.map((f) => f.rel)
1791
+ });
1792
+ }
1404
1793
  }
1405
1794
  }
1406
- if (maxNesting > DEEP_NESTING) {
1407
- smells.push({ kind: "deep-nesting", line: 1, endLine: 1, text: `nesting depth ${maxNesting}`, severity: 3, note: `control flow nested ${maxNesting} levels deep` });
1795
+ pillars.sort((a, b) => {
1796
+ const gravA = real.filter((f) => a.memberFiles.includes(f.rel)).reduce((s, f) => s + f.gravity, 0);
1797
+ const gravB = real.filter((f) => b.memberFiles.includes(f.rel)).reduce((s, f) => s + f.gravity, 0);
1798
+ return gravB - gravA;
1799
+ });
1800
+ if (pillars.length === 0 && real.length > 0) {
1801
+ pillars.push({ name: "Core", description: "Primary application code.", memberFiles: real.slice(0, 20).map((f) => f.rel) });
1408
1802
  }
1409
- const exported = collectExports(root, lang);
1410
- const publicSurface = exported.length;
1411
- const signature = exported.map((e) => e.text).join("\n").slice(0, 4e3);
1412
- if (loc > GOD_FILE_LOC && publicSurface > GOD_FILE_EXPORTS) {
1413
- smells.push({
1414
- kind: "god-file",
1415
- line: 1,
1416
- endLine: 1,
1417
- text: `${loc} LOC, ${publicSurface} exports`,
1418
- severity: 4,
1419
- note: `god-file: ${loc} lines exporting ${publicSurface} symbols`
1420
- });
1803
+ const finalPillars = [];
1804
+ for (const p of pillars) {
1805
+ if (p.memberFiles.length > 15) {
1806
+ const groups = /* @__PURE__ */ new Map();
1807
+ for (const rel of p.memberFiles) {
1808
+ const f = classified.find((c) => c.rel === rel);
1809
+ const role = f?.frameworkRole || "unknown";
1810
+ const domain = f?.productDomain || "unknown";
1811
+ let bucket;
1812
+ if (domain !== "unknown" && domain !== "routing_infrastructure" && domain !== "test_infrastructure" && domain !== "generated_noise") {
1813
+ bucket = domainToGroupLabel(domain);
1814
+ } else if (role === "hook") {
1815
+ bucket = "Hooks";
1816
+ } else if (["app_route_page", "app_route_handler", "app_route_layout", "pages_route", "pages_api_route", "trpc_api_route"].includes(role)) {
1817
+ bucket = "Routes";
1818
+ } else if (role === "component") {
1819
+ bucket = "Components";
1820
+ } else {
1821
+ bucket = "Logic";
1822
+ }
1823
+ const key = `${p.name} (${bucket})`;
1824
+ if (!groups.has(key))
1825
+ groups.set(key, []);
1826
+ groups.get(key).push(rel);
1827
+ }
1828
+ for (const [key, files] of groups) {
1829
+ if (files.length > 0)
1830
+ finalPillars.push({ name: key, description: `Subdivided from ${p.name}`, memberFiles: files });
1831
+ }
1832
+ } else {
1833
+ finalPillars.push(p);
1834
+ }
1421
1835
  }
1422
- scored.sort((a, b) => b.score - a.score);
1423
- const hotSpans = scored.slice(0, 3).filter((s) => s.bodyLOC >= 4).map((s) => {
1424
- const raw = source.split("\n").slice(s.node.startPosition.row, s.node.endPosition.row + 1).join("\n");
1425
- const snippet = stripLeadingComments(raw).slice(0, 2e3);
1426
- return {
1427
- startLine: s.node.startPosition.row + 1,
1428
- endLine: s.node.endPosition.row + 1,
1429
- snippet,
1430
- reason: `high complexity: ${s.decisions} decision branches across ${s.bodyLOC} lines`
1431
- };
1432
- });
1433
- return {
1434
- language: lang,
1435
- loc,
1436
- cyclomatic,
1437
- maxNesting,
1438
- publicSurface,
1439
- exportedNames: exported.map((e) => e.name),
1440
- signature,
1441
- longFunctions,
1442
- magicNumbers,
1443
- swallowedCatches,
1444
- smells,
1445
- hotSpans
1446
- };
1447
- }
1448
- function collectExports(root, lang) {
1449
- const out = [];
1450
1836
  const seen = /* @__PURE__ */ new Set();
1451
- const push = (name, node) => {
1452
- if (!name || seen.has(name))
1453
- return;
1454
- seen.add(name);
1455
- out.push({ name, text: firstLine(node.text).trim().slice(0, 200) });
1456
- };
1457
- if (lang === "python") {
1458
- for (const c of root.children) {
1459
- if (c.type === "function_definition" || c.type === "class_definition") {
1460
- const name = c.childForFieldName("name")?.text;
1461
- if (name && !name.startsWith("_"))
1462
- push(name, c);
1463
- }
1837
+ for (const p of finalPillars) {
1838
+ let n = p.name, i = 2;
1839
+ while (seen.has(n)) {
1840
+ n = `${p.name} ${i++}`;
1841
+ }
1842
+ p.name = n;
1843
+ seen.add(n);
1844
+ }
1845
+ return finalPillars;
1846
+ }
1847
+ async function runClassification(projectRoot, inv, res) {
1848
+ const { work, entrypoints } = inv;
1849
+ const { importedBy, importsResolved, importsUnresolved, fanOut } = res;
1850
+ const isRealSource = /* @__PURE__ */ new Map();
1851
+ const demoteReason = /* @__PURE__ */ new Map();
1852
+ for (const w of work) {
1853
+ if (w.pathDemote) {
1854
+ isRealSource.set(w.rel, false);
1855
+ demoteReason.set(w.rel, w.pathDemote);
1856
+ } else {
1857
+ isRealSource.set(w.rel, true);
1858
+ demoteReason.set(w.rel, null);
1464
1859
  }
1465
- return out;
1466
1860
  }
1467
- if (lang === "go") {
1468
- const walk2 = (n) => {
1469
- if (n.type === "function_declaration" || n.type === "method_declaration" || n.type === "type_declaration") {
1470
- const name = n.childForFieldName("name")?.text;
1471
- if (name && /^[A-Z]/.test(name))
1472
- push(name, n);
1473
- }
1474
- for (const c of n.children)
1475
- walk2(c);
1476
- };
1477
- walk2(root);
1478
- return out;
1861
+ for (const w of work) {
1862
+ if (!isRealSource.get(w.rel))
1863
+ continue;
1864
+ if (entrypoints.has(w.rel))
1865
+ continue;
1866
+ const inbound = [...importedBy.get(w.rel) || []].filter((src) => isRealSource.get(src));
1867
+ if (inbound.length === 0) {
1868
+ isRealSource.set(w.rel, false);
1869
+ demoteReason.set(w.rel, "no inbound references from application code");
1870
+ }
1479
1871
  }
1480
- if (lang === "rust") {
1481
- const walk2 = (n) => {
1482
- if (/_item$/.test(n.type) && n.children.some((c) => c.type === "visibility_modifier")) {
1483
- const name = n.childForFieldName("name")?.text;
1484
- push(name, n);
1485
- }
1486
- for (const c of n.children)
1487
- walk2(c);
1488
- };
1489
- walk2(root);
1490
- return out;
1872
+ const realNodes = work.filter((w) => isRealSource.get(w.rel)).map((w) => w.rel);
1873
+ const realSet = new Set(realNodes);
1874
+ const outEdges = /* @__PURE__ */ new Map();
1875
+ const undirected = /* @__PURE__ */ new Map();
1876
+ for (const node of realNodes) {
1877
+ outEdges.set(node, /* @__PURE__ */ new Set());
1878
+ undirected.set(node, /* @__PURE__ */ new Map());
1491
1879
  }
1492
- if (lang === "java") {
1493
- const walk2 = (n) => {
1494
- if ((n.type === "method_declaration" || n.type === "class_declaration") && /\bpublic\b/.test(firstLine(n.text))) {
1495
- const name = n.childForFieldName("name")?.text;
1496
- push(name, n);
1497
- }
1498
- for (const c of n.children)
1499
- walk2(c);
1880
+ for (const w of work) {
1881
+ if (!realSet.has(w.rel))
1882
+ continue;
1883
+ for (const target of importsResolved.get(w.rel) || /* @__PURE__ */ new Set()) {
1884
+ if (!realSet.has(target))
1885
+ continue;
1886
+ outEdges.get(w.rel).add(target);
1887
+ const wDir = w.rel.split(sep3)[0];
1888
+ const tDir = target.split(sep3)[0];
1889
+ const weight = wDir === tDir ? 1 : 0.5;
1890
+ undirected.get(w.rel).set(target, weight);
1891
+ undirected.get(target).set(w.rel, weight);
1892
+ }
1893
+ }
1894
+ const ranks = pageRank(realNodes, outEdges);
1895
+ const communities = detectCommunities(realNodes, undirected);
1896
+ const metaLookup = /* @__PURE__ */ new Map();
1897
+ const sideEffectsByFile = /* @__PURE__ */ new Map();
1898
+ const writeIntentsByFile = /* @__PURE__ */ new Map();
1899
+ for (const w of work) {
1900
+ const effects = inferSideEffectProfile(w.source, w.importSpecs, w.productDomain, w.frameworkRole);
1901
+ sideEffectsByFile.set(w.rel, effects);
1902
+ writeIntentsByFile.set(w.rel, inferWriteIntents(w.productDomain, w.rel, effects));
1903
+ metaLookup.set(w.rel, { frameworkRole: w.frameworkRole, productDomain: w.productDomain });
1904
+ }
1905
+ const riskTypesByFile = /* @__PURE__ */ new Map();
1906
+ const gravityByFile = /* @__PURE__ */ new Map();
1907
+ const heatByFile = /* @__PURE__ */ new Map();
1908
+ const fanInByFile = /* @__PURE__ */ new Map();
1909
+ const centralityByFile = /* @__PURE__ */ new Map();
1910
+ const gravitySignalsByFile = /* @__PURE__ */ new Map();
1911
+ for (const w of work) {
1912
+ const real = isRealSource.get(w.rel);
1913
+ const fanIn = [...importedBy.get(w.rel) || []].filter((src) => isRealSource.get(src)).length;
1914
+ const centrality = real ? ranks.get(w.rel) || 0 : 0;
1915
+ const gs = {
1916
+ fanIn,
1917
+ fanOut: fanOut.get(w.rel) || 0,
1918
+ centrality,
1919
+ cyclomatic: w.ast.cyclomatic,
1920
+ publicSurface: w.ast.publicSurface,
1921
+ loc: w.ast.loc
1500
1922
  };
1501
- walk2(root);
1502
- return out;
1923
+ const depthRatio = (w.ast.cyclomatic + w.ast.maxNesting * 2) / Math.max(1, w.ast.publicSurface);
1924
+ const depthFactor = Math.min(1, Math.log2(depthRatio + 1) / 3);
1925
+ const adjustedCentrality = centrality * (0.3 + 0.7 * depthFactor);
1926
+ let gravityRaw = adjustedCentrality * 50 + Math.log2(fanIn + 1) * 6 + Math.log2(w.ast.cyclomatic + 1) * 7 + Math.log2(w.ast.publicSurface + 1) * 2 + (w.ast.maxNesting >= 4 ? 5 : 0);
1927
+ if (!real)
1928
+ gravityRaw *= 0.2;
1929
+ const gravity = Math.max(0, Math.min(100, gravityRaw));
1930
+ const heat = real ? computeHeat(w.ast.smells) : 0;
1931
+ gravityByFile.set(w.rel, gravity);
1932
+ heatByFile.set(w.rel, heat);
1933
+ fanInByFile.set(w.rel, fanIn);
1934
+ centralityByFile.set(w.rel, centrality);
1935
+ gravitySignalsByFile.set(w.rel, gs);
1503
1936
  }
1504
- const walk = (n) => {
1505
- if (n.type === "export_statement") {
1506
- const decl = n.childForFieldName("declaration");
1507
- if (decl) {
1508
- const name = decl.childForFieldName("name")?.text;
1509
- if (name)
1510
- push(name, decl);
1511
- for (const c of decl.namedChildren) {
1512
- const dn = c.childForFieldName("name")?.text;
1513
- if (dn)
1514
- push(dn, c);
1515
- }
1516
- }
1517
- for (const spec of n.descendantsOfType("export_specifier")) {
1518
- push(spec.childForFieldName("name")?.text, spec);
1937
+ for (const w of work) {
1938
+ const gs = gravitySignalsByFile.get(w.rel);
1939
+ const smellKinds = new Set(w.ast.smells.map((s) => s.kind));
1940
+ const effects = sideEffectsByFile.get(w.rel);
1941
+ const types = inferRiskTypesPass1(w.rel, w.frameworkRole, w.productDomain, effects, gs, smellKinds);
1942
+ riskTypesByFile.set(w.rel, types);
1943
+ }
1944
+ for (const w of work) {
1945
+ if (w.productDomain === "forms" && (w.frameworkRole === "component" || w.frameworkRole === "hook")) {
1946
+ const importsResolved_w = importsResolved.get(w.rel) || /* @__PURE__ */ new Set();
1947
+ const importsAny = [...importsResolved_w, ...w.importSpecs.filter((s) => s.startsWith("@"))];
1948
+ const consumesBottleneck = importsAny.some((dep) => {
1949
+ const types2 = riskTypesByFile.get(dep);
1950
+ return types2?.includes("registry_bottleneck");
1951
+ });
1952
+ if (consumesBottleneck) {
1953
+ const existing = riskTypesByFile.get(w.rel);
1954
+ if (!existing.includes("registry_consumer"))
1955
+ existing.push("registry_consumer");
1956
+ if (!existing.includes("type_boundary_leak"))
1957
+ existing.push("type_boundary_leak");
1958
+ const idx = existing.indexOf("complexity_hotspot");
1959
+ if (idx >= 0)
1960
+ existing.splice(idx, 1);
1519
1961
  }
1520
- if (n.text.includes("export default"))
1521
- push("default", n);
1522
1962
  }
1523
- for (const c of n.children)
1524
- walk(c);
1963
+ const types = riskTypesByFile.get(w.rel);
1964
+ if (types.length === 0)
1965
+ types.push("complexity_hotspot");
1966
+ }
1967
+ const classified = [];
1968
+ for (const w of work) {
1969
+ const real = isRealSource.get(w.rel);
1970
+ const fanIn = fanInByFile.get(w.rel);
1971
+ const gravity = gravityByFile.get(w.rel);
1972
+ const heat = heatByFile.get(w.rel);
1973
+ const gs = gravitySignalsByFile.get(w.rel);
1974
+ const effects = sideEffectsByFile.get(w.rel);
1975
+ const writeIntents = writeIntentsByFile.get(w.rel);
1976
+ const riskTypes = riskTypesByFile.get(w.rel);
1977
+ const hs = {
1978
+ todos: w.ast.smells.filter((s) => s.kind === "todo").length,
1979
+ suppressions: w.ast.smells.filter((s) => s.kind === "suppression").length,
1980
+ swallowedCatches: w.ast.swallowedCatches,
1981
+ maxNesting: w.ast.maxNesting,
1982
+ longFunctions: w.ast.longFunctions,
1983
+ magicNumbers: w.ast.magicNumbers
1984
+ };
1985
+ const keywordPillar = matchPillarByImports(w.importSpecs);
1986
+ const pathPillar = matchPillarByPath(w.rel);
1987
+ const pillarHint = real ? keywordPillar || pathPillar || `community-${communities.get(w.rel)}` : null;
1988
+ const importedByReal = [...importedBy.get(w.rel) || []].filter((src) => isRealSource.get(src));
1989
+ const imports = [...importsResolved.get(w.rel) || /* @__PURE__ */ new Set()];
1990
+ const importsUnresolvedArr = [...importsUnresolved.get(w.rel) || /* @__PURE__ */ new Set()];
1991
+ const runtimeEntrypoints = findRuntimeEntrypoints(w.rel, importedBy, metaLookup);
1992
+ const entrypointTraceStatus = deriveEntrypointTraceStatus(w.productDomain, runtimeEntrypoints, importsUnresolvedArr);
1993
+ const smellMaxSeverity = w.ast.smells.length > 0 ? Math.max(...w.ast.smells.map((s) => s.severity)) : 0;
1994
+ const loadBearingScore = computeLoadBearingScore(gravity, heat, fanIn, effects, w.productDomain, smellMaxSeverity, runtimeEntrypoints);
1995
+ classified.push({
1996
+ rel: w.rel,
1997
+ abs: w.abs,
1998
+ lang: w.lang,
1999
+ isRealSource: real,
2000
+ demoteReason: demoteReason.get(w.rel) || null,
2001
+ gravity,
2002
+ heat,
2003
+ gravitySignals: gs,
2004
+ heatSignals: hs,
2005
+ smells: w.ast.smells,
2006
+ pillarHint,
2007
+ importedBy: importedByReal,
2008
+ imports,
2009
+ importsUnresolved: importsUnresolvedArr,
2010
+ frameworkRole: w.frameworkRole,
2011
+ productDomain: w.productDomain,
2012
+ sideEffectProfile: effects,
2013
+ writeIntents,
2014
+ riskTypes,
2015
+ runtimeEntrypoints,
2016
+ entrypointTraceStatus,
2017
+ blockedImports: importsUnresolvedArr,
2018
+ loadBearingScore,
2019
+ hotSpans: w.ast.hotSpans,
2020
+ source: w.source
2021
+ });
2022
+ }
2023
+ const dir = join6(projectRoot, ".vibe-splainer");
2024
+ await mkdir5(dir, { recursive: true });
2025
+ const stage05 = Object.fromEntries(classified.map((f) => [f.rel, f.sideEffectProfile]));
2026
+ await writeFile6(join6(dir, "stage-05-side-effects.json"), JSON.stringify(stage05, null, 2), "utf8");
2027
+ const stage06 = Object.fromEntries(classified.map((f) => [f.rel, f.writeIntents]));
2028
+ await writeFile6(join6(dir, "stage-06-write-intents.json"), JSON.stringify(stage06, null, 2), "utf8");
2029
+ const stage07 = Object.fromEntries(classified.map((f) => [f.rel, f.riskTypes]));
2030
+ await writeFile6(join6(dir, "stage-07-risk-types.json"), JSON.stringify(stage07, null, 2), "utf8");
2031
+ const stage08 = Object.fromEntries(classified.map((f) => [f.rel, {
2032
+ isLoadBearing: f.loadBearingScore >= 5,
2033
+ loadBearingScore: f.loadBearingScore,
2034
+ runtimeEntrypoints: f.runtimeEntrypoints.length,
2035
+ entrypointTraceStatus: f.entrypointTraceStatus
2036
+ }]));
2037
+ await writeFile6(join6(dir, "stage-08-load-bearing.json"), JSON.stringify(stage08, null, 2), "utf8");
2038
+ const realClassified = classified.filter((f) => f.isRealSource).sort((a, b) => b.gravity - a.gravity);
2039
+ const wildCandidates = realClassified.filter((f) => f.heat >= 60 || f.smells.some((s) => s.severity >= 4));
2040
+ const pillars = buildPillars(classified, communities);
2041
+ const map = {
2042
+ stack: inv.stack,
2043
+ entrypoints: [...entrypoints],
2044
+ pillars,
2045
+ fileCount: work.length,
2046
+ realSourceCount: realClassified.length,
2047
+ topGravity: realClassified.slice(0, 12).map((f) => f.rel),
2048
+ topHeat: wildCandidates.slice(0, 12).map((f) => f.rel),
2049
+ brief: null
1525
2050
  };
1526
- walk(root);
1527
- return out;
2051
+ return { projectRoot, classified, stack: inv.stack, entrypoints, map, communities };
1528
2052
  }
1529
- function pageRank(nodes, outEdges, damping = 0.85, iters = 20) {
1530
- const n = nodes.length;
1531
- const rank = /* @__PURE__ */ new Map();
1532
- if (n === 0)
1533
- return rank;
1534
- for (const node of nodes)
1535
- rank.set(node, 1 / n);
1536
- const inEdges = /* @__PURE__ */ new Map();
1537
- for (const node of nodes)
1538
- inEdges.set(node, []);
1539
- const outCount = /* @__PURE__ */ new Map();
1540
- for (const [from, tos] of outEdges) {
1541
- const valid = [...tos].filter((t) => rank.has(t));
1542
- outCount.set(from, valid.length);
1543
- for (const to of valid)
1544
- inEdges.get(to).push(from);
2053
+
2054
+ // ../brain/dist/pipeline/scoring.js
2055
+ import { join as join7 } from "path";
2056
+ import { writeFile as writeFile7, mkdir as mkdir6, readFile as readFile6 } from "fs/promises";
2057
+ import { createHash } from "crypto";
2058
+ function computeSeverity(sideEffectProfile, productDomain, gravity, heat, maxNesting, hasLongFunctions, swallowedCatches, runtimeEntrypoints) {
2059
+ let score = 0;
2060
+ if (sideEffectProfile.includes("database_write"))
2061
+ score += 3;
2062
+ if (sideEffectProfile.includes("booking_mutation"))
2063
+ score += 4;
2064
+ if (sideEffectProfile.includes("payment_mutation"))
2065
+ score += 4;
2066
+ if (sideEffectProfile.includes("auth_token_mutation"))
2067
+ score += 4;
2068
+ if (sideEffectProfile.includes("webhook_delivery"))
2069
+ score += 3;
2070
+ if (sideEffectProfile.includes("webhook_ingress"))
2071
+ score += 3;
2072
+ if (sideEffectProfile.includes("calendar_mutation"))
2073
+ score += 3;
2074
+ if (productDomain === "booking_creation")
2075
+ score += 3;
2076
+ if (productDomain === "payments" || productDomain === "payments_webhooks")
2077
+ score += 3;
2078
+ if (productDomain === "auth_oauth")
2079
+ score += 3;
2080
+ if (productDomain === "webhooks")
2081
+ score += 2;
2082
+ if (gravity >= 85)
2083
+ score += 2;
2084
+ if (heat >= 70)
2085
+ score += 2;
2086
+ if (maxNesting >= 4)
2087
+ score += 1;
2088
+ if (hasLongFunctions)
2089
+ score += 1;
2090
+ if (swallowedCatches >= 1)
2091
+ score += 1;
2092
+ if (runtimeEntrypoints.length >= 2)
2093
+ score += 2;
2094
+ if (score >= 10)
2095
+ return 5;
2096
+ if (score >= 7)
2097
+ return 4;
2098
+ if (score >= 4)
2099
+ return 3;
2100
+ if (score >= 2)
2101
+ return 2;
2102
+ return 1;
2103
+ }
2104
+ function applyCorrections(file) {
2105
+ if (file.writeIntents.includes("handle_payment_webhook")) {
2106
+ if (!file.sideEffectProfile.includes("payment_mutation"))
2107
+ file.sideEffectProfile.push("payment_mutation");
2108
+ if (!file.sideEffectProfile.includes("webhook_ingress"))
2109
+ file.sideEffectProfile.push("webhook_ingress");
2110
+ file.sideEffectProfile = file.sideEffectProfile.filter((s) => s !== "none_detected");
1545
2111
  }
1546
- for (let it = 0; it < iters; it++) {
1547
- const next = /* @__PURE__ */ new Map();
1548
- let dangling = 0;
1549
- for (const node of nodes) {
1550
- if ((outCount.get(node) || 0) === 0)
1551
- dangling += rank.get(node);
1552
- }
1553
- for (const node of nodes) {
1554
- let sum = 0;
1555
- for (const from of inEdges.get(node)) {
1556
- sum += rank.get(from) / (outCount.get(from) || 1);
1557
- }
1558
- next.set(node, (1 - damping) / n + damping * (sum + dangling / n));
1559
- }
1560
- for (const node of nodes)
1561
- rank.set(node, next.get(node));
2112
+ if (file.sideEffectProfile.includes("payment_mutation") || file.sideEffectProfile.includes("booking_mutation")) {
2113
+ if (file.canonicalSeverity < 4)
2114
+ file.canonicalSeverity = 4;
2115
+ }
2116
+ if (file.canonicalSeverity === 5)
2117
+ file.canonicalLoadBearing = true;
2118
+ if (file.riskTypes.includes("registry_bottleneck")) {
2119
+ if (file.canonicalSeverity < 4)
2120
+ file.canonicalSeverity = 4;
2121
+ file.canonicalLoadBearing = true;
1562
2122
  }
1563
- let max = 0;
1564
- for (const v of rank.values())
1565
- max = Math.max(max, v);
1566
- if (max > 0)
1567
- for (const node of nodes)
1568
- rank.set(node, rank.get(node) / max);
1569
- return rank;
1570
2123
  }
1571
- function detectCommunities(nodes, adjacency) {
1572
- const label = /* @__PURE__ */ new Map();
1573
- nodes.forEach((node, i) => label.set(node, i));
1574
- const order = [...nodes];
1575
- for (let pass = 0; pass < 10; pass++) {
1576
- let changed = false;
1577
- for (const node of order) {
1578
- const neighbors = adjacency.get(node);
1579
- if (!neighbors || neighbors.size === 0)
1580
- continue;
1581
- const counts = /* @__PURE__ */ new Map();
1582
- for (const [nb, weight] of neighbors) {
1583
- const l = label.get(nb);
1584
- counts.set(l, (counts.get(l) || 0) + weight);
1585
- }
1586
- let best = label.get(node), bestCount = -1;
1587
- for (const [l, c] of counts) {
1588
- if (c > bestCount || c === bestCount && l < best) {
1589
- best = l;
1590
- bestCount = c;
1591
- }
1592
- }
1593
- if (best !== label.get(node)) {
1594
- label.set(node, best);
1595
- changed = true;
1596
- }
1597
- }
1598
- if (!changed)
1599
- break;
2124
+ function inferObservableOutputs(frameworkRole, productDomain, sideEffectProfile) {
2125
+ const outputs = [];
2126
+ const ENTRYPOINT_ROLES2 = /* @__PURE__ */ new Set(["app_route_page", "app_route_handler", "pages_route", "pages_api_route", "trpc_api_route"]);
2127
+ if (sideEffectProfile.includes("redirect"))
2128
+ outputs.push("redirect_url");
2129
+ if (ENTRYPOINT_ROLES2.has(frameworkRole))
2130
+ outputs.push("http_status");
2131
+ if (frameworkRole === "app_route_handler" || frameworkRole === "pages_api_route") {
2132
+ outputs.push("json_response_shape");
2133
+ }
2134
+ if (productDomain === "booking_creation" || productDomain === "booking_management")
2135
+ outputs.push("booking_uid");
2136
+ if (productDomain === "payments" || productDomain === "payments_webhooks")
2137
+ outputs.push("payment_status");
2138
+ if (productDomain === "auth_oauth")
2139
+ outputs.push("auth_token");
2140
+ if (sideEffectProfile.includes("webhook_delivery") || sideEffectProfile.includes("webhook_ingress")) {
2141
+ outputs.push("webhook_payload");
1600
2142
  }
1601
- return label;
2143
+ if (sideEffectProfile.includes("calendar_mutation"))
2144
+ outputs.push("calendar_event_id");
2145
+ if (sideEffectProfile.includes("email_send"))
2146
+ outputs.push("email_payload");
2147
+ if (sideEffectProfile.includes("analytics_event"))
2148
+ outputs.push("sdk_event_name");
2149
+ if (frameworkRole === "hook" || frameworkRole === "store")
2150
+ outputs.push("ui_state_transition");
2151
+ if (productDomain === "data_table" && frameworkRole === "provider") {
2152
+ outputs.push("ui_state_transition", "filter_state", "selected_segment");
2153
+ }
2154
+ return [...new Set(outputs)];
1602
2155
  }
1603
- async function detectStackAndEntrypoints(projectRoot, files) {
1604
- const stack = /* @__PURE__ */ new Set();
1605
- const entrypoints = /* @__PURE__ */ new Set();
1606
- const rel = (abs) => relative(projectRoot, abs);
1607
- const pkgPath = join4(projectRoot, "package.json");
1608
- if (existsSync2(pkgPath)) {
1609
- try {
1610
- const pkg = JSON.parse(await readFile4(pkgPath, "utf8"));
1611
- stack.add("Node.js");
1612
- const deps = { ...pkg.dependencies || {}, ...pkg.devDependencies || {} };
1613
- for (const known of ["react", "next", "vue", "svelte", "express", "fastify", "typescript", "vite"]) {
1614
- if (deps[known])
1615
- stack.add(known === "next" ? "Next.js" : known[0].toUpperCase() + known.slice(1));
1616
- }
1617
- const addEntry = (p) => {
1618
- if (!p)
1619
- return;
1620
- const abs = join4(projectRoot, p);
1621
- const r = relative(projectRoot, abs);
1622
- if (files.includes(abs))
1623
- entrypoints.add(r);
1624
- };
1625
- addEntry(pkg.main);
1626
- if (typeof pkg.bin === "string")
1627
- addEntry(pkg.bin);
1628
- else if (pkg.bin)
1629
- for (const v of Object.values(pkg.bin))
1630
- addEntry(v);
1631
- } catch {
1632
- }
2156
+ function inferPatchRisk(productDomain, riskTypes, sideEffectProfile, importedByCount, loadBearingScore) {
2157
+ if (loadBearingScore >= 12 || productDomain === "booking_creation" && riskTypes.includes("mutation_orchestration")) {
2158
+ return {
2159
+ level: "critical",
2160
+ reason: `${productDomain} domain with ${riskTypes.join(", ")} \u2014 any patch risks breaking live booking, payment, or auth flows.`
2161
+ };
1633
2162
  }
1634
- const pyproject = join4(projectRoot, "pyproject.toml");
1635
- const setupPy = join4(projectRoot, "setup.py");
1636
- const requirements = join4(projectRoot, "requirements.txt");
1637
- if (existsSync2(pyproject) || existsSync2(setupPy) || existsSync2(requirements)) {
1638
- stack.add("Python");
1639
- let reqText = "";
1640
- for (const f of [pyproject, requirements]) {
1641
- if (existsSync2(f)) {
1642
- try {
1643
- reqText += await readFile4(f, "utf8");
1644
- } catch {
1645
- }
1646
- }
1647
- }
1648
- for (const known of ["pygame", "PySide6", "PyQt5", "PyQt6", "flask", "django", "fastapi", "numpy", "pandas", "torch", "tensorflow"]) {
1649
- if (new RegExp(known, "i").test(reqText))
1650
- stack.add(known);
1651
- }
2163
+ if (loadBearingScore >= 8 || sideEffectProfile.includes("payment_mutation") || sideEffectProfile.includes("auth_token_mutation")) {
2164
+ const external = sideEffectProfile.filter((s) => ["payment_mutation", "auth_token_mutation", "database_write", "webhook_delivery"].includes(s));
2165
+ return {
2166
+ level: "high",
2167
+ reason: `${productDomain} writes to external state (${external.join(", ") || "database"}). Changes require integration testing.`
2168
+ };
1652
2169
  }
1653
- if (existsSync2(join4(projectRoot, "go.mod")))
1654
- stack.add("Go");
1655
- if (existsSync2(join4(projectRoot, "Cargo.toml")))
1656
- stack.add("Rust");
1657
- if (existsSync2(join4(projectRoot, "pom.xml")) || existsSync2(join4(projectRoot, "build.gradle")))
1658
- stack.add("Java");
1659
- for (const abs of files) {
1660
- const r = rel(abs);
1661
- const base = basename(r);
1662
- if (base === "main.py" || base === "__main__.py")
1663
- entrypoints.add(r);
1664
- if (/^index\.(ts|tsx|js|jsx|mjs|cjs)$/.test(base) && dirname(r).split(sep).length <= 2)
1665
- entrypoints.add(r);
1666
- if (base === "main.go" && r.includes("cmd" + sep))
1667
- entrypoints.add(r);
1668
- if (base === "main.go" && !r.includes(sep))
1669
- entrypoints.add(r);
1670
- if (base === "main.rs" || base === "lib.rs")
1671
- entrypoints.add(r);
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
+ };
1672
2175
  }
1673
- if (stack.has("Next.js")) {
1674
- const appRouterNames = /* @__PURE__ */ new Set(["page", "layout", "route", "loading", "error", "not-found", "template", "default"]);
1675
- for (const abs of files) {
1676
- const r = rel(abs);
1677
- const stem = basename(r, extname(r));
1678
- const underApp = /(?:^|[/\\])app[/\\]/.test(r);
1679
- const underPages = /(?:^|[/\\])pages[/\\]/.test(r);
1680
- if (underApp && appRouterNames.has(stem))
1681
- entrypoints.add(r);
1682
- if (underPages && !stem.startsWith("_"))
1683
- entrypoints.add(r);
1684
- }
2176
+ if (loadBearingScore >= 5 || importedByCount >= 5) {
2177
+ return { level: "medium", reason: `Imported by ${importedByCount} files. Interface changes will cascade.` };
1685
2178
  }
1686
- return { stack: [...stack], entrypoints };
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
+ }
2185
+ return { level: "low", reason: "Locally contained \u2014 limited blast radius." };
1687
2186
  }
1688
- var SMELL_WEIGHT = {
1689
- "todo": 3,
1690
- "suppression": 5,
1691
- "swallowed-catch": 10,
1692
- "deep-nesting": 6,
1693
- "long-function": 5,
1694
- "magic-number": 3,
1695
- "god-file": 14
1696
- };
1697
- function computeHeat(smells) {
1698
- let sum = 0;
1699
- for (const s of smells)
1700
- sum += s.severity * SMELL_WEIGHT[s.kind];
1701
- return Math.min(100, sum);
2187
+ function inferSafePatchStrategy(riskTypes, sideEffectProfile) {
2188
+ if (riskTypes.includes("mutation_orchestration")) {
2189
+ return "Do not rewrite inline. Extract pure decision logic into a tested reducer or state machine first. Preserve all side-effect call sites (redirect URLs, SDK event names, response shapes) as invariants.";
2190
+ }
2191
+ if (riskTypes.includes("registry_bottleneck")) {
2192
+ return "Add new entries without removing existing keys. Treat the registry map as append-only until all consumers are verified.";
2193
+ }
2194
+ if (riskTypes.includes("registry_consumer")) {
2195
+ return "Verify the registry contract (Components.tsx) before patching. Changes to field types must be reflected in both the registry and all rendering paths.";
2196
+ }
2197
+ if (riskTypes.includes("route_handler_write_path")) {
2198
+ return "Add integration tests covering success and failure paths before modifying. Verify HTTP status codes and response shapes are preserved.";
2199
+ }
2200
+ if (riskTypes.includes("god_component") || riskTypes.includes("god_hook")) {
2201
+ return "Extract sub-concerns into separate modules first. Only refactor the extraction points after tests confirm equivalence.";
2202
+ }
2203
+ if (sideEffectProfile.includes("database_write")) {
2204
+ return "Wrap changes in a transaction or use a feature flag. Run against a staging database before production.";
2205
+ }
2206
+ return "Review importedBy before patching. Run affected integration tests.";
1702
2207
  }
1703
- async function scanProject(projectRoot) {
1704
- await initParser();
1705
- const abs = [];
1706
- await collectFiles(projectRoot, projectRoot, abs);
1707
- const fileSet = new Set(abs.map((f) => relative(projectRoot, f)));
1708
- const basenameIndex = /* @__PURE__ */ new Map();
1709
- for (const rel of fileSet) {
1710
- const b = basename(rel).slice(0, basename(rel).length - extname(rel).length);
1711
- if (!basenameIndex.has(b))
1712
- basenameIndex.set(b, []);
1713
- basenameIndex.get(b).push(rel);
2208
+ function inferDoNotTouch(sideEffectProfile, productDomain) {
2209
+ const items = [];
2210
+ if (sideEffectProfile.includes("payment_mutation"))
2211
+ items.push("payment flow branch");
2212
+ if (sideEffectProfile.includes("auth_token_mutation"))
2213
+ items.push("token issuance / refresh branch");
2214
+ if (sideEffectProfile.includes("webhook_delivery") || sideEffectProfile.includes("webhook_ingress")) {
2215
+ items.push("webhook payload shape");
1714
2216
  }
1715
- const { stack, entrypoints } = await detectStackAndEntrypoints(projectRoot, abs);
1716
- const work = [];
1717
- const graph = { nodes: {}, edges: [] };
1718
- for (const file of abs) {
1719
- const rel = relative(projectRoot, file);
1720
- const ext = extname(file);
1721
- const lang = EXT_LANG[ext];
1722
- if (!lang)
1723
- continue;
1724
- let source;
1725
- try {
1726
- source = await readFile4(file, "utf8");
1727
- } catch {
1728
- continue;
1729
- }
1730
- if (/if\s+__name__\s*==\s*['"]__main__['"]/.test(source) || /^#![^\n]*\b(node|python\d?)\b/.test(source)) {
1731
- entrypoints.add(rel);
1732
- }
1733
- const tree = await parseAs(lang, source);
1734
- if (!tree)
1735
- continue;
1736
- const ast = analyzeAst(source, lang, tree);
1737
- const importSpecs = extractImports(source, lang);
1738
- graph.nodes[rel] = { imports: importSpecs };
1739
- const frameworkRole = inferFrameworkRole(rel);
1740
- const productDomain = inferProductDomain(rel, importSpecs);
1741
- const sideEffectProfile = inferSideEffectProfile(source, importSpecs);
1742
- work.push({
1743
- abs: file,
1744
- rel,
1745
- lang,
1746
- source,
1747
- ast,
1748
- importSpecs,
1749
- pathDemote: pathDemoteReason(rel),
1750
- frameworkRole,
1751
- productDomain,
1752
- sideEffectProfile
2217
+ if (sideEffectProfile.includes("redirect"))
2218
+ items.push("redirect URL strings");
2219
+ if (sideEffectProfile.includes("analytics_event"))
2220
+ items.push("SDK event names");
2221
+ if (sideEffectProfile.includes("booking_mutation")) {
2222
+ items.push("booking success response shape", "recurring booking branch");
2223
+ }
2224
+ if (productDomain === "auth_oauth")
2225
+ items.push("OAuth callback URLs", "token scopes");
2226
+ return items;
2227
+ }
2228
+ function inferTestProbes(writeIntents, observableOutputs) {
2229
+ const probes = [];
2230
+ if (writeIntents.includes("create_booking")) {
2231
+ probes.push({
2232
+ name: "standard booking success",
2233
+ scenario: "create a standard booking and assert success redirect and booking uid",
2234
+ expectedObservable: ["booking_uid", "redirect_url", "sdk_event_name"].filter((o) => observableOutputs.includes(o))
1753
2235
  });
1754
2236
  }
1755
- const importedBy = /* @__PURE__ */ new Map();
1756
- const importsResolved = /* @__PURE__ */ new Map();
1757
- const importsUnresolved = /* @__PURE__ */ new Map();
1758
- const fanOut = /* @__PURE__ */ new Map();
1759
- for (const w of work) {
1760
- importedBy.set(w.rel, /* @__PURE__ */ new Set());
1761
- importsResolved.set(w.rel, /* @__PURE__ */ new Set());
1762
- importsUnresolved.set(w.rel, /* @__PURE__ */ new Set());
2237
+ if (writeIntents.includes("reschedule_booking")) {
2238
+ probes.push({
2239
+ name: "reschedule booking",
2240
+ scenario: "reschedule an existing booking and assert reschedule event path",
2241
+ expectedObservable: ["booking_uid", "redirect_url"].filter((o) => observableOutputs.includes(o))
2242
+ });
1763
2243
  }
1764
- for (const w of work) {
1765
- const distinctModules = /* @__PURE__ */ new Set();
1766
- for (const spec of w.importSpecs) {
1767
- distinctModules.add(spec);
1768
- const { resolved, isAlias } = resolveImport(spec, w.abs, w.lang, projectRoot, fileSet, basenameIndex);
1769
- if (resolved && resolved !== w.rel && importedBy.has(resolved)) {
1770
- importedBy.get(resolved).add(w.rel);
1771
- importsResolved.get(w.rel).add(resolved);
1772
- graph.edges.push({ from: w.rel, to: resolved });
1773
- } else if (resolved === null && isAlias) {
1774
- importsUnresolved.get(w.rel).add(spec);
1775
- }
2244
+ if (writeIntents.includes("create_recurring_booking")) {
2245
+ probes.push({
2246
+ name: "recurring booking",
2247
+ scenario: "create recurring booking and assert recurring success behavior",
2248
+ expectedObservable: ["booking_uid", "redirect_url"].filter((o) => observableOutputs.includes(o))
2249
+ });
2250
+ }
2251
+ if (writeIntents.includes("handle_payment_webhook")) {
2252
+ probes.push({
2253
+ name: "payment webhook ingestion",
2254
+ scenario: "send a valid payment webhook and assert booking/payment state updated",
2255
+ expectedObservable: ["payment_status", "booking_uid", "http_status"].filter((o) => observableOutputs.includes(o))
2256
+ });
2257
+ }
2258
+ if (writeIntents.includes("issue_auth_token")) {
2259
+ probes.push({
2260
+ name: "token issuance",
2261
+ scenario: "complete OAuth flow and assert access token issued with correct scopes",
2262
+ expectedObservable: ["auth_token", "http_status"].filter((o) => observableOutputs.includes(o))
2263
+ });
2264
+ }
2265
+ return probes;
2266
+ }
2267
+ function deriveConfidence(fanIn, gravity) {
2268
+ if (fanIn >= 10 && gravity >= 40)
2269
+ return "high";
2270
+ if (fanIn >= 5 || gravity >= 25)
2271
+ return "medium";
2272
+ return "low";
2273
+ }
2274
+ async function runScoring(projectRoot, cr) {
2275
+ const dir = join7(projectRoot, ".vibe-splainer");
2276
+ await mkdir6(dir, { recursive: true });
2277
+ const persisted = {};
2278
+ const severityBreakdowns = {};
2279
+ for (const f of cr.classified) {
2280
+ const severity = computeSeverity(f.sideEffectProfile, f.productDomain, f.gravity, f.heat, f.heatSignals.maxNesting, f.heatSignals.longFunctions > 0, f.heatSignals.swallowedCatches, f.runtimeEntrypoints);
2281
+ const isLoadBearing = f.loadBearingScore >= 5;
2282
+ const pf = {
2283
+ relativePath: f.rel,
2284
+ language: f.lang,
2285
+ isRealSource: f.isRealSource,
2286
+ demoteReason: f.demoteReason,
2287
+ gravity: Math.round(f.gravity),
2288
+ heat: Math.round(f.heat),
2289
+ gravitySignals: f.gravitySignals,
2290
+ heatSignals: f.heatSignals,
2291
+ smells: f.smells,
2292
+ pillarHint: f.pillarHint,
2293
+ importedBy: f.importedBy,
2294
+ imports: f.imports,
2295
+ importsUnresolved: f.importsUnresolved,
2296
+ frameworkRole: f.frameworkRole,
2297
+ productDomain: f.productDomain,
2298
+ sideEffectProfile: f.sideEffectProfile,
2299
+ hotSpans: f.hotSpans,
2300
+ riskTypes: f.riskTypes,
2301
+ writeIntents: f.writeIntents,
2302
+ canonicalSeverity: severity,
2303
+ canonicalLoadBearing: isLoadBearing
2304
+ };
2305
+ applyCorrections(pf);
2306
+ persisted[f.rel] = pf;
2307
+ severityBreakdowns[f.rel] = `severity=${pf.canonicalSeverity} loadBearing=${pf.canonicalLoadBearing} effects=${pf.sideEffectProfile.join(",")} domain=${pf.productDomain}`;
2308
+ }
2309
+ const stage09 = Object.fromEntries(Object.entries(persisted).filter(([, pf]) => pf.isRealSource).map(([rel, pf]) => [rel, { canonicalSeverity: pf.canonicalSeverity, canonicalLoadBearing: pf.canonicalLoadBearing, scoreBreakdown: severityBreakdowns[rel] }]));
2310
+ await writeFile7(join7(dir, "stage-09-severity.json"), JSON.stringify(stage09, null, 2), "utf8");
2311
+ const store = { files: persisted };
2312
+ const importedByMapForDelta = /* @__PURE__ */ new Map();
2313
+ for (const [rel, pf] of Object.entries(persisted)) {
2314
+ importedByMapForDelta.set(rel, new Set(pf.importedBy));
2315
+ }
2316
+ const metaForDelta = new Map(Object.entries(persisted).map(([rel, pf]) => [rel, { frameworkRole: pf.frameworkRole, productDomain: pf.productDomain }]));
2317
+ const deltaTargets = Object.values(persisted).filter((pf) => pf.isRealSource).sort((a, b) => b.gravity - a.gravity).map((pf) => {
2318
+ const runtimeEntrypoints = findRuntimeEntrypoints(pf.relativePath, importedByMapForDelta, metaForDelta);
2319
+ const entrypointTraceStatus = deriveEntrypointTraceStatus(pf.productDomain, runtimeEntrypoints, pf.importsUnresolved);
2320
+ const smellMaxSeverity = pf.smells.length > 0 ? Math.max(...pf.smells.map((s) => s.severity)) : 0;
2321
+ const loadBearingScore = computeLoadBearingScore(pf.gravity, pf.heat, pf.importedBy.length, pf.sideEffectProfile, pf.productDomain, smellMaxSeverity, runtimeEntrypoints);
2322
+ const observableOutputs = inferObservableOutputs(pf.frameworkRole, pf.productDomain, pf.sideEffectProfile);
2323
+ const patchRisk = inferPatchRisk(pf.productDomain, pf.riskTypes, pf.sideEffectProfile, pf.importedBy.length, loadBearingScore);
2324
+ const confidence = deriveConfidence(pf.gravitySignals.fanIn, pf.gravity);
2325
+ const fileHashInput = pf.hotSpans.map((h) => h.snippet).join("");
2326
+ const fileHash = createHash("sha256").update(fileHashInput || pf.relativePath).digest("hex").slice(0, 12);
2327
+ const rawEvidence = pf.hotSpans.map((span) => ({
2328
+ file: pf.relativePath,
2329
+ startLine: span.startLine,
2330
+ endLine: span.endLine,
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
2340
+ }));
2341
+ return {
2342
+ path: pf.relativePath,
2343
+ frameworkRole: pf.frameworkRole,
2344
+ productDomain: pf.productDomain,
2345
+ gravity: Math.round(pf.gravity),
2346
+ heat: Math.round(pf.heat),
2347
+ severity: pf.canonicalSeverity,
2348
+ confidence,
2349
+ isLoadBearing: pf.canonicalLoadBearing || loadBearingScore >= 5,
2350
+ loadBearingScore,
2351
+ riskTypes: pf.riskTypes,
2352
+ sideEffectProfile: pf.sideEffectProfile,
2353
+ blastRadius: pf.importedBy,
2354
+ runtimeEntrypoints,
2355
+ entrypointTraceStatus,
2356
+ blockedImports: pf.importsUnresolved,
2357
+ observableOutputs,
2358
+ writeIntents: pf.writeIntents,
2359
+ patchRisk,
2360
+ safePatchStrategy: inferSafePatchStrategy(pf.riskTypes, pf.sideEffectProfile),
2361
+ doNotTouch: inferDoNotTouch(pf.sideEffectProfile, pf.productDomain),
2362
+ testProbes: inferTestProbes(pf.writeIntents, observableOutputs),
2363
+ rawEvidence,
2364
+ displayEvidence,
2365
+ analysisAnnotation: `${pf.frameworkRole} in ${pf.productDomain} domain. fanIn=${pf.gravitySignals.fanIn} cyclomatic=${pf.gravitySignals.cyclomatic} loc=${pf.gravitySignals.loc}`,
2366
+ hashes: { fileHash, evidenceHash: rawEvidence.map((e) => e.evidenceHash).join("-") }
2367
+ };
2368
+ });
2369
+ const dest = join7(dir, "delta_targets.json");
2370
+ const tmp = dest + ".tmp";
2371
+ await writeFile7(tmp, JSON.stringify(deltaTargets, null, 2), "utf8");
2372
+ const { rename } = await import("fs/promises");
2373
+ await rename(tmp, dest);
2374
+ const validationReport = await buildValidationReport(store, deltaTargets, projectRoot);
2375
+ await writeFile7(join7(dir, "validation_report.json"), JSON.stringify(validationReport, null, 2), "utf8");
2376
+ for (const e of validationReport.errors) {
2377
+ console.error(`[vibe-splain] VALIDATION ERROR [${e.rule}] ${e.file}: ${e.detail}`);
2378
+ }
2379
+ for (const w of validationReport.warnings) {
2380
+ console.error(`[vibe-splain] VALIDATION WARN [${w.rule}] ${w.file}: ${w.detail}`);
2381
+ }
2382
+ return { store, deltaTargets, validationReport };
2383
+ }
2384
+ async function buildValidationReport(store, deltaTargets, projectRoot) {
2385
+ const errors = [];
2386
+ const warnings = [];
2387
+ let passCount = 0;
2388
+ const deltaByPath = new Map(deltaTargets.map((d) => [d.path, d]));
2389
+ for (const [, pf] of Object.entries(store.files)) {
2390
+ if (!pf.isRealSource)
2391
+ continue;
2392
+ const delta = deltaByPath.get(pf.relativePath);
2393
+ if (pf.canonicalSeverity === 5 && !pf.canonicalLoadBearing) {
2394
+ errors.push({
2395
+ file: pf.relativePath,
2396
+ rule: "severity_5_not_load_bearing",
2397
+ detail: "severity=5 but canonicalLoadBearing=false \u2014 post-correction invariant violated",
2398
+ expected: "canonicalLoadBearing=true",
2399
+ actual: "canonicalLoadBearing=false"
2400
+ });
2401
+ continue;
1776
2402
  }
1777
- fanOut.set(w.rel, distinctModules.size);
1778
- }
1779
- const isRealSource = /* @__PURE__ */ new Map();
1780
- const demoteReason = /* @__PURE__ */ new Map();
1781
- for (const w of work) {
1782
- if (w.pathDemote) {
1783
- isRealSource.set(w.rel, false);
1784
- demoteReason.set(w.rel, w.pathDemote);
1785
- } else {
1786
- isRealSource.set(w.rel, true);
1787
- demoteReason.set(w.rel, null);
2403
+ if (pf.writeIntents.includes("handle_payment_webhook") && pf.sideEffectProfile.includes("none_detected")) {
2404
+ errors.push({
2405
+ file: pf.relativePath,
2406
+ rule: "payment_webhook_no_effects",
2407
+ detail: "writeIntents includes handle_payment_webhook but sideEffectProfile is none_detected",
2408
+ expected: "payment_mutation + webhook_ingress",
2409
+ actual: "none_detected"
2410
+ });
2411
+ continue;
1788
2412
  }
1789
- }
1790
- for (const w of work) {
1791
- if (!isRealSource.get(w.rel))
2413
+ if (pf.productDomain === "booking_creation" && delta?.entrypointTraceStatus === "no_runtime_entrypoint_found" && pf.importsUnresolved.length === 0) {
2414
+ errors.push({
2415
+ file: pf.relativePath,
2416
+ rule: "booking_creation_no_entrypoint_no_blockers",
2417
+ detail: "booking_creation domain with no entrypoint found and no blocked imports \u2014 classification may be wrong"
2418
+ });
1792
2419
  continue;
1793
- if (entrypoints.has(w.rel))
2420
+ }
2421
+ if (delta && delta.severity !== pf.canonicalSeverity) {
2422
+ errors.push({
2423
+ file: pf.relativePath,
2424
+ rule: "severity_mismatch_delta",
2425
+ detail: "DeltaTarget severity does not match canonicalSeverity",
2426
+ expected: String(pf.canonicalSeverity),
2427
+ actual: String(delta.severity)
2428
+ });
1794
2429
  continue;
1795
- const inbound = [...importedBy.get(w.rel)].filter((src) => isRealSource.get(src));
1796
- if (inbound.length === 0) {
1797
- isRealSource.set(w.rel, false);
1798
- demoteReason.set(w.rel, "no inbound references from application code");
1799
2430
  }
1800
- }
1801
- const realNodes = work.filter((w) => isRealSource.get(w.rel)).map((w) => w.rel);
1802
- const realSet = new Set(realNodes);
1803
- const outEdges = /* @__PURE__ */ new Map();
1804
- const undirected = /* @__PURE__ */ new Map();
1805
- for (const node of realNodes) {
1806
- outEdges.set(node, /* @__PURE__ */ new Set());
1807
- undirected.set(node, /* @__PURE__ */ new Map());
1808
- }
1809
- for (const w of work) {
1810
- if (!realSet.has(w.rel))
2431
+ if (pf.canonicalSeverity >= 4 && (delta?.rawEvidence.length ?? 0) === 0 && pf.hotSpans.length === 0) {
2432
+ errors.push({
2433
+ file: pf.relativePath,
2434
+ rule: "high_severity_no_evidence",
2435
+ detail: `severity=${pf.canonicalSeverity} but rawEvidence is empty`
2436
+ });
1811
2437
  continue;
1812
- for (const target of importsResolved.get(w.rel)) {
1813
- if (!realSet.has(target))
1814
- continue;
1815
- outEdges.get(w.rel).add(target);
1816
- const wDir = w.rel.split(sep)[0];
1817
- const tDir = target.split(sep)[0];
1818
- const weight = wDir === tDir ? 1 : 0.5;
1819
- undirected.get(w.rel).set(target, weight);
1820
- undirected.get(target).set(w.rel, weight);
1821
2438
  }
1822
- }
1823
- const ranks = pageRank(realNodes, outEdges);
1824
- const communities = detectCommunities(realNodes, undirected);
1825
- const analyses = [];
1826
- const persisted = {};
1827
- for (const w of work) {
1828
- const real = isRealSource.get(w.rel);
1829
- const fanIn = [...importedBy.get(w.rel)].filter((src) => isRealSource.get(src)).length;
1830
- const centrality = real ? ranks.get(w.rel) || 0 : 0;
1831
- const gravitySignals = {
1832
- fanIn,
1833
- fanOut: fanOut.get(w.rel) || 0,
1834
- centrality,
1835
- cyclomatic: w.ast.cyclomatic,
1836
- publicSurface: w.ast.publicSurface,
1837
- loc: w.ast.loc
1838
- };
1839
- const depthRatio = (w.ast.cyclomatic + w.ast.maxNesting * 2) / Math.max(1, w.ast.publicSurface);
1840
- const depthFactor = Math.min(1, Math.log2(depthRatio + 1) / 3);
1841
- const adjustedCentrality = centrality * (0.3 + 0.7 * depthFactor);
1842
- let gravityRaw = adjustedCentrality * 50 + Math.log2(fanIn + 1) * 6 + Math.log2(w.ast.cyclomatic + 1) * 7 + Math.log2(w.ast.publicSurface + 1) * 2 + (w.ast.maxNesting >= 4 ? 5 : 0);
1843
- if (!real)
1844
- gravityRaw *= 0.2;
1845
- const gravity = Math.max(0, Math.min(100, gravityRaw));
1846
- const heatSignals = {
1847
- todos: w.ast.smells.filter((s) => s.kind === "todo").length,
1848
- suppressions: w.ast.smells.filter((s) => s.kind === "suppression").length,
1849
- swallowedCatches: w.ast.swallowedCatches,
1850
- maxNesting: w.ast.maxNesting,
1851
- longFunctions: w.ast.longFunctions,
1852
- magicNumbers: w.ast.magicNumbers
1853
- };
1854
- const heat = real ? computeHeat(w.ast.smells) : 0;
1855
- const keywordPillar = matchPillarByImports(w.importSpecs);
1856
- const pathPillar = matchPillarByPath(w.rel);
1857
- const pillarHint = real ? keywordPillar || pathPillar || `community-${communities.get(w.rel)}` : null;
1858
- const fa = {
1859
- path: w.abs,
1860
- relativePath: w.rel,
1861
- language: w.lang,
1862
- isRealSource: real,
1863
- demoteReason: demoteReason.get(w.rel) || null,
1864
- gravity,
1865
- heat,
1866
- gravitySignals,
1867
- heatSignals,
1868
- smells: w.ast.smells,
1869
- pillarHint,
1870
- frameworkRole: w.frameworkRole,
1871
- productDomain: w.productDomain,
1872
- sideEffectProfile: w.sideEffectProfile
1873
- };
1874
- analyses.push(fa);
1875
- persisted[w.rel] = {
1876
- relativePath: w.rel,
1877
- language: w.lang,
1878
- isRealSource: real,
1879
- demoteReason: demoteReason.get(w.rel) || null,
1880
- gravity,
1881
- heat,
1882
- gravitySignals,
1883
- heatSignals,
1884
- smells: w.ast.smells,
1885
- pillarHint,
1886
- importedBy: [...importedBy.get(w.rel)].filter((src) => isRealSource.get(src)),
1887
- imports: [...importsResolved.get(w.rel)],
1888
- importsUnresolved: [...importsUnresolved.get(w.rel)],
1889
- frameworkRole: w.frameworkRole,
1890
- productDomain: w.productDomain,
1891
- sideEffectProfile: w.sideEffectProfile,
1892
- hotSpans: w.ast.hotSpans
1893
- };
1894
- }
1895
- const realAnalyses = analyses.filter((a) => a.isRealSource).sort((a, b) => b.gravity - a.gravity);
1896
- const wildCandidates = realAnalyses.filter((a) => a.heat >= 60 || a.smells.some((s) => s.severity >= 4)).sort((a, b) => b.heat - a.heat);
1897
- const pillars = buildPillars(realAnalyses, persisted, communities, stack);
1898
- const topGravity = realAnalyses.slice(0, 12).map((a) => a.relativePath);
1899
- const topHeat = wildCandidates.slice(0, 12).map((a) => a.relativePath);
1900
- const map = {
1901
- stack,
1902
- entrypoints: [...entrypoints],
1903
- pillars,
1904
- fileCount: work.length,
1905
- realSourceCount: realAnalyses.length,
1906
- topGravity,
1907
- topHeat,
1908
- brief: null
1909
- };
1910
- await writeGraph(projectRoot, graph);
1911
- const analysisStore = { files: persisted };
1912
- await writeAnalysis(projectRoot, analysisStore);
1913
- await writeDeltaTargets(projectRoot, analysisStore, entrypoints);
1914
- const uiUrl = `file://${join4(projectRoot, ".vibe-splainer", "ui", "index.html")}`;
1915
- return {
1916
- projectRoot,
1917
- totalFilesScanned: work.length,
1918
- realSourceCount: realAnalyses.length,
1919
- files: realAnalyses,
1920
- map,
1921
- wildCandidates,
1922
- uiUrl,
1923
- graph
1924
- };
1925
- }
1926
- function buildPillars(real, persisted, communities, _stack) {
1927
- const keywordGroups = /* @__PURE__ */ new Map();
1928
- const unlabeled = [];
1929
- for (const a of real) {
1930
- if (a.pillarHint && !a.pillarHint.startsWith("community-")) {
1931
- if (!keywordGroups.has(a.pillarHint))
1932
- keywordGroups.set(a.pillarHint, []);
1933
- keywordGroups.get(a.pillarHint).push(a);
1934
- } else {
1935
- unlabeled.push(a);
2439
+ if (pf.canonicalSeverity >= 4 && (delta?.runtimeEntrypoints.length ?? 0) === 0) {
2440
+ warnings.push({
2441
+ file: pf.relativePath,
2442
+ rule: "high_severity_no_entrypoints",
2443
+ detail: `severity=${pf.canonicalSeverity} but no runtime entrypoints found \u2014 check alias resolution`
2444
+ });
1936
2445
  }
1937
- }
1938
- const pillars = [];
1939
- for (const [name, files] of keywordGroups) {
1940
- const sorted = [...files].sort((a, b) => b.gravity - a.gravity);
1941
- pillars.push({
1942
- name,
1943
- description: `${name} subsystem: ${files.length} file${files.length > 1 ? "s" : ""} centered on ${basename(sorted[0].relativePath)}.`,
1944
- memberFiles: sorted.map((f) => f.relativePath)
1945
- });
1946
- }
1947
- if (unlabeled.length > 0) {
1948
- const communityGroups = /* @__PURE__ */ new Map();
1949
- for (const a of unlabeled) {
1950
- const c = communities.get(a.relativePath);
1951
- if (c === void 0)
1952
- continue;
1953
- if (!communityGroups.has(c))
1954
- communityGroups.set(c, []);
1955
- communityGroups.get(c).push(a);
2446
+ if (delta?.entrypointTraceStatus === "partial_wrong_surface") {
2447
+ const foundPaths = delta.runtimeEntrypoints.map((e) => e.path).join(", ");
2448
+ warnings.push({
2449
+ file: pf.relativePath,
2450
+ rule: "partial_wrong_surface",
2451
+ detail: `Entrypoints found but domain surface mismatch for ${pf.productDomain}. Found: ${foundPaths}`
2452
+ });
1956
2453
  }
1957
- const remainingSlots = Math.max(0, 6 - pillars.length);
1958
- const sorted = [...communityGroups.entries()].map(([id, files]) => ({ id, files, weight: files.reduce((s, f) => s + f.gravity, 0) })).filter((g) => g.files.length >= 2).sort((a, b) => b.weight - a.weight).slice(0, remainingSlots);
1959
- for (const g of sorted) {
1960
- const top = [...g.files].sort((a, b) => b.gravity - a.gravity);
1961
- const name = pillarNameFromCluster(top);
1962
- const existing = pillars.find((p) => p.name === name);
1963
- if (existing) {
1964
- existing.memberFiles.push(...top.map((f) => f.relativePath));
1965
- existing.description = `${name} subsystem: ${existing.memberFiles.length} files centered on ${basename(existing.memberFiles[0])}.`;
1966
- } else {
1967
- pillars.push({
1968
- name,
1969
- description: `${g.files.length} files centered on ${basename(top[0].relativePath)}.`,
1970
- memberFiles: top.map((f) => f.relativePath)
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"
1971
2478
  });
1972
- }
1973
2479
  }
1974
- }
1975
- pillars.sort((a, b) => {
1976
- const gravA = real.filter((f) => a.memberFiles.includes(f.relativePath)).reduce((s, f) => s + f.gravity, 0);
1977
- const gravB = real.filter((f) => b.memberFiles.includes(f.relativePath)).reduce((s, f) => s + f.gravity, 0);
1978
- return gravB - gravA;
1979
- });
1980
- if (pillars.length === 0 && real.length > 0) {
1981
- pillars.push({ name: "Core", description: "Primary application code.", memberFiles: real.slice(0, 20).map((f) => f.relativePath) });
1982
- }
1983
- const finalPillars = [];
1984
- for (const p of pillars) {
1985
- if (p.memberFiles.length > 15) {
1986
- const groups = /* @__PURE__ */ new Map();
1987
- for (const f of p.memberFiles) {
1988
- const pf = persisted[f];
1989
- const role = pf?.frameworkRole || "unknown";
1990
- const domain = pf?.productDomain || "unknown";
1991
- let bucket;
1992
- if (domain !== "unknown" && domain !== "routing_infrastructure" && domain !== "test_infrastructure" && domain !== "generated_noise") {
1993
- bucket = domainToGroupLabel(domain);
1994
- } else if (role === "hook") {
1995
- bucket = "Hooks";
1996
- } else if ([
1997
- "app_route_page",
1998
- "app_route_handler",
1999
- "app_route_layout",
2000
- "pages_route",
2001
- "pages_api_route",
2002
- "trpc_api_route"
2003
- ].includes(role)) {
2004
- bucket = "Routes";
2005
- } else if (role === "component") {
2006
- bucket = "Components";
2007
- } else {
2008
- bucket = "Logic";
2009
- }
2010
- const key = `${p.name} (${bucket})`;
2011
- if (!groups.has(key))
2012
- groups.set(key, []);
2013
- groups.get(key).push(f);
2014
- }
2015
- for (const [key, files] of groups) {
2016
- if (files.length > 0) {
2017
- finalPillars.push({
2018
- name: key,
2019
- description: `Subdivided from ${p.name}`,
2020
- memberFiles: files
2021
- });
2022
- }
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
+ }
2487
+ passCount++;
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 {
2023
2514
  }
2024
- } else {
2025
- finalPillars.push(p);
2026
2515
  }
2027
- }
2028
- const seen = /* @__PURE__ */ new Set();
2029
- for (const p of finalPillars) {
2030
- let n = p.name, i = 2;
2031
- while (seen.has(n)) {
2032
- n = `${p.name} ${i++}`;
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 });
2033
2555
  }
2034
- p.name = n;
2035
- seen.add(n);
2036
2556
  }
2037
- return finalPillars;
2038
- }
2039
- function domainToGroupLabel(domain) {
2040
- const labels = {
2041
- booking_creation: "Booking",
2042
- booking_management: "Booking",
2043
- booking_audit: "Booking Audit",
2044
- event_type_configuration: "Event Types",
2045
- availability: "Availability",
2046
- auth: "Auth",
2047
- auth_oauth: "Auth OAuth",
2048
- payments: "Payments",
2049
- payments_webhooks: "Payment Webhooks",
2050
- webhooks: "Webhooks",
2051
- apps_marketplace: "Apps",
2052
- calendar_integrations: "Calendar",
2053
- video: "Video",
2054
- onboarding: "Onboarding",
2055
- settings: "Settings",
2056
- admin: "Admin",
2057
- data_table: "Data Table",
2058
- shell_navigation: "Shell",
2059
- forms: "Forms",
2060
- embed: "Embed",
2061
- notifications: "Notifications"
2557
+ return {
2558
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2559
+ passed: errors.length === 0,
2560
+ errors,
2561
+ warnings,
2562
+ summary: { errorCount: errors.length, warningCount: warnings.length, passCount }
2062
2563
  };
2063
- return labels[domain] || titleCase(domain.replace(/_/g, " "));
2064
2564
  }
2065
- function pillarNameFromCluster(files) {
2066
- const hintCounts = /* @__PURE__ */ new Map();
2067
- for (const f of files) {
2068
- if (f.pillarHint && !f.pillarHint.startsWith("community-")) {
2069
- hintCounts.set(f.pillarHint, (hintCounts.get(f.pillarHint) || 0) + 1);
2070
- }
2071
- }
2072
- if (hintCounts.size > 0) {
2073
- const best = [...hintCounts.entries()].sort((a, b) => b[1] - a[1])[0];
2074
- if (best[1] >= files.length * 0.4)
2075
- return best[0];
2076
- }
2077
- const dirs = files.map((f) => dirname(f.relativePath)).filter((d) => d && d !== ".");
2078
- if (dirs.length) {
2079
- const segCounts = /* @__PURE__ */ new Map();
2080
- for (const d of dirs) {
2081
- const segments = d.split(sep).filter((s) => !MEANINGLESS_SEGMENTS.has(s.toLowerCase()));
2082
- const meaningful = segments.pop();
2083
- if (meaningful)
2084
- segCounts.set(meaningful, (segCounts.get(meaningful) || 0) + 1);
2565
+
2566
+ // ../brain/dist/pipeline/orchestrator.js
2567
+ async function runPipeline(projectRoot) {
2568
+ const inv = await runInventory(projectRoot);
2569
+ const res = await runResolution(projectRoot, inv);
2570
+ const cr = await runClassification(projectRoot, inv, res);
2571
+ const scoring = await runScoring(projectRoot, cr);
2572
+ await writeGraph(projectRoot, res.graph);
2573
+ await writeAnalysis(projectRoot, scoring.store);
2574
+ const files = cr.classified.filter((f) => f.isRealSource).sort((a, b) => b.gravity - a.gravity).map((f) => ({
2575
+ path: f.abs,
2576
+ relativePath: f.rel,
2577
+ language: f.lang,
2578
+ isRealSource: f.isRealSource,
2579
+ demoteReason: f.demoteReason,
2580
+ gravity: Math.round(f.gravity),
2581
+ heat: Math.round(f.heat),
2582
+ gravitySignals: f.gravitySignals,
2583
+ heatSignals: f.heatSignals,
2584
+ smells: f.smells,
2585
+ pillarHint: f.pillarHint,
2586
+ frameworkRole: f.frameworkRole,
2587
+ productDomain: f.productDomain,
2588
+ sideEffectProfile: f.sideEffectProfile
2589
+ }));
2590
+ const wildCandidates = cr.classified.filter((f) => f.isRealSource && (f.heat >= 60 || f.smells.some((s) => s.severity >= 4))).sort((a, b) => b.heat - a.heat).map((f) => ({
2591
+ path: f.abs,
2592
+ relativePath: f.rel,
2593
+ language: f.lang,
2594
+ isRealSource: f.isRealSource,
2595
+ demoteReason: f.demoteReason,
2596
+ gravity: Math.round(f.gravity),
2597
+ heat: Math.round(f.heat),
2598
+ gravitySignals: f.gravitySignals,
2599
+ heatSignals: f.heatSignals,
2600
+ smells: f.smells,
2601
+ pillarHint: f.pillarHint,
2602
+ frameworkRole: f.frameworkRole,
2603
+ productDomain: f.productDomain,
2604
+ sideEffectProfile: f.sideEffectProfile
2605
+ }));
2606
+ const uiUrl = `file://${join8(projectRoot, ".vibe-splainer", "ui", "index.html")}`;
2607
+ return {
2608
+ projectRoot,
2609
+ totalFilesScanned: cr.classified.length,
2610
+ realSourceCount: files.length,
2611
+ files,
2612
+ map: cr.map,
2613
+ wildCandidates,
2614
+ uiUrl,
2615
+ graph: res.graph,
2616
+ validation: {
2617
+ passed: scoring.validationReport.passed,
2618
+ errors: scoring.validationReport.summary.errorCount,
2619
+ warnings: scoring.validationReport.summary.warningCount,
2620
+ reportPath: ".vibe-splainer/validation_report.json"
2085
2621
  }
2086
- const top = [...segCounts.entries()].sort((a, b) => b[1] - a[1])[0];
2087
- if (top)
2088
- return titleCase(top[0]);
2089
- }
2090
- const topFile = basename(files[0].relativePath, extname(files[0].relativePath));
2091
- return titleCase(topFile);
2622
+ };
2092
2623
  }
2093
- function titleCase(s) {
2094
- return s.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
2624
+
2625
+ // ../brain/dist/scanner.js
2626
+ async function initParser2() {
2627
+ return initParser();
2628
+ }
2629
+ async function scanProject(projectRoot) {
2630
+ return runPipeline(projectRoot);
2095
2631
  }
2096
2632
  async function getFileAnalysis(absPath) {
2097
- const ext = extname(absPath);
2633
+ const ext = extname4(absPath);
2098
2634
  const lang = EXT_LANG[ext];
2099
2635
  if (!lang)
2100
2636
  return null;
2101
2637
  let source;
2102
2638
  try {
2103
- source = await readFile4(absPath, "utf8");
2639
+ source = await readFile7(absPath, "utf8");
2104
2640
  } catch {
2105
2641
  return null;
2106
2642
  }
@@ -2119,20 +2655,19 @@ async function getFileAnalysis(absPath) {
2119
2655
  reason: `${s.kind}: ${s.note}`
2120
2656
  };
2121
2657
  });
2122
- const heatSignals = {
2123
- todos: ast.smells.filter((s) => s.kind === "todo").length,
2124
- suppressions: ast.smells.filter((s) => s.kind === "suppression").length,
2125
- swallowedCatches: ast.swallowedCatches,
2126
- maxNesting: ast.maxNesting,
2127
- longFunctions: ast.longFunctions,
2128
- magicNumbers: ast.magicNumbers
2129
- };
2130
2658
  return {
2131
2659
  language: lang,
2132
2660
  signature: ast.signature,
2133
2661
  hotSpans: ast.hotSpans,
2134
2662
  smellSpans,
2135
- heatSignals,
2663
+ heatSignals: {
2664
+ todos: ast.smells.filter((s) => s.kind === "todo").length,
2665
+ suppressions: ast.smells.filter((s) => s.kind === "suppression").length,
2666
+ swallowedCatches: ast.swallowedCatches,
2667
+ maxNesting: ast.maxNesting,
2668
+ longFunctions: ast.longFunctions,
2669
+ magicNumbers: ast.magicNumbers
2670
+ },
2136
2671
  loc: ast.loc,
2137
2672
  cyclomatic: ast.cyclomatic
2138
2673
  };
@@ -2140,16 +2675,16 @@ async function getFileAnalysis(absPath) {
2140
2675
 
2141
2676
  // ../brain/dist/dossier.js
2142
2677
  import { Mutex } from "async-mutex";
2143
- import { join as join5, dirname as dirname2 } from "path";
2678
+ import { join as join9, dirname as dirname3 } from "path";
2144
2679
  import { fileURLToPath as fileURLToPath2 } from "url";
2145
- import { readFile as readFile5, writeFile as writeFile4, mkdir as mkdir3 } from "fs/promises";
2146
- import { existsSync as existsSync3, cpSync } from "fs";
2147
- var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
2680
+ import { readFile as readFile8, writeFile as writeFile8, mkdir as mkdir7 } from "fs/promises";
2681
+ import { existsSync as existsSync4, cpSync } from "fs";
2682
+ var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
2148
2683
  var dossierMutex = new Mutex();
2149
2684
  async function readDossier(projectRoot) {
2150
- const dossierPath = join5(projectRoot, ".vibe-splainer", "dossier.json");
2685
+ const dossierPath = join9(projectRoot, ".vibe-splainer", "dossier.json");
2151
2686
  try {
2152
- const raw = await readFile5(dossierPath, "utf8");
2687
+ const raw = await readFile8(dossierPath, "utf8");
2153
2688
  return JSON.parse(raw);
2154
2689
  } catch {
2155
2690
  return null;
@@ -2161,33 +2696,33 @@ async function writeDossier(projectRoot, dossier) {
2161
2696
  p.decisions = p.decisions.filter((c) => !(c.severity === 1 && c.category === "Convention"));
2162
2697
  p.cardCount = p.decisions.length;
2163
2698
  }
2164
- const dir = join5(projectRoot, ".vibe-splainer");
2165
- await mkdir3(dir, { recursive: true });
2166
- const dossierPath = join5(dir, "dossier.json");
2699
+ const dir = join9(projectRoot, ".vibe-splainer");
2700
+ await mkdir7(dir, { recursive: true });
2701
+ const dossierPath = join9(dir, "dossier.json");
2167
2702
  const tmp = dossierPath + ".tmp";
2168
- await writeFile4(tmp, JSON.stringify(dossier, null, 2), "utf8");
2703
+ await writeFile8(tmp, JSON.stringify(dossier, null, 2), "utf8");
2169
2704
  const { rename } = await import("fs/promises");
2170
2705
  await rename(tmp, dossierPath);
2171
2706
  await regenerateUI(projectRoot, dossier);
2172
2707
  });
2173
2708
  }
2174
2709
  async function regenerateUI(projectRoot, dossier) {
2175
- const uiDir = join5(projectRoot, ".vibe-splainer", "ui");
2176
- await mkdir3(uiDir, { recursive: true });
2177
- let templateDir = join5(__dirname2, "ui");
2178
- if (!existsSync3(templateDir)) {
2179
- templateDir = join5(__dirname2, "../../cli/dist/ui");
2710
+ const uiDir = join9(projectRoot, ".vibe-splainer", "ui");
2711
+ await mkdir7(uiDir, { recursive: true });
2712
+ let templateDir = join9(__dirname2, "ui");
2713
+ if (!existsSync4(templateDir)) {
2714
+ templateDir = join9(__dirname2, "../../cli/dist/ui");
2180
2715
  }
2181
- if (!existsSync3(templateDir)) {
2716
+ if (!existsSync4(templateDir)) {
2182
2717
  console.error("[vibe-splain] UI template not found at", templateDir, "- skipping UI regeneration");
2183
2718
  return;
2184
2719
  }
2185
2720
  cpSync(templateDir, uiDir, { recursive: true });
2186
- let html = await readFile5(join5(templateDir, "index.html"), "utf8");
2721
+ let html = await readFile8(join9(templateDir, "index.html"), "utf8");
2187
2722
  const injection = `<script>window.__VIBE_DOSSIER__ = ${JSON.stringify(dossier)};</script>`;
2188
2723
  html = html.replace("<!-- VIBE_DOSSIER_INJECTION_POINT -->", injection);
2189
- await writeFile4(join5(uiDir, "index.html"), html, "utf8");
2190
- console.error("[vibe-splain] UI regenerated at", join5(uiDir, "index.html"));
2724
+ await writeFile8(join9(uiDir, "index.html"), html, "utf8");
2725
+ console.error("[vibe-splain] UI regenerated at", join9(uiDir, "index.html"));
2191
2726
  }
2192
2727
  function validateMermaidNodeCount(diagram) {
2193
2728
  if (!diagram)
@@ -2207,8 +2742,8 @@ function validateMermaidNodeCount(diagram) {
2207
2742
  // ../brain/dist/watcher.js
2208
2743
  import chokidar from "chokidar";
2209
2744
  import { createHash as createHash2 } from "crypto";
2210
- import { readFile as readFile6 } from "fs/promises";
2211
- import { join as join6 } from "path";
2745
+ import { readFile as readFile9 } from "fs/promises";
2746
+ import { join as join10 } from "path";
2212
2747
  function startWatcher(projectRoot, watchedPaths) {
2213
2748
  const watcher = chokidar.watch(watchedPaths.length > 0 ? watchedPaths : projectRoot, {
2214
2749
  ignoreInitial: true,
@@ -2220,14 +2755,14 @@ function startWatcher(projectRoot, watchedPaths) {
2220
2755
  const dossier = await readDossier(projectRoot);
2221
2756
  if (!dossier)
2222
2757
  return;
2223
- const content = await readFile6(filepath, "utf8");
2758
+ const content = await readFile9(filepath, "utf8");
2224
2759
  const newHash = createHash2("sha256").update(content).digest("hex");
2225
2760
  let mutated = false;
2226
2761
  for (const pillar of dossier.pillars) {
2227
2762
  for (const card of pillar.decisions) {
2228
2763
  if (!card.primaryFile)
2229
2764
  continue;
2230
- const absMatch = filepath === join6(projectRoot, card.primaryFile) || filepath.endsWith("/" + card.primaryFile);
2765
+ const absMatch = filepath === join10(projectRoot, card.primaryFile) || filepath.endsWith("/" + card.primaryFile);
2231
2766
  if (absMatch && card.lastScannedHash !== newHash) {
2232
2767
  card.status = "stale";
2233
2768
  const rel = card.primaryFile;
@@ -2286,7 +2821,22 @@ async function handleScanProject(args) {
2286
2821
  await writeDossier(projectRoot, dossier);
2287
2822
  startWatcher(projectRoot, result.files.map((f) => f.path));
2288
2823
  console.error(`[vibe-splain] Scan complete. ${result.totalFilesScanned} files, ${result.realSourceCount} real-source, ${result.wildCandidates.length} wild candidates.`);
2824
+ const validation = result.validation ?? { passed: true, errors: 0, warnings: 0, reportPath: ".vibe-splainer/validation_report.json" };
2289
2825
  return {
2826
+ ok: true,
2827
+ validation: {
2828
+ passed: validation.passed,
2829
+ errors: validation.errors,
2830
+ warnings: validation.warnings,
2831
+ reportPath: validation.reportPath
2832
+ },
2833
+ artifacts: {
2834
+ analysis: ".vibe-splainer/analysis.json",
2835
+ deltaTargets: ".vibe-splainer/delta_targets.json",
2836
+ dossier: ".vibe-splainer/dossier.json",
2837
+ graph: ".vibe-splainer/graph.json",
2838
+ html: ".vibe-splainer/ui/index.html"
2839
+ },
2290
2840
  projectRoot: result.projectRoot,
2291
2841
  totalFilesScanned: result.totalFilesScanned,
2292
2842
  realSourceCount: result.realSourceCount,
@@ -2382,8 +2932,8 @@ async function handleSetProjectBrief(args) {
2382
2932
  }
2383
2933
 
2384
2934
  // dist/mcp/tools/get_file_context.js
2385
- import { readFile as readFile7 } from "fs/promises";
2386
- import { join as join7, relative as relative2, isAbsolute } from "path";
2935
+ import { readFile as readFile10 } from "fs/promises";
2936
+ import { join as join11, relative as relative3, isAbsolute } from "path";
2387
2937
  var getFileContextTool = {
2388
2938
  name: "get_file_context",
2389
2939
  description: "Returns PRE-EXTRACTED evidence for a file so you do not have to read the whole thing and paraphrase its header comment. Returns: gravity/heat scores + signals, importedBy (named fan-in \u2014 use this for blastRadius), hotSpans (the gnarliest function bodies, comment-stripped, each with a reason), smellSpans (located tech debt with \xB13 lines of context), and signature (the exported API surface). Base your evidence on hotSpans/smellSpans \u2014 NEVER on header comments. Pass { full: true } only if you truly need the raw source.",
@@ -2403,8 +2953,8 @@ async function handleGetFileContext(args) {
2403
2953
  const full = args.full === true;
2404
2954
  if (!projectRoot || !filePath)
2405
2955
  throw new Error("projectRoot and filePath are required");
2406
- const fullPath = isAbsolute(filePath) ? filePath : join7(projectRoot, filePath);
2407
- const relPath = relative2(projectRoot, fullPath);
2956
+ const fullPath = isAbsolute(filePath) ? filePath : join11(projectRoot, filePath);
2957
+ const relPath = relative3(projectRoot, fullPath);
2408
2958
  const evidence = await getFileAnalysis(fullPath);
2409
2959
  if (!evidence) {
2410
2960
  throw new Error(`Could not analyze ${relPath} (unsupported language or parse failure).`);
@@ -2428,7 +2978,7 @@ async function handleGetFileContext(args) {
2428
2978
  smellSpans: evidence.smellSpans
2429
2979
  };
2430
2980
  if (full) {
2431
- result.source = await readFile7(fullPath, "utf8");
2981
+ result.source = await readFile10(fullPath, "utf8");
2432
2982
  }
2433
2983
  return result;
2434
2984
  }
@@ -2436,8 +2986,8 @@ async function handleGetFileContext(args) {
2436
2986
  // dist/mcp/tools/write_decision_card.js
2437
2987
  import { v4 as uuidv4 } from "uuid";
2438
2988
  import { createHash as createHash3 } from "crypto";
2439
- import { readFile as readFile8 } from "fs/promises";
2440
- import { join as join8 } from "path";
2989
+ import { readFile as readFile11 } from "fs/promises";
2990
+ import { join as join12 } from "path";
2441
2991
  var CATEGORIES = ["Bottleneck", "Hack", "Smart-Move", "Risk", "Convention", "Dead-Weight"];
2442
2992
  function normalizeSnippet(s) {
2443
2993
  let out = (s ?? "").replace(/\r\n/g, "\n");
@@ -2529,7 +3079,7 @@ async function handleWriteDecisionCard(args) {
2529
3079
  const heat = persisted ? Math.round(persisted.heat) : void 0;
2530
3080
  let primaryContent = "";
2531
3081
  try {
2532
- primaryContent = await readFile8(join8(projectRoot, primaryFile), "utf8");
3082
+ primaryContent = await readFile11(join12(projectRoot, primaryFile), "utf8");
2533
3083
  } catch {
2534
3084
  }
2535
3085
  const hash = createHash3("sha256").update(primaryContent).digest("hex");
@@ -2773,7 +3323,7 @@ var TOOL_HANDLERS = {
2773
3323
  mark_stale: handleMarkStale
2774
3324
  };
2775
3325
  async function startMCPServer() {
2776
- await initParser();
3326
+ await initParser2();
2777
3327
  console.error("[vibe-splain] Tree-Sitter parser initialized");
2778
3328
  const server = new Server({ name: "vibe-splain", version: "2.0.0" }, { capabilities: { tools: {}, prompts: {} } });
2779
3329
  server.setRequestHandler(ListPromptsRequestSchema, async () => ({
@@ -2898,7 +3448,7 @@ async function serveCommand() {
2898
3448
 
2899
3449
  // dist/index.js
2900
3450
  var program = new Command();
2901
- program.name("vibe-splain").description("Architectural dossier engine for vibe-coded projects").version("2.4.1");
3451
+ program.name("vibe-splain").description("Architectural dossier engine for vibe-coded projects").version("2.5.0");
2902
3452
  program.command("install").description("Patch coding agent MCP config files to register vibe-splain").action(installCommand);
2903
3453
  program.command("serve").description("Start the MCP server (called by the coding agent, not by you)").action(serveCommand);
2904
3454
  program.parse();