vibe-splain 2.4.0 → 2.5.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
@@ -87,12 +87,11 @@ import { StdioServerTransport } from "@modelcontextprotocol/sdk/server/stdio.js"
87
87
  import { CallToolRequestSchema, ListToolsRequestSchema, ListPromptsRequestSchema, GetPromptRequestSchema } from "@modelcontextprotocol/sdk/types.js";
88
88
 
89
89
  // ../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";
90
+ import { extname as extname4 } from "path";
91
+ import { readFile as readFile6 } from "fs/promises";
92
+
93
+ // ../brain/dist/pipeline/orchestrator.js
94
+ import { join as join8 } from "path";
96
95
 
97
96
  // ../brain/dist/graph.js
98
97
  import { join as join2 } from "path";
@@ -125,80 +124,17 @@ async function writeAnalysis(projectRoot, store) {
125
124
  const { rename } = await import("fs/promises");
126
125
  await rename(tmp, dest);
127
126
  }
128
- var LOAD_BEARING_FAN_IN_THRESHOLD = 10;
129
- function deriveRiskTypes(f) {
130
- const kinds = new Set(f.smells.map((s) => s.kind));
131
- const types = [];
132
- if (f.gravitySignals.cyclomatic > 15)
133
- types.push("state_machine");
134
- if (kinds.has("god-file"))
135
- types.push("god_object");
136
- if (f.gravitySignals.fanIn > 15)
137
- types.push("deep_coupling");
138
- if (kinds.has("swallowed-catch"))
139
- types.push("error_sink");
140
- if (f.gravitySignals.fanIn > 10 && f.gravitySignals.publicSurface > 8)
141
- types.push("mutation_hotspot");
142
- if (kinds.has("todo") && kinds.has("suppression"))
143
- types.push("tech_debt");
144
- return types.length > 0 ? types : ["complexity_hotspot"];
145
- }
146
- function deriveConfidence(f) {
147
- if (f.gravitySignals.fanIn >= LOAD_BEARING_FAN_IN_THRESHOLD && f.gravity >= 40)
148
- return "high";
149
- if (f.gravitySignals.fanIn >= 5 || f.gravity >= 25)
150
- return "medium";
151
- return "low";
152
- }
153
- function findRuntimeEntrypoints(relPath, files, entrypoints) {
154
- const found = [];
155
- const visited = /* @__PURE__ */ new Set();
156
- const queue = [relPath];
157
- while (queue.length > 0) {
158
- const curr = queue.shift();
159
- if (visited.has(curr))
160
- continue;
161
- visited.add(curr);
162
- if (entrypoints.has(curr) && curr !== relPath)
163
- found.push(curr);
164
- if (found.length >= 5)
165
- break;
166
- const f = files[curr];
167
- if (f) {
168
- for (const importer of f.importedBy)
169
- if (!visited.has(importer))
170
- queue.push(importer);
171
- }
172
- }
173
- return found;
174
- }
175
- async function writeDeltaTargets(projectRoot, store, entrypoints = /* @__PURE__ */ new Set()) {
176
- const domain = (f) => f.pillarHint && !f.pillarHint.startsWith("community-") ? f.pillarHint : null;
177
- const targets = Object.values(store.files).filter((f) => f.isRealSource).sort((a, b) => b.gravity - a.gravity).map((f) => ({
178
- path: f.relativePath,
179
- gravity: f.gravity,
180
- isLoadBearing: f.gravitySignals.fanIn >= LOAD_BEARING_FAN_IN_THRESHOLD,
181
- blastRadius: f.importedBy,
182
- pillarHint: domain(f),
183
- domain: domain(f),
184
- riskTypes: deriveRiskTypes(f),
185
- severity: f.smells.length > 0 ? Math.max(...f.smells.map((s) => s.severity)) : 0,
186
- confidence: deriveConfidence(f),
187
- runtimeEntrypoints: findRuntimeEntrypoints(f.relativePath, store.files, entrypoints)
188
- }));
189
- const dir = join3(projectRoot, ".vibe-splainer");
190
- await mkdir2(dir, { recursive: true });
191
- const dest = join3(dir, "delta_targets.json");
192
- const tmp = dest + ".tmp";
193
- await writeFile3(tmp, JSON.stringify(targets, null, 2), "utf8");
194
- const { rename } = await import("fs/promises");
195
- await rename(tmp, dest);
196
- }
197
127
 
198
- // ../brain/dist/scanner.js
128
+ // ../brain/dist/pipeline/inventory.js
129
+ import Parser from "web-tree-sitter";
130
+ import { join as join4, dirname, relative, extname, basename, sep } from "path";
131
+ import { fileURLToPath } from "url";
132
+ import { createRequire } from "module";
133
+ import { readFile as readFile4, readdir, writeFile as writeFile4, mkdir as mkdir3 } from "fs/promises";
134
+ import { existsSync as existsSync2 } from "fs";
199
135
  var __dirname = dirname(fileURLToPath(import.meta.url));
200
136
  var require2 = createRequire(import.meta.url);
201
- var parser = null;
137
+ var _parser = null;
202
138
  var langCache = /* @__PURE__ */ new Map();
203
139
  var EXT_LANG = {
204
140
  ".ts": "typescript",
@@ -230,7 +166,7 @@ function resolveWasm(file) {
230
166
  return p;
231
167
  } catch {
232
168
  }
233
- const local = join4(__dirname, "../wasm", file);
169
+ const local = join4(__dirname, "../../wasm", file);
234
170
  return existsSync2(local) ? local : null;
235
171
  }
236
172
  async function getLanguage(lang) {
@@ -239,7 +175,7 @@ async function getLanguage(lang) {
239
175
  return cached;
240
176
  const wasm = resolveWasm(LANG_WASM[lang]);
241
177
  if (!wasm) {
242
- console.error(`[vibe-splain] grammar missing for ${lang} (${LANG_WASM[lang]}); skipping language`);
178
+ console.error(`[vibe-splain] grammar missing for ${lang} (${LANG_WASM[lang]}); skipping`);
243
179
  return null;
244
180
  }
245
181
  try {
@@ -252,14 +188,14 @@ async function getLanguage(lang) {
252
188
  }
253
189
  }
254
190
  async function initParser() {
255
- if (parser)
256
- return parser;
191
+ if (_parser)
192
+ return _parser;
257
193
  await Parser.init();
258
- parser = new Parser();
194
+ _parser = new Parser();
259
195
  const ts = await getLanguage("typescript");
260
196
  if (ts)
261
- parser.setLanguage(ts);
262
- return parser;
197
+ _parser.setLanguage(ts);
198
+ return _parser;
263
199
  }
264
200
  async function parseAs(lang, source) {
265
201
  const p = await initParser();
@@ -324,6 +260,143 @@ var VENDOR_SEGMENTS = /* @__PURE__ */ new Set([
324
260
  "third_party",
325
261
  "third-party"
326
262
  ]);
263
+ async function collectFiles(dir, projectRoot, acc) {
264
+ let entries;
265
+ try {
266
+ entries = await readdir(dir, { withFileTypes: true });
267
+ } catch {
268
+ return;
269
+ }
270
+ for (const entry of entries) {
271
+ if (entry.name.startsWith(".") && entry.name !== ".") {
272
+ if (entry.isDirectory())
273
+ continue;
274
+ }
275
+ if (EXCLUDE_DIRS.has(entry.name))
276
+ continue;
277
+ const fullPath = join4(dir, entry.name);
278
+ if (entry.isDirectory()) {
279
+ await collectFiles(fullPath, projectRoot, acc);
280
+ } else if (entry.isFile()) {
281
+ const ext = extname(entry.name);
282
+ if (!SUPPORTED_EXTENSIONS.has(ext))
283
+ continue;
284
+ if (EXCLUDE_FILE_PATTERNS.some((p) => p.test(entry.name)))
285
+ continue;
286
+ acc.push(fullPath);
287
+ }
288
+ }
289
+ }
290
+ function pathDemoteReason(relPath) {
291
+ const segs = relPath.split(sep);
292
+ for (const s of segs) {
293
+ if (VENDOR_SEGMENTS.has(s))
294
+ return `vendored code (${s})`;
295
+ if (s.endsWith(".venv") || s === "venv" || s === "env")
296
+ return "virtual environment";
297
+ }
298
+ for (const s of segs) {
299
+ if (DEMOTE_SEGMENTS.has(s.toLowerCase()))
300
+ return `non-application path segment (${s})`;
301
+ }
302
+ const b = basename(relPath);
303
+ if (/\.min\./.test(b))
304
+ return "minified bundle";
305
+ if (/\.generated\./.test(b))
306
+ return "generated file";
307
+ return null;
308
+ }
309
+ function inferFrameworkRole(relPath) {
310
+ const p = relPath.replace(/\\/g, "/");
311
+ if (/\.test\.|\.spec\./.test(p))
312
+ return "test";
313
+ if (/\.generated\.|__generated__|\.prisma\//.test(p))
314
+ return "generated";
315
+ if (/(?:^|\/)app\/.*\/page\.tsx?$/.test(p))
316
+ return "app_route_page";
317
+ if (/(?:^|\/)app\/.*\/layout\.tsx?$/.test(p))
318
+ return "app_route_layout";
319
+ if (/(?:^|\/)app\/.*\/route\.tsx?$/.test(p))
320
+ return "app_route_handler";
321
+ if (/(?:^|\/)app\/.*\/loading\.tsx?$/.test(p))
322
+ return "app_loading_boundary";
323
+ if (/(?:^|\/)app\/.*\/error\.tsx?$/.test(p))
324
+ return "app_error_boundary";
325
+ if (/(?:^|\/)pages\/api\/trpc\//.test(p))
326
+ return "trpc_api_route";
327
+ if (/(?:^|\/)pages\/api\//.test(p))
328
+ return "pages_api_route";
329
+ if (/(?:^|\/)pages\//.test(p))
330
+ return "pages_route";
331
+ if (/\/hooks\/|\/use[A-Z][^/]*\.(ts|tsx)$/.test(p))
332
+ return "hook";
333
+ if (/\/stores?\/|[Ss]tore\.(ts|tsx)$/.test(p))
334
+ return "store";
335
+ if (/[Pp]rovider\.(tsx?|jsx?)$|\/providers?\//.test(p))
336
+ return "provider";
337
+ if (/\.types\.ts$|\/types\.ts$|\/types\/[^/]+\.ts$/.test(p))
338
+ return "type_definition";
339
+ if (/\.(tsx|jsx)$/.test(p))
340
+ return "component";
341
+ if (/\.(ts|js|mjs|cjs)$/.test(p))
342
+ return "utility";
343
+ return "unknown";
344
+ }
345
+ function inferProductDomain(relPath, importSpecs) {
346
+ const p = relPath.toLowerCase().replace(/\\/g, "/");
347
+ if (/\.test\.|\.spec\.|__tests__|\/e2e\/|\/playwright\/|\/cypress\//.test(p)) {
348
+ return "test_infrastructure";
349
+ }
350
+ if (/\.generated\.|__generated__|\.prisma\//.test(p)) {
351
+ return "generated_noise";
352
+ }
353
+ if (p.includes("booking-audit") || p.includes("bookingaudit"))
354
+ return "booking_audit";
355
+ 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"))
356
+ return "booking_creation";
357
+ if (p.includes("modules/bookings") || p.includes("components/booking/actions") || p.includes("/bookings/[status]") || p.includes("/booking/[uid]") || p.includes("/bookings/"))
358
+ return "booking_management";
359
+ if (p.includes("event-types") || p.includes("eventtypes") || p.includes("eventavailabilitytab") || p.includes("eventadvancedtab") || p.includes("eventlimits") || p.includes("eventrecurring"))
360
+ return "event_type_configuration";
361
+ if (p.includes("availability") || p.includes("/schedules/") || p.includes("/slots/")) {
362
+ return "availability";
363
+ }
364
+ 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")))
365
+ return "auth_oauth";
366
+ 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/")))
367
+ return "auth";
368
+ if ((p.includes("stripe") || p.includes("paypal") || p.includes("btcpay") || p.includes("alby") || p.includes("payment")) && (p.includes("webhook") || p.includes("hook")))
369
+ return "payments_webhooks";
370
+ 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/")))
371
+ return "payments";
372
+ if (p.includes("webhook"))
373
+ return "webhooks";
374
+ if (p.includes("app-store") || p.includes("appstore") || p.includes("/apps/") || p.includes("modules/apps"))
375
+ return "apps_marketplace";
376
+ if (p.includes("calendar") || p.includes("selected-calendars") || importSpecs.some((s) => s.includes("googleapis") || s.includes("@google-cloud/")))
377
+ return "calendar_integrations";
378
+ if (p.includes("video") || p.includes("calvideo") || p.includes("daily.co"))
379
+ return "video";
380
+ if (p.includes("onboarding") || p.includes("getting-started"))
381
+ return "onboarding";
382
+ if (p.includes("/settings/") || p.includes("/settings."))
383
+ return "settings";
384
+ if (p.includes("/admin/") || p.includes("/admin."))
385
+ return "admin";
386
+ if (p.includes("data-table") || p.includes("datatable") || p.includes("datasegment") || p.includes("segment"))
387
+ return "data_table";
388
+ if (p.includes("shell/navigation") || p.includes("navigationitem") || p.includes("/shell/") || p.includes("sidebar") || p.includes("topnav") || p.includes("mainnav"))
389
+ return "shell_navigation";
390
+ if (p.includes("form-builder") || p.includes("formbuilder") || p.includes("/forms/") || p.includes("routingforms"))
391
+ return "forms";
392
+ if (p.includes("embed"))
393
+ return "embed";
394
+ if (p.includes("notification") || p.includes("/email/") || p.includes("/emails/") || importSpecs.some((s) => s.includes("nodemailer") || s.includes("resend") || s.includes("@sendgrid/")))
395
+ return "notifications";
396
+ if (p.includes("middleware") && !p.includes("pages/api/") || p.includes("/router.") || p.includes("routerconfig"))
397
+ return "routing_infrastructure";
398
+ return "unknown";
399
+ }
327
400
  var PILLAR_KEYWORDS = {
328
401
  "Auth": [
329
402
  "passport",
@@ -376,20 +449,6 @@ var PILLAR_KEYWORDS = {
376
449
  "paddle",
377
450
  "lemon-squeezy"
378
451
  ],
379
- "Routing": [
380
- "express",
381
- "fastify",
382
- "koa",
383
- "koa-router",
384
- "next/router",
385
- "next/navigation",
386
- "react-router",
387
- "@remix-run/",
388
- "hono",
389
- "express-rate-limit",
390
- "cors",
391
- "helmet"
392
- ],
393
452
  "Queue": [
394
453
  "bull",
395
454
  "bullmq",
@@ -412,35 +471,14 @@ var PILLAR_KEYWORDS = {
412
471
  "sharp",
413
472
  "imagekit"
414
473
  ],
415
- "Config": [
416
- "dotenv",
417
- "convict",
418
- "env-var",
419
- "@t3-oss/env",
420
- "envalid"
421
- ],
422
- "Email": [
423
- "nodemailer",
424
- "resend",
425
- "@sendgrid/",
426
- "postmark",
427
- "@resend/",
428
- "mailgun"
429
- ],
430
- "Realtime": [
431
- "socket.io",
432
- "ws",
433
- "pusher",
434
- "ably",
435
- "@supabase/realtime",
436
- "socket.io-client"
437
- ]
474
+ "Config": ["dotenv", "convict", "env-var", "@t3-oss/env", "envalid"],
475
+ "Email": ["nodemailer", "resend", "@sendgrid/", "postmark", "@resend/", "mailgun"],
476
+ "Realtime": ["socket.io", "ws", "pusher", "ably", "@supabase/realtime", "socket.io-client"]
438
477
  };
439
478
  var PILLAR_PATH_PATTERNS = {
440
479
  "Auth": /(?:^|[\/\\])(?:auth|login|signup|register|session|oauth)(?:[\/\\]|$)/i,
441
480
  "Database": /(?:^|[\/\\])(?:db|database|models?|schema|migrations?|seeds?)(?:[\/\\]|$)/i,
442
481
  "Payments": /(?:^|[\/\\])(?:pay|payments?|billing|checkout|subscriptions?|stripe)(?:[\/\\]|$)/i,
443
- "Routing": /(?:^|[\/\\])(?:routes?|router|middleware|api)(?:[\/\\]|$)/i,
444
482
  "Queue": /(?:^|[\/\\])(?:queues?|workers?|jobs?|consumers?|producers?)(?:[\/\\]|$)/i,
445
483
  "Storage": /(?:^|[\/\\])(?:storage|uploads?|s3|blobs?|media)(?:[\/\\]|$)/i,
446
484
  "Config": /(?:^|[\/\\])(?:config|env|settings?)(?:[\/\\]|$)/i,
@@ -481,52 +519,6 @@ function matchPillarByPath(relPath) {
481
519
  }
482
520
  return null;
483
521
  }
484
- async function collectFiles(dir, projectRoot, acc) {
485
- let entries;
486
- try {
487
- entries = await readdir(dir, { withFileTypes: true });
488
- } catch {
489
- return;
490
- }
491
- for (const entry of entries) {
492
- if (entry.name.startsWith(".") && entry.name !== ".") {
493
- if (entry.isDirectory())
494
- continue;
495
- }
496
- if (EXCLUDE_DIRS.has(entry.name))
497
- continue;
498
- const fullPath = join4(dir, entry.name);
499
- if (entry.isDirectory()) {
500
- await collectFiles(fullPath, projectRoot, acc);
501
- } else if (entry.isFile()) {
502
- const ext = extname(entry.name);
503
- if (!SUPPORTED_EXTENSIONS.has(ext))
504
- continue;
505
- if (EXCLUDE_FILE_PATTERNS.some((p) => p.test(entry.name)))
506
- continue;
507
- acc.push(fullPath);
508
- }
509
- }
510
- }
511
- function pathDemoteReason(relPath) {
512
- const segs = relPath.split(sep);
513
- for (const s of segs) {
514
- if (VENDOR_SEGMENTS.has(s))
515
- return `vendored code (${s})`;
516
- if (s.endsWith(".venv") || s === "venv" || s === "env")
517
- return "virtual environment";
518
- }
519
- for (const s of segs) {
520
- if (DEMOTE_SEGMENTS.has(s.toLowerCase()))
521
- return `non-application path segment (${s})`;
522
- }
523
- const base = basename(relPath);
524
- if (/\.min\./.test(base))
525
- return "minified bundle";
526
- if (/\.generated\./.test(base))
527
- return "generated file";
528
- return null;
529
- }
530
522
  function extractImports(source, lang) {
531
523
  const specs = [];
532
524
  if (lang === "python") {
@@ -575,77 +567,92 @@ function extractImports(source, lang) {
575
567
  }
576
568
  const re = /(?:import|export)\s[^;]*?from\s*['"]([^'"]+)['"]|(?:import|require)\s*\(\s*['"]([^'"]+)['"]/g;
577
569
  let m;
578
- while ((m = re.exec(source)) !== null) {
570
+ while ((m = re.exec(source)) !== null)
579
571
  specs.push(m[1] || m[2]);
580
- }
581
572
  return specs;
582
573
  }
583
- var JS_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
584
- function resolveImport(spec, fromAbs, lang, projectRoot, fileSet, basenameIndex) {
585
- if (lang === "python") {
586
- return resolvePython(spec, fromAbs, projectRoot, fileSet);
587
- }
588
- if (lang === "typescript" || lang === "tsx" || lang === "javascript") {
589
- if (!spec.startsWith("."))
590
- return null;
591
- const base = join4(dirname(fromAbs), spec);
592
- return tryJsCandidates(base, projectRoot, fileSet);
593
- }
594
- return resolveGeneric(spec, projectRoot, fileSet, basenameIndex);
595
- }
596
- function tryJsCandidates(base, projectRoot, fileSet) {
597
- const candidates = [];
598
- for (const ext of JS_EXTS)
599
- candidates.push(base + ext);
600
- for (const ext of JS_EXTS)
601
- candidates.push(join4(base, "index" + ext));
602
- candidates.unshift(base);
603
- for (const c of candidates) {
604
- const rel = relative(projectRoot, c);
605
- if (fileSet.has(rel))
606
- return rel;
607
- }
608
- return null;
609
- }
610
- function resolvePython(spec, fromAbs, projectRoot, fileSet) {
611
- let modulePath;
612
- if (spec.startsWith(".")) {
613
- const dots = spec.match(/^\.+/)[0].length;
614
- let dir = dirname(fromAbs);
615
- for (let i = 1; i < dots; i++)
616
- dir = dirname(dir);
617
- const rest = spec.slice(dots).replace(/\./g, sep);
618
- modulePath = rest ? join4(dir, rest) : dir;
619
- } else {
620
- modulePath = join4(projectRoot, spec.replace(/\./g, sep));
574
+ async function detectStackAndEntrypoints(projectRoot, files) {
575
+ const stack = /* @__PURE__ */ new Set();
576
+ const entrypoints = /* @__PURE__ */ new Set();
577
+ const rel = (abs) => relative(projectRoot, abs);
578
+ const pkgPath = join4(projectRoot, "package.json");
579
+ if (existsSync2(pkgPath)) {
580
+ try {
581
+ const pkg = JSON.parse(await readFile4(pkgPath, "utf8"));
582
+ stack.add("Node.js");
583
+ const deps = { ...pkg.dependencies || {}, ...pkg.devDependencies || {} };
584
+ for (const known of ["react", "next", "vue", "svelte", "express", "fastify", "typescript", "vite"]) {
585
+ if (deps[known])
586
+ stack.add(known === "next" ? "Next.js" : known[0].toUpperCase() + known.slice(1));
587
+ }
588
+ const addEntry = (p) => {
589
+ if (!p)
590
+ return;
591
+ const abs = join4(projectRoot, p);
592
+ const r = relative(projectRoot, abs);
593
+ if (files.includes(abs))
594
+ entrypoints.add(r);
595
+ };
596
+ addEntry(pkg.main);
597
+ if (typeof pkg.bin === "string")
598
+ addEntry(pkg.bin);
599
+ else if (pkg.bin)
600
+ for (const v of Object.values(pkg.bin))
601
+ addEntry(v);
602
+ } catch {
603
+ }
621
604
  }
622
- const candidates = [modulePath + ".py", join4(modulePath, "__init__.py")];
623
- for (const c of candidates) {
624
- const rel = relative(projectRoot, c);
625
- if (fileSet.has(rel))
626
- return rel;
605
+ const pyproject = join4(projectRoot, "pyproject.toml");
606
+ const setupPy = join4(projectRoot, "setup.py");
607
+ const requirements = join4(projectRoot, "requirements.txt");
608
+ if (existsSync2(pyproject) || existsSync2(setupPy) || existsSync2(requirements)) {
609
+ stack.add("Python");
610
+ let reqText = "";
611
+ for (const f of [pyproject, requirements]) {
612
+ if (existsSync2(f)) {
613
+ try {
614
+ reqText += await readFile4(f, "utf8");
615
+ } catch {
616
+ }
617
+ }
618
+ }
619
+ for (const known of ["pygame", "PySide6", "PyQt5", "PyQt6", "flask", "django", "fastapi", "numpy", "pandas", "torch", "tensorflow"]) {
620
+ if (new RegExp(known, "i").test(reqText))
621
+ stack.add(known);
622
+ }
627
623
  }
628
- if (!spec.startsWith(".")) {
629
- const last = spec.split(".").pop();
630
- void last;
624
+ if (existsSync2(join4(projectRoot, "go.mod")))
625
+ stack.add("Go");
626
+ if (existsSync2(join4(projectRoot, "Cargo.toml")))
627
+ stack.add("Rust");
628
+ if (existsSync2(join4(projectRoot, "pom.xml")) || existsSync2(join4(projectRoot, "build.gradle")))
629
+ stack.add("Java");
630
+ for (const abs of files) {
631
+ const r = rel(abs);
632
+ const b = basename(r);
633
+ if (b === "main.py" || b === "__main__.py")
634
+ entrypoints.add(r);
635
+ if (/^index\.(ts|tsx|js|jsx|mjs|cjs)$/.test(b) && dirname(r).split(sep).length <= 2)
636
+ entrypoints.add(r);
637
+ if (b === "main.go" && r.includes("cmd" + sep))
638
+ entrypoints.add(r);
639
+ if (b === "main.go" && !r.includes(sep))
640
+ entrypoints.add(r);
641
+ if (b === "main.rs" || b === "lib.rs")
642
+ entrypoints.add(r);
631
643
  }
632
- return null;
633
- }
634
- function resolveGeneric(spec, projectRoot, fileSet, basenameIndex) {
635
- const normalized = spec.replace(/^crate::/, "").replace(/::/g, "/").replace(/\./g, "/");
636
- const parts = normalized.split("/").filter(Boolean);
637
- if (parts.length === 0)
638
- return null;
639
- const last = parts[parts.length - 1];
640
- for (const rel of fileSet) {
641
- const noExt = rel.slice(0, rel.length - extname(rel).length);
642
- if (noExt.endsWith(parts.join(sep)))
643
- return rel;
644
+ if (stack.has("Next.js")) {
645
+ const appRouterNames = /* @__PURE__ */ new Set(["page", "layout", "route", "loading", "error", "not-found", "template", "default"]);
646
+ for (const abs of files) {
647
+ const r = rel(abs);
648
+ const stem = basename(r, extname(r));
649
+ if (/(?:^|[/\\])app[/\\]/.test(r) && appRouterNames.has(stem))
650
+ entrypoints.add(r);
651
+ if (/(?:^|[/\\])pages[/\\]/.test(r) && !stem.startsWith("_"))
652
+ entrypoints.add(r);
653
+ }
644
654
  }
645
- const byBase = basenameIndex.get(last);
646
- if (byBase && byBase.length === 1)
647
- return byBase[0];
648
- return null;
655
+ return { stack: [...stack], entrypoints };
649
656
  }
650
657
  var FUNCTION_TYPES = /* @__PURE__ */ new Set([
651
658
  "function_declaration",
@@ -806,41 +813,120 @@ function collectFunctionNodes(root) {
806
813
  walk(root);
807
814
  return out;
808
815
  }
809
- function catchIsSwallowed(node, lang) {
816
+ function catchIsSwallowed(node) {
810
817
  const bodyText = node.text;
811
818
  const inner = bodyText.replace(/^[^{:]*[{:]/, "");
812
819
  const meaningful = inner.split("\n").map((l) => l.trim()).filter((l) => l && !l.startsWith("//") && !l.startsWith("#") && l !== "}" && l !== "pass");
813
820
  if (meaningful.length === 0)
814
821
  return true;
815
- const onlyLogs = meaningful.every((l) => /^(console\.(log|error|warn|info)|print|println!?|System\.out|logger?\.)/.test(l) || l === "pass" || l === "{" || l === "});" || l === ")" || l === "`");
816
- return onlyLogs;
822
+ return meaningful.every((l) => /^(console\.(log|error|warn|info)|print|println!?|System\.out|logger?\.)/.test(l) || l === "pass" || l === "{" || l === "});" || l === ")" || l === "`");
817
823
  }
818
- function analyzeAst(source, lang, tree) {
819
- const root = tree.rootNode;
820
- const lines = source.split("\n");
821
- const loc = lines.length;
822
- const cyclomatic = countDecisions(root);
823
- const maxNesting = computeNesting(root, 0);
824
- const smells = [];
825
- let todos = 0, suppressions = 0;
826
- for (let i = 0; i < lines.length; i++) {
827
- const line = lines[i];
828
- if (TODO_RE.test(line)) {
829
- todos++;
830
- smells.push({ kind: "todo", line: i + 1, endLine: i + 1, text: line.trim().slice(0, 200), severity: 2, note: "unfinished / known-bad marker" });
831
- }
832
- if (SUPPRESS_RE.test(line)) {
833
- suppressions++;
834
- smells.push({ kind: "suppression", line: i + 1, endLine: i + 1, text: line.trim().slice(0, 200), severity: 3, note: "type/lint safety suppressed" });
835
- }
836
- }
837
- let magicNumbers = 0;
838
- const magicWalk = (n) => {
839
- if (n.type === "number" || n.type === "integer_literal" || n.type === "float_literal" || n.type === "int_literal") {
840
- const v = n.text.replace(/_/g, "");
841
- if (!["0", "1", "2", "-1", "100", "1000"].includes(v) && /^\d{2,}$/.test(v)) {
842
- magicNumbers++;
824
+ function collectExports(root, lang) {
825
+ const out = [];
826
+ const seen = /* @__PURE__ */ new Set();
827
+ const push = (name, node) => {
828
+ if (!name || seen.has(name))
829
+ return;
830
+ seen.add(name);
831
+ out.push({ name, text: firstLine(node.text).trim().slice(0, 200) });
832
+ };
833
+ if (lang === "python") {
834
+ for (const c of root.children) {
835
+ if (c.type === "function_definition" || c.type === "class_definition") {
836
+ const name = c.childForFieldName("name")?.text;
837
+ if (name && !name.startsWith("_"))
838
+ push(name, c);
839
+ }
840
+ }
841
+ return out;
842
+ }
843
+ if (lang === "go") {
844
+ const walk2 = (n) => {
845
+ if (n.type === "function_declaration" || n.type === "method_declaration" || n.type === "type_declaration") {
846
+ const name = n.childForFieldName("name")?.text;
847
+ if (name && /^[A-Z]/.test(name))
848
+ push(name, n);
849
+ }
850
+ for (const c of n.children)
851
+ walk2(c);
852
+ };
853
+ walk2(root);
854
+ return out;
855
+ }
856
+ if (lang === "rust") {
857
+ const walk2 = (n) => {
858
+ if (/_item$/.test(n.type) && n.children.some((c) => c.type === "visibility_modifier")) {
859
+ const name = n.childForFieldName("name")?.text;
860
+ push(name, n);
861
+ }
862
+ for (const c of n.children)
863
+ walk2(c);
864
+ };
865
+ walk2(root);
866
+ return out;
867
+ }
868
+ if (lang === "java") {
869
+ const walk2 = (n) => {
870
+ if ((n.type === "method_declaration" || n.type === "class_declaration") && /\bpublic\b/.test(firstLine(n.text))) {
871
+ const name = n.childForFieldName("name")?.text;
872
+ push(name, n);
873
+ }
874
+ for (const c of n.children)
875
+ walk2(c);
876
+ };
877
+ walk2(root);
878
+ return out;
879
+ }
880
+ const walk = (n) => {
881
+ if (n.type === "export_statement") {
882
+ const decl = n.childForFieldName("declaration");
883
+ if (decl) {
884
+ const name = decl.childForFieldName("name")?.text;
885
+ if (name)
886
+ push(name, decl);
887
+ for (const c of decl.namedChildren) {
888
+ const dn = c.childForFieldName("name")?.text;
889
+ if (dn)
890
+ push(dn, c);
891
+ }
892
+ }
893
+ for (const spec of n.descendantsOfType("export_specifier")) {
894
+ push(spec.childForFieldName("name")?.text, spec);
843
895
  }
896
+ if (n.text.includes("export default"))
897
+ push("default", n);
898
+ }
899
+ for (const c of n.children)
900
+ walk(c);
901
+ };
902
+ walk(root);
903
+ return out;
904
+ }
905
+ function analyzeAst(source, lang, tree) {
906
+ const root = tree.rootNode;
907
+ const lines = source.split("\n");
908
+ const loc = lines.length;
909
+ const cyclomatic = countDecisions(root);
910
+ const maxNesting = computeNesting(root, 0);
911
+ const smells = [];
912
+ let todos = 0, suppressions = 0;
913
+ for (let i = 0; i < lines.length; i++) {
914
+ const line = lines[i];
915
+ if (TODO_RE.test(line)) {
916
+ todos++;
917
+ smells.push({ kind: "todo", line: i + 1, endLine: i + 1, text: line.trim().slice(0, 200), severity: 2, note: "unfinished / known-bad marker" });
918
+ }
919
+ if (SUPPRESS_RE.test(line)) {
920
+ suppressions++;
921
+ smells.push({ kind: "suppression", line: i + 1, endLine: i + 1, text: line.trim().slice(0, 200), severity: 3, note: "type/lint safety suppressed" });
922
+ }
923
+ }
924
+ let magicNumbers = 0;
925
+ const magicWalk = (n) => {
926
+ if (n.type === "number" || n.type === "integer_literal" || n.type === "float_literal" || n.type === "int_literal") {
927
+ const v = n.text.replace(/_/g, "");
928
+ if (!["0", "1", "2", "-1", "100", "1000"].includes(v) && /^\d{2,}$/.test(v))
929
+ magicNumbers++;
844
930
  }
845
931
  for (const c of n.children)
846
932
  magicWalk(c);
@@ -851,7 +937,7 @@ function analyzeAst(source, lang, tree) {
851
937
  }
852
938
  let swallowedCatches = 0;
853
939
  const catchWalk = (n) => {
854
- if (CATCH_TYPES.has(n.type) && catchIsSwallowed(n, lang)) {
940
+ if (CATCH_TYPES.has(n.type) && catchIsSwallowed(n)) {
855
941
  swallowedCatches++;
856
942
  smells.push({
857
943
  kind: "swallowed-catch",
@@ -927,86 +1013,596 @@ function analyzeAst(source, lang, tree) {
927
1013
  hotSpans
928
1014
  };
929
1015
  }
930
- function collectExports(root, lang) {
931
- const out = [];
932
- const seen = /* @__PURE__ */ new Set();
933
- const push = (name, node) => {
934
- if (!name || seen.has(name))
935
- return;
936
- seen.add(name);
937
- out.push({ name, text: firstLine(node.text).trim().slice(0, 200) });
1016
+ var SMELL_WEIGHT = {
1017
+ "todo": 3,
1018
+ "suppression": 5,
1019
+ "swallowed-catch": 10,
1020
+ "deep-nesting": 6,
1021
+ "long-function": 5,
1022
+ "magic-number": 3,
1023
+ "god-file": 14
1024
+ };
1025
+ function computeHeat(smells) {
1026
+ let sum = 0;
1027
+ for (const s of smells)
1028
+ sum += s.severity * SMELL_WEIGHT[s.kind];
1029
+ return Math.min(100, sum);
1030
+ }
1031
+ async function runInventory(projectRoot) {
1032
+ await initParser();
1033
+ const abs = [];
1034
+ await collectFiles(projectRoot, projectRoot, abs);
1035
+ const fileSet = new Set(abs.map((f) => relative(projectRoot, f)));
1036
+ const basenameIndex = /* @__PURE__ */ new Map();
1037
+ for (const rel of fileSet) {
1038
+ const b = basename(rel).slice(0, basename(rel).length - extname(rel).length);
1039
+ if (!basenameIndex.has(b))
1040
+ basenameIndex.set(b, []);
1041
+ basenameIndex.get(b).push(rel);
1042
+ }
1043
+ const { stack, entrypoints } = await detectStackAndEntrypoints(projectRoot, abs);
1044
+ const work = [];
1045
+ for (const file of abs) {
1046
+ const rel = relative(projectRoot, file);
1047
+ const ext = extname(file);
1048
+ const lang = EXT_LANG[ext];
1049
+ if (!lang)
1050
+ continue;
1051
+ let source;
1052
+ try {
1053
+ source = await readFile4(file, "utf8");
1054
+ } catch {
1055
+ continue;
1056
+ }
1057
+ if (/if\s+__name__\s*==\s*['"]__main__['"]/.test(source) || /^#![^\n]*\b(node|python\d?)\b/.test(source)) {
1058
+ entrypoints.add(rel);
1059
+ }
1060
+ const tree = await parseAs(lang, source);
1061
+ if (!tree)
1062
+ continue;
1063
+ const ast = analyzeAst(source, lang, tree);
1064
+ const importSpecs = extractImports(source, lang);
1065
+ const frameworkRole = inferFrameworkRole(rel);
1066
+ const productDomain = inferProductDomain(rel, importSpecs);
1067
+ work.push({
1068
+ abs: file,
1069
+ rel,
1070
+ lang,
1071
+ source,
1072
+ ast,
1073
+ importSpecs,
1074
+ pathDemote: pathDemoteReason(rel),
1075
+ frameworkRole,
1076
+ productDomain
1077
+ });
1078
+ }
1079
+ const dir = join4(projectRoot, ".vibe-splainer");
1080
+ await mkdir3(dir, { recursive: true });
1081
+ const stage01 = {
1082
+ files: work.map((w) => ({
1083
+ absPath: w.abs,
1084
+ relPath: w.rel,
1085
+ language: w.lang,
1086
+ demoteReason: w.pathDemote
1087
+ })),
1088
+ totalCount: work.length,
1089
+ realSourceCount: work.filter((w) => !w.pathDemote).length
938
1090
  };
939
- if (lang === "python") {
940
- for (const c of root.children) {
941
- if (c.type === "function_definition" || c.type === "class_definition") {
942
- const name = c.childForFieldName("name")?.text;
943
- if (name && !name.startsWith("_"))
944
- push(name, c);
1091
+ await writeFile4(join4(dir, "stage-01-inventory.json"), JSON.stringify(stage01, null, 2), "utf8");
1092
+ const stage02 = Object.fromEntries(work.map((w) => [w.rel, w.frameworkRole]));
1093
+ await writeFile4(join4(dir, "stage-02-framework-roles.json"), JSON.stringify(stage02, null, 2), "utf8");
1094
+ const stage03 = Object.fromEntries(work.map((w) => [w.rel, w.productDomain]));
1095
+ await writeFile4(join4(dir, "stage-03-domains.json"), JSON.stringify(stage03, null, 2), "utf8");
1096
+ return { projectRoot, work, stack, entrypoints, fileSet, basenameIndex };
1097
+ }
1098
+
1099
+ // ../brain/dist/pipeline/resolution.js
1100
+ import { join as join5, dirname as dirname2, relative as relative2, extname as extname2, sep as sep2 } from "path";
1101
+ import { readFile as readFile5, writeFile as writeFile5, mkdir as mkdir4 } from "fs/promises";
1102
+ import { existsSync as existsSync3 } from "fs";
1103
+ function parseJsonLenient(text) {
1104
+ const stripped = text.replace(/\/\/[^\n]*/g, "").replace(/\/\*[\s\S]*?\*\//g, "");
1105
+ try {
1106
+ return JSON.parse(stripped);
1107
+ } catch {
1108
+ return null;
1109
+ }
1110
+ }
1111
+ async function extractTsConfigPaths(tsconfigPath, projectRoot, depth = 0) {
1112
+ if (depth > 3 || !existsSync3(tsconfigPath))
1113
+ return {};
1114
+ let raw;
1115
+ try {
1116
+ raw = await readFile5(tsconfigPath, "utf8");
1117
+ } catch {
1118
+ return {};
1119
+ }
1120
+ const parsed = parseJsonLenient(raw);
1121
+ if (!parsed)
1122
+ return {};
1123
+ const result = {};
1124
+ if (typeof parsed.extends === "string") {
1125
+ const baseFile = join5(dirname2(tsconfigPath), parsed.extends);
1126
+ const base = await extractTsConfigPaths(baseFile, projectRoot, depth + 1);
1127
+ Object.assign(result, base);
1128
+ }
1129
+ const opts = parsed.compilerOptions || {};
1130
+ const baseUrl = typeof opts.baseUrl === "string" ? join5(dirname2(tsconfigPath), opts.baseUrl) : dirname2(tsconfigPath);
1131
+ const paths = opts.paths || {};
1132
+ for (const [alias, targets] of Object.entries(paths)) {
1133
+ if (!Array.isArray(targets) || targets.length === 0)
1134
+ continue;
1135
+ const first = targets[0].replace(/\/\*$/, "");
1136
+ const resolved = relative2(projectRoot, join5(baseUrl, first));
1137
+ const key = alias.replace(/\/\*$/, "");
1138
+ result[key] = resolved;
1139
+ }
1140
+ return result;
1141
+ }
1142
+ async function discoverWorkspacePackages(projectRoot) {
1143
+ const packages = {};
1144
+ const pkgPath = join5(projectRoot, "package.json");
1145
+ if (!existsSync3(pkgPath))
1146
+ return packages;
1147
+ let rootPkg;
1148
+ try {
1149
+ rootPkg = JSON.parse(await readFile5(pkgPath, "utf8"));
1150
+ } catch {
1151
+ return packages;
1152
+ }
1153
+ const workspaces = rootPkg.workspaces;
1154
+ const globs = Array.isArray(workspaces) ? workspaces : Array.isArray(workspaces?.packages) ? workspaces.packages : [];
1155
+ for (const glob of globs) {
1156
+ const prefix = glob.replace(/\/\*$/, "");
1157
+ const absPrefix = join5(projectRoot, prefix);
1158
+ if (!existsSync3(absPrefix))
1159
+ continue;
1160
+ const { readdir: readdir2 } = await import("fs/promises");
1161
+ let entries = [];
1162
+ try {
1163
+ const dirents = await readdir2(absPrefix, { withFileTypes: true });
1164
+ entries = dirents.filter((d) => d.isDirectory()).map((d) => d.name);
1165
+ } catch {
1166
+ continue;
1167
+ }
1168
+ for (const entry of entries) {
1169
+ const wsPkgPath = join5(absPrefix, entry, "package.json");
1170
+ if (!existsSync3(wsPkgPath))
1171
+ continue;
1172
+ try {
1173
+ const wsPkg = JSON.parse(await readFile5(wsPkgPath, "utf8"));
1174
+ if (typeof wsPkg.name === "string") {
1175
+ packages[wsPkg.name] = relative2(projectRoot, join5(absPrefix, entry));
1176
+ }
1177
+ } catch {
1178
+ continue;
945
1179
  }
946
1180
  }
947
- return out;
948
1181
  }
949
- if (lang === "go") {
950
- const walk2 = (n) => {
951
- if (n.type === "function_declaration" || n.type === "method_declaration" || n.type === "type_declaration") {
952
- const name = n.childForFieldName("name")?.text;
953
- if (name && /^[A-Z]/.test(name))
954
- push(name, n);
1182
+ return packages;
1183
+ }
1184
+ async function discoverAppTsConfigPaths(projectRoot) {
1185
+ const result = {};
1186
+ const scanDirs = ["apps", "packages"];
1187
+ for (const scanDir of scanDirs) {
1188
+ const absDir = join5(projectRoot, scanDir);
1189
+ if (!existsSync3(absDir))
1190
+ continue;
1191
+ const { readdir: readdir2 } = await import("fs/promises");
1192
+ try {
1193
+ const entries = await readdir2(absDir, { withFileTypes: true });
1194
+ for (const entry of entries.filter((e) => e.isDirectory())) {
1195
+ const tsconfig = join5(absDir, entry.name, "tsconfig.json");
1196
+ const paths = await extractTsConfigPaths(tsconfig, projectRoot);
1197
+ Object.assign(result, paths);
955
1198
  }
956
- for (const c of n.children)
957
- walk2(c);
958
- };
959
- walk2(root);
960
- return out;
1199
+ } catch {
1200
+ continue;
1201
+ }
961
1202
  }
962
- if (lang === "rust") {
963
- const walk2 = (n) => {
964
- if (/_item$/.test(n.type) && n.children.some((c) => c.type === "visibility_modifier")) {
965
- const name = n.childForFieldName("name")?.text;
966
- push(name, n);
967
- }
968
- for (const c of n.children)
969
- walk2(c);
970
- };
971
- walk2(root);
972
- return out;
1203
+ return result;
1204
+ }
1205
+ var CONVENTIONAL_ALIASES = [
1206
+ { prefix: "~/", replacement: "" },
1207
+ { prefix: "@components/", replacement: "components/" },
1208
+ { prefix: "@lib/", replacement: "lib/" },
1209
+ { prefix: "@server/", replacement: "server/" },
1210
+ { prefix: "@calcom/web/", replacement: "" },
1211
+ { prefix: "@calcom/features/", replacement: "../packages/features/" },
1212
+ { prefix: "@calcom/lib/", replacement: "../packages/lib/" },
1213
+ { prefix: "@calcom/prisma/", replacement: "../packages/prisma/" },
1214
+ { prefix: "@calcom/trpc/", replacement: "../packages/trpc/" },
1215
+ { prefix: "@calcom/ui/", replacement: "../packages/ui/" },
1216
+ { prefix: "@calcom/emails/", replacement: "../packages/emails/" }
1217
+ ];
1218
+ async function buildAliasMap(projectRoot) {
1219
+ const rootPaths = await extractTsConfigPaths(join5(projectRoot, "tsconfig.json"), projectRoot);
1220
+ const workspacePackages = await discoverWorkspacePackages(projectRoot);
1221
+ const appPaths = await discoverAppTsConfigPaths(projectRoot);
1222
+ const resolvedAliases = { ...appPaths, ...rootPaths };
1223
+ for (const [pkgName, pkgDir] of Object.entries(workspacePackages)) {
1224
+ if (!(pkgName in resolvedAliases)) {
1225
+ resolvedAliases[pkgName] = pkgDir;
1226
+ }
973
1227
  }
974
- if (lang === "java") {
975
- const walk2 = (n) => {
976
- if ((n.type === "method_declaration" || n.type === "class_declaration") && /\bpublic\b/.test(firstLine(n.text))) {
977
- const name = n.childForFieldName("name")?.text;
978
- push(name, n);
1228
+ return { resolvedAliases, workspacePackages };
1229
+ }
1230
+ var JS_EXTS = [".ts", ".tsx", ".js", ".jsx", ".mjs", ".cjs"];
1231
+ function tryJsCandidates(base, projectRoot, fileSet) {
1232
+ const candidates = [];
1233
+ candidates.unshift(base);
1234
+ for (const ext of JS_EXTS)
1235
+ candidates.push(base + ext);
1236
+ for (const ext of JS_EXTS)
1237
+ candidates.push(join5(base, "index" + ext));
1238
+ for (const c of candidates) {
1239
+ const rel = relative2(projectRoot, c);
1240
+ if (fileSet.has(rel))
1241
+ return rel;
1242
+ }
1243
+ return null;
1244
+ }
1245
+ function resolvePython(spec, fromAbs, projectRoot, fileSet) {
1246
+ let modulePath;
1247
+ if (spec.startsWith(".")) {
1248
+ const dots = spec.match(/^\.+/)[0].length;
1249
+ let dir = dirname2(fromAbs);
1250
+ for (let i = 1; i < dots; i++)
1251
+ dir = dirname2(dir);
1252
+ const rest = spec.slice(dots).replace(/\./g, sep2);
1253
+ modulePath = rest ? join5(dir, rest) : dir;
1254
+ } else {
1255
+ modulePath = join5(projectRoot, spec.replace(/\./g, sep2));
1256
+ }
1257
+ for (const c of [modulePath + ".py", join5(modulePath, "__init__.py")]) {
1258
+ if (fileSet.has(relative2(projectRoot, c)))
1259
+ return relative2(projectRoot, c);
1260
+ }
1261
+ return null;
1262
+ }
1263
+ function resolveGeneric(spec, projectRoot, fileSet, basenameIndex) {
1264
+ const normalized = spec.replace(/^crate::/, "").replace(/::/g, "/").replace(/\./g, "/");
1265
+ const parts = normalized.split("/").filter(Boolean);
1266
+ if (parts.length === 0)
1267
+ return null;
1268
+ const last = parts[parts.length - 1];
1269
+ for (const rel of fileSet) {
1270
+ const noExt = rel.slice(0, rel.length - extname2(rel).length);
1271
+ if (noExt.endsWith(parts.join(sep2)))
1272
+ return rel;
1273
+ }
1274
+ const byBase = basenameIndex.get(last);
1275
+ if (byBase && byBase.length === 1)
1276
+ return byBase[0];
1277
+ return null;
1278
+ }
1279
+ function resolveImportWithAliasMap(spec, fromAbs, lang, projectRoot, fileSet, basenameIndex, aliasMap) {
1280
+ if (lang === "python") {
1281
+ return { resolved: resolvePython(spec, fromAbs, projectRoot, fileSet), isAlias: false };
1282
+ }
1283
+ if (lang === "typescript" || lang === "tsx" || lang === "javascript") {
1284
+ if (spec.startsWith(".")) {
1285
+ const base = join5(dirname2(fromAbs), spec);
1286
+ return { resolved: tryJsCandidates(base, projectRoot, fileSet), isAlias: false };
1287
+ }
1288
+ for (const [prefix, replacement] of Object.entries(aliasMap.resolvedAliases)) {
1289
+ if (spec === prefix || spec.startsWith(prefix + "/")) {
1290
+ const rest = spec.slice(prefix.length).replace(/^\//, "");
1291
+ const base = join5(projectRoot, replacement, rest);
1292
+ const resolved = tryJsCandidates(base, projectRoot, fileSet);
1293
+ return { resolved, isAlias: true, reason: resolved ? void 0 : `alias '${prefix}' found but path '${replacement}/${rest}' not in file set` };
979
1294
  }
980
- for (const c of n.children)
981
- walk2(c);
982
- };
983
- walk2(root);
984
- return out;
1295
+ }
1296
+ for (const [pkgName, pkgDir] of Object.entries(aliasMap.workspacePackages)) {
1297
+ if (spec === pkgName || spec.startsWith(pkgName + "/")) {
1298
+ const rest = spec.slice(pkgName.length).replace(/^\//, "");
1299
+ const base = join5(projectRoot, pkgDir, rest);
1300
+ const resolved = tryJsCandidates(base, projectRoot, fileSet);
1301
+ return { resolved, isAlias: true, reason: resolved ? void 0 : `workspace package '${pkgName}' found but subpath '${rest}' not in file set` };
1302
+ }
1303
+ }
1304
+ for (const { prefix, replacement } of CONVENTIONAL_ALIASES) {
1305
+ if (spec.startsWith(prefix)) {
1306
+ const rest = replacement + spec.slice(prefix.length);
1307
+ const base = join5(projectRoot, rest);
1308
+ const resolved = tryJsCandidates(base, projectRoot, fileSet);
1309
+ return { resolved, isAlias: true, reason: resolved ? void 0 : `conventional alias '${prefix}' \u2192 path not found` };
1310
+ }
1311
+ }
1312
+ return { resolved: null, isAlias: false };
985
1313
  }
986
- const walk = (n) => {
987
- if (n.type === "export_statement") {
988
- const decl = n.childForFieldName("declaration");
989
- if (decl) {
990
- const name = decl.childForFieldName("name")?.text;
991
- if (name)
992
- push(name, decl);
993
- for (const c of decl.namedChildren) {
994
- const dn = c.childForFieldName("name")?.text;
995
- if (dn)
996
- push(dn, c);
1314
+ return { resolved: resolveGeneric(spec, projectRoot, fileSet, basenameIndex), isAlias: false };
1315
+ }
1316
+ async function runResolution(projectRoot, inv) {
1317
+ const aliasMap = await buildAliasMap(projectRoot);
1318
+ const { work, fileSet, basenameIndex } = inv;
1319
+ const importedBy = /* @__PURE__ */ new Map();
1320
+ const importsResolved = /* @__PURE__ */ new Map();
1321
+ const importsUnresolved = /* @__PURE__ */ new Map();
1322
+ const fanOut = /* @__PURE__ */ new Map();
1323
+ for (const w of work) {
1324
+ importedBy.set(w.rel, /* @__PURE__ */ new Set());
1325
+ importsResolved.set(w.rel, /* @__PURE__ */ new Set());
1326
+ importsUnresolved.set(w.rel, /* @__PURE__ */ new Set());
1327
+ }
1328
+ const graph = { nodes: {}, edges: [] };
1329
+ for (const w of work) {
1330
+ graph.nodes[w.rel] = { imports: w.importSpecs };
1331
+ }
1332
+ const resolutionFailuresByFile = {};
1333
+ const resolutionFailureReasons = {};
1334
+ const unresolvedSet = /* @__PURE__ */ new Set();
1335
+ for (const w of work) {
1336
+ const distinctModules = /* @__PURE__ */ new Set();
1337
+ for (const spec of w.importSpecs) {
1338
+ distinctModules.add(spec);
1339
+ const { resolved, isAlias, reason } = resolveImportWithAliasMap(spec, w.abs, w.lang, projectRoot, fileSet, basenameIndex, aliasMap);
1340
+ if (resolved && resolved !== w.rel && importedBy.has(resolved)) {
1341
+ importedBy.get(resolved).add(w.rel);
1342
+ importsResolved.get(w.rel).add(resolved);
1343
+ graph.edges.push({ from: w.rel, to: resolved });
1344
+ } else if (resolved === null && isAlias) {
1345
+ importsUnresolved.get(w.rel).add(spec);
1346
+ unresolvedSet.add(spec);
1347
+ if (reason) {
1348
+ if (!resolutionFailuresByFile[w.rel])
1349
+ resolutionFailuresByFile[w.rel] = [];
1350
+ resolutionFailuresByFile[w.rel].push(spec);
1351
+ if (!resolutionFailureReasons[spec])
1352
+ resolutionFailureReasons[spec] = reason;
997
1353
  }
998
1354
  }
999
- for (const spec of n.descendantsOfType("export_specifier")) {
1000
- push(spec.childForFieldName("name")?.text, spec);
1001
- }
1002
- if (n.text.includes("export default"))
1003
- push("default", n);
1004
1355
  }
1005
- for (const c of n.children)
1006
- walk(c);
1356
+ fanOut.set(w.rel, distinctModules.size);
1357
+ }
1358
+ const unresolvedImports = [...unresolvedSet];
1359
+ const dir = join5(projectRoot, ".vibe-splainer");
1360
+ await mkdir4(dir, { recursive: true });
1361
+ const stage04 = {
1362
+ resolvedAliases: aliasMap.resolvedAliases,
1363
+ workspacePackages: aliasMap.workspacePackages,
1364
+ unresolvedImports,
1365
+ resolutionFailuresByFile,
1366
+ resolutionFailureReasons
1007
1367
  };
1008
- walk(root);
1009
- return out;
1368
+ await writeFile5(join5(dir, "stage-04-aliases.json"), JSON.stringify(stage04, null, 2), "utf8");
1369
+ return {
1370
+ aliasMap,
1371
+ importedBy,
1372
+ importsResolved,
1373
+ importsUnresolved,
1374
+ fanOut,
1375
+ graph,
1376
+ unresolvedImports,
1377
+ resolutionFailuresByFile,
1378
+ resolutionFailureReasons
1379
+ };
1380
+ }
1381
+
1382
+ // ../brain/dist/pipeline/classification.js
1383
+ import { join as join6, basename as basename2, extname as extname3, sep as sep3 } from "path";
1384
+ import { writeFile as writeFile6, mkdir as mkdir5 } from "fs/promises";
1385
+ function inferSideEffectProfile(source, importSpecs, productDomain, frameworkRole) {
1386
+ const effects = /* @__PURE__ */ new Set();
1387
+ if (/router\.(push|replace|back)\(|redirect\(|notFound\(|permanentRedirect\(/.test(source)) {
1388
+ effects.add("redirect");
1389
+ }
1390
+ if (/["']use server["']/.test(source))
1391
+ effects.add("server_action");
1392
+ if (/useMutation\b|\.mutate\b|\.mutateAsync\b/.test(source))
1393
+ effects.add("trpc_mutation");
1394
+ if (/sdkActionManager\.fire|telemetry\.|posthog\.|mixpanel\.|amplitude\.|ga\(/.test(source) || importSpecs.some((s) => /analytics|telemetry|posthog|mixpanel|amplitude/.test(s)))
1395
+ effects.add("analytics_event");
1396
+ if (/prisma\s*[.?]\s*\w+\s*[.?]\s*(create|update|upsert|delete|deleteMany|updateMany|createMany|transaction|executeRaw|queryRaw)\b/.test(source)) {
1397
+ effects.add("database_write");
1398
+ }
1399
+ if (/prisma\s*[.?]\s*\w+\s*[.?]\s*(findMany|findUnique|findFirst|findFirstOrThrow|findUniqueOrThrow|count|aggregate|groupBy)\b/.test(source)) {
1400
+ effects.add("database_read");
1401
+ }
1402
+ if (/createBooking|handleNewBooking|cancelBooking|rescheduleBooking|handleBooking|createRecurring/.test(source) || productDomain === "booking_creation" && /useMutation\b|\.mutate\b|\.mutateAsync\b/.test(source))
1403
+ effects.add("booking_mutation");
1404
+ if (/stripe\.webhooks\.(constructEvent|constructEventAsync)|webhookSecret|validateWebhook|verifyWebhook|verifySignature/.test(source) || productDomain === "payments_webhooks" && frameworkRole === "pages_api_route")
1405
+ effects.add("webhook_ingress");
1406
+ 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"))
1407
+ effects.add("payment_mutation");
1408
+ if (/signIn\b|signOut\b|createSession|destroySession|issueToken|refreshToken|getToken/.test(source)) {
1409
+ effects.add("auth_token_mutation");
1410
+ }
1411
+ if (/triggerWebhook|sendWebhook|webhook\.send\b/.test(source))
1412
+ effects.add("webhook_delivery");
1413
+ if (/sendEmail|sendMail\b|mailer\./.test(source) || importSpecs.some((s) => /nodemailer|resend|sendgrid|postmark|mailgun/.test(s)))
1414
+ effects.add("email_send");
1415
+ if (/createCalendarEvent|updateCalendarEvent|deleteCalendarEvent|calendar\.events\.(insert|update|delete|patch)/.test(source)) {
1416
+ effects.add("calendar_mutation");
1417
+ }
1418
+ if (/revalidatePath\b|revalidateTag\b/.test(source))
1419
+ effects.add("cache_revalidation");
1420
+ if (/localStorage\.|sessionStorage\./.test(source))
1421
+ effects.add("local_storage");
1422
+ if (/indexedDB\b|new Dexie|idb\./.test(source))
1423
+ effects.add("indexed_db");
1424
+ if (/\bfetch\s*\(|axios\.(get|post|put|patch|delete)\b/.test(source)) {
1425
+ effects.add("external_api_call");
1426
+ }
1427
+ if (effects.size === 0)
1428
+ effects.add("none_detected");
1429
+ return [...effects];
1430
+ }
1431
+ function inferWriteIntents(productDomain, relPath, sideEffectProfile) {
1432
+ const intents = [];
1433
+ if (productDomain === "booking_creation") {
1434
+ intents.push("create_booking");
1435
+ if (relPath.includes("reschedule") || relPath.includes("Reschedule"))
1436
+ intents.push("reschedule_booking");
1437
+ if (relPath.includes("recurring") || relPath.includes("Recurring"))
1438
+ intents.push("create_recurring_booking");
1439
+ }
1440
+ if (productDomain === "booking_management")
1441
+ intents.push("cancel_booking");
1442
+ if (productDomain === "event_type_configuration")
1443
+ intents.push("update_event_type");
1444
+ if (productDomain === "availability")
1445
+ intents.push("update_availability");
1446
+ if (productDomain === "payments")
1447
+ intents.push("create_payment");
1448
+ if (productDomain === "payments_webhooks")
1449
+ intents.push("handle_payment_webhook");
1450
+ if (productDomain === "auth_oauth") {
1451
+ intents.push("issue_auth_token");
1452
+ intents.push("refresh_auth_token");
1453
+ }
1454
+ if (sideEffectProfile.includes("webhook_delivery"))
1455
+ intents.push("send_webhook");
1456
+ if (productDomain === "settings")
1457
+ intents.push("update_user_settings");
1458
+ if (sideEffectProfile.includes("local_storage") || sideEffectProfile.includes("indexed_db")) {
1459
+ intents.push("persist_local_state");
1460
+ }
1461
+ return intents.length > 0 ? intents : ["none_detected"];
1462
+ }
1463
+ var ENTRYPOINT_ROLES = /* @__PURE__ */ new Set([
1464
+ "app_route_page",
1465
+ "app_route_handler",
1466
+ "pages_route",
1467
+ "pages_api_route",
1468
+ "trpc_api_route"
1469
+ ]);
1470
+ function inferRiskTypesPass1(rel, frameworkRole, productDomain, sideEffectProfile, gravitySignals, smellKinds) {
1471
+ const types = [];
1472
+ const smThreshold = ["provider", "store"].includes(frameworkRole) ? 8 : 20;
1473
+ if (gravitySignals.cyclomatic > smThreshold)
1474
+ types.push("state_machine");
1475
+ if (smellKinds.has("god-file")) {
1476
+ if (frameworkRole === "hook")
1477
+ types.push("god_hook");
1478
+ else
1479
+ types.push("god_component");
1480
+ }
1481
+ if (sideEffectProfile.length > 3 && !sideEffectProfile.includes("none_detected")) {
1482
+ types.push("side_effect_coupling");
1483
+ }
1484
+ if (productDomain === "forms" && (gravitySignals.fanIn > 3 || gravitySignals.publicSurface > 5))
1485
+ types.push("registry_bottleneck");
1486
+ if (sideEffectProfile.some((s) => ["booking_mutation", "payment_mutation", "auth_token_mutation"].includes(s)) && gravitySignals.cyclomatic > 10)
1487
+ types.push("mutation_orchestration");
1488
+ if (ENTRYPOINT_ROLES.has(frameworkRole) && sideEffectProfile.includes("database_write")) {
1489
+ types.push("route_handler_write_path");
1490
+ }
1491
+ if (smellKinds.has("swallowed-catch"))
1492
+ types.push("error_swallowing");
1493
+ if (sideEffectProfile.includes("local_storage") || sideEffectProfile.includes("indexed_db")) {
1494
+ types.push("storage_persistence_risk");
1495
+ }
1496
+ return types;
1497
+ }
1498
+ var DOMAIN_SURFACE_PATTERNS = {
1499
+ booking_creation: {
1500
+ expected: [/book/i, /booking/i, /reschedule/i, /booking-success/i, /api\/book/i, /create-booking/i],
1501
+ wrong: [/event-type/i, /event-types/i, /eventtypes/i, /availability/i, /schedule/i]
1502
+ },
1503
+ payments_webhooks: {
1504
+ expected: [/webhook/i, /stripe/i, /payment/i],
1505
+ wrong: [/settings/i, /onboarding/i, /profile/i]
1506
+ },
1507
+ auth_oauth: {
1508
+ expected: [/oauth/i, /callback/i, /auth/i, /signin/i, /login/i],
1509
+ wrong: [/booking/i, /payment/i, /settings/i]
1510
+ }
1511
+ };
1512
+ function findRuntimeEntrypoints(relPath, importedByMap, persisted, maxDepth = 8) {
1513
+ const results = [];
1514
+ const seen = /* @__PURE__ */ new Set();
1515
+ const queue = [{ path: relPath, depth: 0 }];
1516
+ while (queue.length > 0) {
1517
+ const current = queue.shift();
1518
+ if (seen.has(current.path))
1519
+ continue;
1520
+ seen.add(current.path);
1521
+ if (current.path !== relPath) {
1522
+ const meta = persisted.get(current.path);
1523
+ if (meta && ENTRYPOINT_ROLES.has(meta.frameworkRole)) {
1524
+ results.push({
1525
+ path: current.path,
1526
+ frameworkRole: meta.frameworkRole,
1527
+ productDomain: meta.productDomain,
1528
+ distance: current.depth
1529
+ });
1530
+ if (results.length >= 8)
1531
+ break;
1532
+ continue;
1533
+ }
1534
+ }
1535
+ if (current.depth >= maxDepth)
1536
+ continue;
1537
+ const importers = importedByMap.get(current.path);
1538
+ if (!importers)
1539
+ continue;
1540
+ for (const importer of importers) {
1541
+ if (!seen.has(importer))
1542
+ queue.push({ path: importer, depth: current.depth + 1 });
1543
+ }
1544
+ }
1545
+ const byPath = /* @__PURE__ */ new Map();
1546
+ for (const r of results) {
1547
+ const existing = byPath.get(r.path);
1548
+ if (!existing || r.distance < existing.distance)
1549
+ byPath.set(r.path, r);
1550
+ }
1551
+ return [...byPath.values()].sort((a, b) => a.distance - b.distance);
1552
+ }
1553
+ function deriveEntrypointTraceStatus(domain, entrypoints, unresolved) {
1554
+ if (entrypoints.length === 0 && unresolved.length > 0)
1555
+ return "blocked_by_alias_resolution";
1556
+ if (entrypoints.length === 0)
1557
+ return "no_runtime_entrypoint_found";
1558
+ const patterns = DOMAIN_SURFACE_PATTERNS[domain];
1559
+ if (patterns) {
1560
+ const allWrong = entrypoints.every((e) => patterns.wrong.some((p) => p.test(e.path)) && !patterns.expected.some((p) => p.test(e.path)));
1561
+ if (allWrong)
1562
+ return "partial_wrong_surface";
1563
+ }
1564
+ return unresolved.length === 0 ? "complete" : "partial";
1565
+ }
1566
+ function computeLoadBearingScore(gravity, heat, importedByCount, sideEffectProfile, productDomain, smellMaxSeverity, runtimeEntrypoints) {
1567
+ let score = 0;
1568
+ if (gravity >= 85)
1569
+ score += 2;
1570
+ if (heat >= 60)
1571
+ score += 1;
1572
+ if (runtimeEntrypoints.length >= 2)
1573
+ score += 2;
1574
+ if (importedByCount >= 3)
1575
+ score += 1;
1576
+ if (sideEffectProfile.includes("database_write"))
1577
+ score += 3;
1578
+ if (sideEffectProfile.includes("booking_mutation"))
1579
+ score += 3;
1580
+ if (sideEffectProfile.includes("payment_mutation"))
1581
+ score += 3;
1582
+ if (sideEffectProfile.includes("auth_token_mutation"))
1583
+ score += 3;
1584
+ if (sideEffectProfile.includes("webhook_delivery"))
1585
+ score += 2;
1586
+ if (sideEffectProfile.includes("webhook_ingress"))
1587
+ score += 2;
1588
+ if (sideEffectProfile.includes("calendar_mutation"))
1589
+ score += 2;
1590
+ if (sideEffectProfile.includes("redirect"))
1591
+ score += 1;
1592
+ if (sideEffectProfile.includes("analytics_event"))
1593
+ score += 1;
1594
+ const highImpactDomains = [
1595
+ "booking_creation",
1596
+ "payments",
1597
+ "auth_oauth",
1598
+ "webhooks",
1599
+ "payments_webhooks"
1600
+ ];
1601
+ if (highImpactDomains.includes(productDomain))
1602
+ score += 2;
1603
+ if (smellMaxSeverity === 5)
1604
+ score += 3;
1605
+ return score;
1010
1606
  }
1011
1607
  function pageRank(nodes, outEdges, damping = 0.85, iters = 20) {
1012
1608
  const n = nodes.length;
@@ -1053,10 +1649,9 @@ function pageRank(nodes, outEdges, damping = 0.85, iters = 20) {
1053
1649
  function detectCommunities(nodes, adjacency) {
1054
1650
  const label = /* @__PURE__ */ new Map();
1055
1651
  nodes.forEach((node, i) => label.set(node, i));
1056
- const order = [...nodes];
1057
1652
  for (let pass = 0; pass < 10; pass++) {
1058
1653
  let changed = false;
1059
- for (const node of order) {
1654
+ for (const node of nodes) {
1060
1655
  const neighbors = adjacency.get(node);
1061
1656
  if (!neighbors || neighbors.size === 0)
1062
1657
  continue;
@@ -1082,164 +1677,174 @@ function detectCommunities(nodes, adjacency) {
1082
1677
  }
1083
1678
  return label;
1084
1679
  }
1085
- async function detectStackAndEntrypoints(projectRoot, files) {
1086
- const stack = /* @__PURE__ */ new Set();
1087
- const entrypoints = /* @__PURE__ */ new Set();
1088
- const rel = (abs) => relative(projectRoot, abs);
1089
- const pkgPath = join4(projectRoot, "package.json");
1090
- if (existsSync2(pkgPath)) {
1091
- try {
1092
- const pkg = JSON.parse(await readFile4(pkgPath, "utf8"));
1093
- stack.add("Node.js");
1094
- const deps = { ...pkg.dependencies || {}, ...pkg.devDependencies || {} };
1095
- for (const known of ["react", "next", "vue", "svelte", "express", "fastify", "typescript", "vite"]) {
1096
- if (deps[known])
1097
- stack.add(known === "next" ? "Next.js" : known[0].toUpperCase() + known.slice(1));
1098
- }
1099
- const addEntry = (p) => {
1100
- if (!p)
1101
- return;
1102
- const abs = join4(projectRoot, p);
1103
- const r = relative(projectRoot, abs);
1104
- if (files.includes(abs))
1105
- entrypoints.add(r);
1106
- };
1107
- addEntry(pkg.main);
1108
- if (typeof pkg.bin === "string")
1109
- addEntry(pkg.bin);
1110
- else if (pkg.bin)
1111
- for (const v of Object.values(pkg.bin))
1112
- addEntry(v);
1113
- } catch {
1114
- }
1115
- }
1116
- const pyproject = join4(projectRoot, "pyproject.toml");
1117
- const setupPy = join4(projectRoot, "setup.py");
1118
- const requirements = join4(projectRoot, "requirements.txt");
1119
- if (existsSync2(pyproject) || existsSync2(setupPy) || existsSync2(requirements)) {
1120
- stack.add("Python");
1121
- let reqText = "";
1122
- for (const f of [pyproject, requirements]) {
1123
- if (existsSync2(f)) {
1124
- try {
1125
- reqText += await readFile4(f, "utf8");
1126
- } catch {
1127
- }
1128
- }
1129
- }
1130
- for (const known of ["pygame", "PySide6", "PyQt5", "PyQt6", "flask", "django", "fastapi", "numpy", "pandas", "torch", "tensorflow"]) {
1131
- if (new RegExp(known, "i").test(reqText))
1132
- stack.add(known);
1680
+ function titleCase(s) {
1681
+ return s.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
1682
+ }
1683
+ function domainToGroupLabel(domain) {
1684
+ const labels = {
1685
+ booking_creation: "Booking",
1686
+ booking_management: "Booking",
1687
+ booking_audit: "Booking Audit",
1688
+ event_type_configuration: "Event Types",
1689
+ availability: "Availability",
1690
+ auth: "Auth",
1691
+ auth_oauth: "Auth OAuth",
1692
+ payments: "Payments",
1693
+ payments_webhooks: "Payment Webhooks",
1694
+ webhooks: "Webhooks",
1695
+ apps_marketplace: "Apps",
1696
+ calendar_integrations: "Calendar",
1697
+ video: "Video",
1698
+ onboarding: "Onboarding",
1699
+ settings: "Settings",
1700
+ admin: "Admin",
1701
+ data_table: "Data Table",
1702
+ shell_navigation: "Shell",
1703
+ forms: "Forms",
1704
+ embed: "Embed",
1705
+ notifications: "Notifications"
1706
+ };
1707
+ return labels[domain] || titleCase(domain.replace(/_/g, " "));
1708
+ }
1709
+ function pillarNameFromCluster(files) {
1710
+ const hintCounts = /* @__PURE__ */ new Map();
1711
+ for (const f of files) {
1712
+ if (f.pillarHint && !f.pillarHint.startsWith("community-")) {
1713
+ hintCounts.set(f.pillarHint, (hintCounts.get(f.pillarHint) || 0) + 1);
1133
1714
  }
1134
1715
  }
1135
- if (existsSync2(join4(projectRoot, "go.mod")))
1136
- stack.add("Go");
1137
- if (existsSync2(join4(projectRoot, "Cargo.toml")))
1138
- stack.add("Rust");
1139
- if (existsSync2(join4(projectRoot, "pom.xml")) || existsSync2(join4(projectRoot, "build.gradle")))
1140
- stack.add("Java");
1141
- for (const abs of files) {
1142
- const r = rel(abs);
1143
- const base = basename(r);
1144
- if (base === "main.py" || base === "__main__.py")
1145
- entrypoints.add(r);
1146
- if (/^index\.(ts|tsx|js|jsx|mjs|cjs)$/.test(base) && dirname(r).split(sep).length <= 2)
1147
- entrypoints.add(r);
1148
- if (base === "main.go" && r.includes("cmd" + sep))
1149
- entrypoints.add(r);
1150
- if (base === "main.go" && !r.includes(sep))
1151
- entrypoints.add(r);
1152
- if (base === "main.rs" || base === "lib.rs")
1153
- entrypoints.add(r);
1716
+ if (hintCounts.size > 0) {
1717
+ const best = [...hintCounts.entries()].sort((a, b) => b[1] - a[1])[0];
1718
+ if (best[1] >= files.length * 0.4)
1719
+ return best[0];
1154
1720
  }
1155
- if (stack.has("Next.js")) {
1156
- const appRouterNames = /* @__PURE__ */ new Set(["page", "layout", "route", "loading", "error", "not-found", "template", "default"]);
1157
- for (const abs of files) {
1158
- const r = rel(abs);
1159
- const stem = basename(r, extname(r));
1160
- const underApp = /(?:^|[/\\])app[/\\]/.test(r);
1161
- const underPages = /(?:^|[/\\])pages[/\\]/.test(r);
1162
- if (underApp && appRouterNames.has(stem))
1163
- entrypoints.add(r);
1164
- if (underPages && !stem.startsWith("_"))
1165
- entrypoints.add(r);
1721
+ const dirs = files.map((f) => dirname_simple(f.rel)).filter((d) => d && d !== ".");
1722
+ if (dirs.length) {
1723
+ const segCounts = /* @__PURE__ */ new Map();
1724
+ for (const d of dirs) {
1725
+ const segments = d.split(sep3).filter((s) => !MEANINGLESS_SEGMENTS.has(s.toLowerCase()));
1726
+ const meaningful = segments.pop();
1727
+ if (meaningful)
1728
+ segCounts.set(meaningful, (segCounts.get(meaningful) || 0) + 1);
1166
1729
  }
1730
+ const top = [...segCounts.entries()].sort((a, b) => b[1] - a[1])[0];
1731
+ if (top)
1732
+ return titleCase(top[0]);
1167
1733
  }
1168
- return { stack: [...stack], entrypoints };
1734
+ const topFile = basename2(files[0].rel, extname3(files[0].rel));
1735
+ return titleCase(topFile);
1169
1736
  }
1170
- var SMELL_WEIGHT = {
1171
- "todo": 3,
1172
- "suppression": 5,
1173
- "swallowed-catch": 10,
1174
- "deep-nesting": 6,
1175
- "long-function": 5,
1176
- "magic-number": 3,
1177
- "god-file": 14
1178
- };
1179
- function computeHeat(smells) {
1180
- let sum = 0;
1181
- for (const s of smells)
1182
- sum += s.severity * SMELL_WEIGHT[s.kind];
1183
- return Math.min(100, sum);
1737
+ function dirname_simple(p) {
1738
+ const idx = p.lastIndexOf(sep3);
1739
+ if (idx < 0)
1740
+ return ".";
1741
+ return p.slice(0, idx);
1184
1742
  }
1185
- async function scanProject(projectRoot) {
1186
- await initParser();
1187
- const abs = [];
1188
- await collectFiles(projectRoot, projectRoot, abs);
1189
- const fileSet = new Set(abs.map((f) => relative(projectRoot, f)));
1190
- const basenameIndex = /* @__PURE__ */ new Map();
1191
- for (const rel of fileSet) {
1192
- const b = basename(rel).slice(0, basename(rel).length - extname(rel).length);
1193
- if (!basenameIndex.has(b))
1194
- basenameIndex.set(b, []);
1195
- basenameIndex.get(b).push(rel);
1743
+ function buildPillars(classified, communities) {
1744
+ const real = classified.filter((f) => f.isRealSource);
1745
+ const keywordGroups = /* @__PURE__ */ new Map();
1746
+ const unlabeled = [];
1747
+ for (const f of real) {
1748
+ if (f.pillarHint && !f.pillarHint.startsWith("community-")) {
1749
+ if (!keywordGroups.has(f.pillarHint))
1750
+ keywordGroups.set(f.pillarHint, []);
1751
+ keywordGroups.get(f.pillarHint).push(f);
1752
+ } else {
1753
+ unlabeled.push(f);
1754
+ }
1196
1755
  }
1197
- const { stack, entrypoints } = await detectStackAndEntrypoints(projectRoot, abs);
1198
- const work = [];
1199
- const graph = { nodes: {}, edges: [] };
1200
- for (const file of abs) {
1201
- const rel = relative(projectRoot, file);
1202
- const ext = extname(file);
1203
- const lang = EXT_LANG[ext];
1204
- if (!lang)
1205
- continue;
1206
- let source;
1207
- try {
1208
- source = await readFile4(file, "utf8");
1209
- } catch {
1210
- continue;
1756
+ const pillars = [];
1757
+ for (const [name, files] of keywordGroups) {
1758
+ const sorted = [...files].sort((a, b) => b.gravity - a.gravity);
1759
+ pillars.push({
1760
+ name,
1761
+ description: `${name} subsystem: ${files.length} file${files.length > 1 ? "s" : ""} centered on ${basename2(sorted[0].rel)}.`,
1762
+ memberFiles: sorted.map((f) => f.rel)
1763
+ });
1764
+ }
1765
+ if (unlabeled.length > 0) {
1766
+ const communityGroups = /* @__PURE__ */ new Map();
1767
+ for (const f of unlabeled) {
1768
+ const c = communities.get(f.rel);
1769
+ if (c === void 0)
1770
+ continue;
1771
+ if (!communityGroups.has(c))
1772
+ communityGroups.set(c, []);
1773
+ communityGroups.get(c).push(f);
1211
1774
  }
1212
- if (/if\s+__name__\s*==\s*['"]__main__['"]/.test(source) || /^#![^\n]*\b(node|python\d?)\b/.test(source)) {
1213
- entrypoints.add(rel);
1775
+ const remainingSlots = Math.max(0, 6 - pillars.length);
1776
+ 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);
1777
+ for (const g of sorted) {
1778
+ const top = [...g.files].sort((a, b) => b.gravity - a.gravity);
1779
+ const name = pillarNameFromCluster(top.map((f) => ({ rel: f.rel, pillarHint: f.pillarHint })));
1780
+ const existing = pillars.find((p) => p.name === name);
1781
+ if (existing) {
1782
+ existing.memberFiles.push(...top.map((f) => f.rel));
1783
+ existing.description = `${name} subsystem: ${existing.memberFiles.length} files.`;
1784
+ } else {
1785
+ pillars.push({
1786
+ name,
1787
+ description: `${g.files.length} files centered on ${basename2(top[0].rel)}.`,
1788
+ memberFiles: top.map((f) => f.rel)
1789
+ });
1790
+ }
1214
1791
  }
1215
- const tree = await parseAs(lang, source);
1216
- if (!tree)
1217
- continue;
1218
- const ast = analyzeAst(source, lang, tree);
1219
- const importSpecs = extractImports(source, lang);
1220
- graph.nodes[rel] = { imports: importSpecs };
1221
- work.push({ abs: file, rel, lang, source, ast, importSpecs, pathDemote: pathDemoteReason(rel) });
1222
1792
  }
1223
- const importedBy = /* @__PURE__ */ new Map();
1224
- const importsResolved = /* @__PURE__ */ new Map();
1225
- const fanOut = /* @__PURE__ */ new Map();
1226
- for (const w of work) {
1227
- importedBy.set(w.rel, /* @__PURE__ */ new Set());
1228
- importsResolved.set(w.rel, /* @__PURE__ */ new Set());
1793
+ pillars.sort((a, b) => {
1794
+ const gravA = real.filter((f) => a.memberFiles.includes(f.rel)).reduce((s, f) => s + f.gravity, 0);
1795
+ const gravB = real.filter((f) => b.memberFiles.includes(f.rel)).reduce((s, f) => s + f.gravity, 0);
1796
+ return gravB - gravA;
1797
+ });
1798
+ if (pillars.length === 0 && real.length > 0) {
1799
+ pillars.push({ name: "Core", description: "Primary application code.", memberFiles: real.slice(0, 20).map((f) => f.rel) });
1229
1800
  }
1230
- for (const w of work) {
1231
- const distinctModules = /* @__PURE__ */ new Set();
1232
- for (const spec of w.importSpecs) {
1233
- distinctModules.add(spec);
1234
- const target = resolveImport(spec, w.abs, w.lang, projectRoot, fileSet, basenameIndex);
1235
- if (target && target !== w.rel && importedBy.has(target)) {
1236
- importedBy.get(target).add(w.rel);
1237
- importsResolved.get(w.rel).add(target);
1238
- graph.edges.push({ from: w.rel, to: target });
1801
+ const finalPillars = [];
1802
+ for (const p of pillars) {
1803
+ if (p.memberFiles.length > 15) {
1804
+ const groups = /* @__PURE__ */ new Map();
1805
+ for (const rel of p.memberFiles) {
1806
+ const f = classified.find((c) => c.rel === rel);
1807
+ const role = f?.frameworkRole || "unknown";
1808
+ const domain = f?.productDomain || "unknown";
1809
+ let bucket;
1810
+ if (domain !== "unknown" && domain !== "routing_infrastructure" && domain !== "test_infrastructure" && domain !== "generated_noise") {
1811
+ bucket = domainToGroupLabel(domain);
1812
+ } else if (role === "hook") {
1813
+ bucket = "Hooks";
1814
+ } else if (["app_route_page", "app_route_handler", "app_route_layout", "pages_route", "pages_api_route", "trpc_api_route"].includes(role)) {
1815
+ bucket = "Routes";
1816
+ } else if (role === "component") {
1817
+ bucket = "Components";
1818
+ } else {
1819
+ bucket = "Logic";
1820
+ }
1821
+ const key = `${p.name} (${bucket})`;
1822
+ if (!groups.has(key))
1823
+ groups.set(key, []);
1824
+ groups.get(key).push(rel);
1825
+ }
1826
+ for (const [key, files] of groups) {
1827
+ if (files.length > 0)
1828
+ finalPillars.push({ name: key, description: `Subdivided from ${p.name}`, memberFiles: files });
1239
1829
  }
1830
+ } else {
1831
+ finalPillars.push(p);
1240
1832
  }
1241
- fanOut.set(w.rel, distinctModules.size);
1242
1833
  }
1834
+ const seen = /* @__PURE__ */ new Set();
1835
+ for (const p of finalPillars) {
1836
+ let n = p.name, i = 2;
1837
+ while (seen.has(n)) {
1838
+ n = `${p.name} ${i++}`;
1839
+ }
1840
+ p.name = n;
1841
+ seen.add(n);
1842
+ }
1843
+ return finalPillars;
1844
+ }
1845
+ async function runClassification(projectRoot, inv, res) {
1846
+ const { work, entrypoints } = inv;
1847
+ const { importedBy, importsResolved, importsUnresolved, fanOut } = res;
1243
1848
  const isRealSource = /* @__PURE__ */ new Map();
1244
1849
  const demoteReason = /* @__PURE__ */ new Map();
1245
1850
  for (const w of work) {
@@ -1256,7 +1861,7 @@ async function scanProject(projectRoot) {
1256
1861
  continue;
1257
1862
  if (entrypoints.has(w.rel))
1258
1863
  continue;
1259
- const inbound = [...importedBy.get(w.rel)].filter((src) => isRealSource.get(src));
1864
+ const inbound = [...importedBy.get(w.rel) || []].filter((src) => isRealSource.get(src));
1260
1865
  if (inbound.length === 0) {
1261
1866
  isRealSource.set(w.rel, false);
1262
1867
  demoteReason.set(w.rel, "no inbound references from application code");
@@ -1273,12 +1878,12 @@ async function scanProject(projectRoot) {
1273
1878
  for (const w of work) {
1274
1879
  if (!realSet.has(w.rel))
1275
1880
  continue;
1276
- for (const target of importsResolved.get(w.rel)) {
1881
+ for (const target of importsResolved.get(w.rel) || /* @__PURE__ */ new Set()) {
1277
1882
  if (!realSet.has(target))
1278
1883
  continue;
1279
1884
  outEdges.get(w.rel).add(target);
1280
- const wDir = w.rel.split(sep)[0];
1281
- const tDir = target.split(sep)[0];
1885
+ const wDir = w.rel.split(sep3)[0];
1886
+ const tDir = target.split(sep3)[0];
1282
1887
  const weight = wDir === tDir ? 1 : 0.5;
1283
1888
  undirected.get(w.rel).set(target, weight);
1284
1889
  undirected.get(target).set(w.rel, weight);
@@ -1286,13 +1891,26 @@ async function scanProject(projectRoot) {
1286
1891
  }
1287
1892
  const ranks = pageRank(realNodes, outEdges);
1288
1893
  const communities = detectCommunities(realNodes, undirected);
1289
- const analyses = [];
1290
- const persisted = {};
1894
+ const metaLookup = /* @__PURE__ */ new Map();
1895
+ const sideEffectsByFile = /* @__PURE__ */ new Map();
1896
+ const writeIntentsByFile = /* @__PURE__ */ new Map();
1897
+ for (const w of work) {
1898
+ const effects = inferSideEffectProfile(w.source, w.importSpecs, w.productDomain, w.frameworkRole);
1899
+ sideEffectsByFile.set(w.rel, effects);
1900
+ writeIntentsByFile.set(w.rel, inferWriteIntents(w.productDomain, w.rel, effects));
1901
+ metaLookup.set(w.rel, { frameworkRole: w.frameworkRole, productDomain: w.productDomain });
1902
+ }
1903
+ const riskTypesByFile = /* @__PURE__ */ new Map();
1904
+ const gravityByFile = /* @__PURE__ */ new Map();
1905
+ const heatByFile = /* @__PURE__ */ new Map();
1906
+ const fanInByFile = /* @__PURE__ */ new Map();
1907
+ const centralityByFile = /* @__PURE__ */ new Map();
1908
+ const gravitySignalsByFile = /* @__PURE__ */ new Map();
1291
1909
  for (const w of work) {
1292
1910
  const real = isRealSource.get(w.rel);
1293
- const fanIn = [...importedBy.get(w.rel)].filter((src) => isRealSource.get(src)).length;
1911
+ const fanIn = [...importedBy.get(w.rel) || []].filter((src) => isRealSource.get(src)).length;
1294
1912
  const centrality = real ? ranks.get(w.rel) || 0 : 0;
1295
- const gravitySignals = {
1913
+ const gs = {
1296
1914
  fanIn,
1297
1915
  fanOut: fanOut.get(w.rel) || 0,
1298
1916
  centrality,
@@ -1307,7 +1925,54 @@ async function scanProject(projectRoot) {
1307
1925
  if (!real)
1308
1926
  gravityRaw *= 0.2;
1309
1927
  const gravity = Math.max(0, Math.min(100, gravityRaw));
1310
- const heatSignals = {
1928
+ const heat = real ? computeHeat(w.ast.smells) : 0;
1929
+ gravityByFile.set(w.rel, gravity);
1930
+ heatByFile.set(w.rel, heat);
1931
+ fanInByFile.set(w.rel, fanIn);
1932
+ centralityByFile.set(w.rel, centrality);
1933
+ gravitySignalsByFile.set(w.rel, gs);
1934
+ }
1935
+ for (const w of work) {
1936
+ const gs = gravitySignalsByFile.get(w.rel);
1937
+ const smellKinds = new Set(w.ast.smells.map((s) => s.kind));
1938
+ const effects = sideEffectsByFile.get(w.rel);
1939
+ const types = inferRiskTypesPass1(w.rel, w.frameworkRole, w.productDomain, effects, gs, smellKinds);
1940
+ riskTypesByFile.set(w.rel, types);
1941
+ }
1942
+ for (const w of work) {
1943
+ if (w.productDomain === "forms" && (w.frameworkRole === "component" || w.frameworkRole === "hook")) {
1944
+ const importsResolved_w = importsResolved.get(w.rel) || /* @__PURE__ */ new Set();
1945
+ const importsAny = [...importsResolved_w, ...w.importSpecs.filter((s) => s.startsWith("@"))];
1946
+ const consumesBottleneck = importsAny.some((dep) => {
1947
+ const types2 = riskTypesByFile.get(dep);
1948
+ return types2?.includes("registry_bottleneck");
1949
+ });
1950
+ if (consumesBottleneck) {
1951
+ const existing = riskTypesByFile.get(w.rel);
1952
+ if (!existing.includes("registry_consumer"))
1953
+ existing.push("registry_consumer");
1954
+ if (!existing.includes("type_boundary_leak"))
1955
+ existing.push("type_boundary_leak");
1956
+ const idx = existing.indexOf("complexity_hotspot");
1957
+ if (idx >= 0)
1958
+ existing.splice(idx, 1);
1959
+ }
1960
+ }
1961
+ const types = riskTypesByFile.get(w.rel);
1962
+ if (types.length === 0)
1963
+ types.push("complexity_hotspot");
1964
+ }
1965
+ const classified = [];
1966
+ for (const w of work) {
1967
+ const real = isRealSource.get(w.rel);
1968
+ const fanIn = fanInByFile.get(w.rel);
1969
+ const gravity = gravityByFile.get(w.rel);
1970
+ const heat = heatByFile.get(w.rel);
1971
+ const gs = gravitySignalsByFile.get(w.rel);
1972
+ const effects = sideEffectsByFile.get(w.rel);
1973
+ const writeIntents = writeIntentsByFile.get(w.rel);
1974
+ const riskTypes = riskTypesByFile.get(w.rel);
1975
+ const hs = {
1311
1976
  todos: w.ast.smells.filter((s) => s.kind === "todo").length,
1312
1977
  suppressions: w.ast.smells.filter((s) => s.kind === "suppression").length,
1313
1978
  swallowedCatches: w.ast.swallowedCatches,
@@ -1315,208 +1980,532 @@ async function scanProject(projectRoot) {
1315
1980
  longFunctions: w.ast.longFunctions,
1316
1981
  magicNumbers: w.ast.magicNumbers
1317
1982
  };
1318
- const heat = real ? computeHeat(w.ast.smells) : 0;
1319
1983
  const keywordPillar = matchPillarByImports(w.importSpecs);
1320
1984
  const pathPillar = matchPillarByPath(w.rel);
1321
1985
  const pillarHint = real ? keywordPillar || pathPillar || `community-${communities.get(w.rel)}` : null;
1322
- const fa = {
1323
- path: w.abs,
1324
- relativePath: w.rel,
1325
- language: w.lang,
1326
- isRealSource: real,
1327
- demoteReason: demoteReason.get(w.rel) || null,
1328
- gravity,
1329
- heat,
1330
- gravitySignals,
1331
- heatSignals,
1332
- smells: w.ast.smells,
1333
- pillarHint
1334
- };
1335
- analyses.push(fa);
1336
- persisted[w.rel] = {
1337
- relativePath: w.rel,
1338
- language: w.lang,
1986
+ const importedByReal = [...importedBy.get(w.rel) || []].filter((src) => isRealSource.get(src));
1987
+ const imports = [...importsResolved.get(w.rel) || /* @__PURE__ */ new Set()];
1988
+ const importsUnresolvedArr = [...importsUnresolved.get(w.rel) || /* @__PURE__ */ new Set()];
1989
+ const runtimeEntrypoints = findRuntimeEntrypoints(w.rel, importedBy, metaLookup);
1990
+ const entrypointTraceStatus = deriveEntrypointTraceStatus(w.productDomain, runtimeEntrypoints, importsUnresolvedArr);
1991
+ const smellMaxSeverity = w.ast.smells.length > 0 ? Math.max(...w.ast.smells.map((s) => s.severity)) : 0;
1992
+ const loadBearingScore = computeLoadBearingScore(gravity, heat, fanIn, effects, w.productDomain, smellMaxSeverity, runtimeEntrypoints);
1993
+ classified.push({
1994
+ rel: w.rel,
1995
+ abs: w.abs,
1996
+ lang: w.lang,
1339
1997
  isRealSource: real,
1340
1998
  demoteReason: demoteReason.get(w.rel) || null,
1341
1999
  gravity,
1342
2000
  heat,
1343
- gravitySignals,
1344
- heatSignals,
2001
+ gravitySignals: gs,
2002
+ heatSignals: hs,
1345
2003
  smells: w.ast.smells,
1346
2004
  pillarHint,
1347
- importedBy: [...importedBy.get(w.rel)].filter((src) => isRealSource.get(src)),
1348
- imports: [...importsResolved.get(w.rel)]
1349
- };
2005
+ importedBy: importedByReal,
2006
+ imports,
2007
+ importsUnresolved: importsUnresolvedArr,
2008
+ frameworkRole: w.frameworkRole,
2009
+ productDomain: w.productDomain,
2010
+ sideEffectProfile: effects,
2011
+ writeIntents,
2012
+ riskTypes,
2013
+ runtimeEntrypoints,
2014
+ entrypointTraceStatus,
2015
+ blockedImports: importsUnresolvedArr,
2016
+ loadBearingScore,
2017
+ hotSpans: w.ast.hotSpans,
2018
+ source: w.source
2019
+ });
1350
2020
  }
1351
- const realAnalyses = analyses.filter((a) => a.isRealSource).sort((a, b) => b.gravity - a.gravity);
1352
- const wildCandidates = realAnalyses.filter((a) => a.heat >= 60 || a.smells.some((s) => s.severity >= 4)).sort((a, b) => b.heat - a.heat);
1353
- const pillars = buildPillars(realAnalyses, communities, stack);
1354
- const topGravity = realAnalyses.slice(0, 12).map((a) => a.relativePath);
1355
- const topHeat = wildCandidates.slice(0, 12).map((a) => a.relativePath);
2021
+ const dir = join6(projectRoot, ".vibe-splainer");
2022
+ await mkdir5(dir, { recursive: true });
2023
+ const stage05 = Object.fromEntries(classified.map((f) => [f.rel, f.sideEffectProfile]));
2024
+ await writeFile6(join6(dir, "stage-05-side-effects.json"), JSON.stringify(stage05, null, 2), "utf8");
2025
+ const stage06 = Object.fromEntries(classified.map((f) => [f.rel, f.writeIntents]));
2026
+ await writeFile6(join6(dir, "stage-06-write-intents.json"), JSON.stringify(stage06, null, 2), "utf8");
2027
+ const stage07 = Object.fromEntries(classified.map((f) => [f.rel, f.riskTypes]));
2028
+ await writeFile6(join6(dir, "stage-07-risk-types.json"), JSON.stringify(stage07, null, 2), "utf8");
2029
+ const stage08 = Object.fromEntries(classified.map((f) => [f.rel, {
2030
+ isLoadBearing: f.loadBearingScore >= 5,
2031
+ loadBearingScore: f.loadBearingScore,
2032
+ runtimeEntrypoints: f.runtimeEntrypoints.length,
2033
+ entrypointTraceStatus: f.entrypointTraceStatus
2034
+ }]));
2035
+ await writeFile6(join6(dir, "stage-08-load-bearing.json"), JSON.stringify(stage08, null, 2), "utf8");
2036
+ const realClassified = classified.filter((f) => f.isRealSource).sort((a, b) => b.gravity - a.gravity);
2037
+ const wildCandidates = realClassified.filter((f) => f.heat >= 60 || f.smells.some((s) => s.severity >= 4));
2038
+ const pillars = buildPillars(classified, communities);
1356
2039
  const map = {
1357
- stack,
2040
+ stack: inv.stack,
1358
2041
  entrypoints: [...entrypoints],
1359
2042
  pillars,
1360
2043
  fileCount: work.length,
1361
- realSourceCount: realAnalyses.length,
1362
- topGravity,
1363
- topHeat,
2044
+ realSourceCount: realClassified.length,
2045
+ topGravity: realClassified.slice(0, 12).map((f) => f.rel),
2046
+ topHeat: wildCandidates.slice(0, 12).map((f) => f.rel),
1364
2047
  brief: null
1365
2048
  };
1366
- await writeGraph(projectRoot, graph);
1367
- const analysisStore = { files: persisted };
1368
- await writeAnalysis(projectRoot, analysisStore);
1369
- await writeDeltaTargets(projectRoot, analysisStore, entrypoints);
1370
- const uiUrl = `file://${join4(projectRoot, ".vibe-splainer", "ui", "index.html")}`;
1371
- return {
1372
- projectRoot,
1373
- totalFilesScanned: work.length,
1374
- realSourceCount: realAnalyses.length,
1375
- files: realAnalyses,
1376
- map,
1377
- wildCandidates,
1378
- uiUrl,
1379
- graph
1380
- };
2049
+ return { projectRoot, classified, stack: inv.stack, entrypoints, map, communities };
1381
2050
  }
1382
- function buildPillars(real, communities, _stack) {
1383
- const keywordGroups = /* @__PURE__ */ new Map();
1384
- const unlabeled = [];
1385
- for (const a of real) {
1386
- if (a.pillarHint && !a.pillarHint.startsWith("community-")) {
1387
- if (!keywordGroups.has(a.pillarHint))
1388
- keywordGroups.set(a.pillarHint, []);
1389
- keywordGroups.get(a.pillarHint).push(a);
1390
- } else {
1391
- unlabeled.push(a);
1392
- }
2051
+
2052
+ // ../brain/dist/pipeline/scoring.js
2053
+ import { join as join7 } from "path";
2054
+ import { writeFile as writeFile7, mkdir as mkdir6 } from "fs/promises";
2055
+ import { createHash } from "crypto";
2056
+ function computeSeverity(sideEffectProfile, productDomain, gravity, heat, maxNesting, hasLongFunctions, swallowedCatches, runtimeEntrypoints) {
2057
+ let score = 0;
2058
+ if (sideEffectProfile.includes("database_write"))
2059
+ score += 3;
2060
+ if (sideEffectProfile.includes("booking_mutation"))
2061
+ score += 4;
2062
+ if (sideEffectProfile.includes("payment_mutation"))
2063
+ score += 4;
2064
+ if (sideEffectProfile.includes("auth_token_mutation"))
2065
+ score += 4;
2066
+ if (sideEffectProfile.includes("webhook_delivery"))
2067
+ score += 3;
2068
+ if (sideEffectProfile.includes("webhook_ingress"))
2069
+ score += 3;
2070
+ if (sideEffectProfile.includes("calendar_mutation"))
2071
+ score += 3;
2072
+ if (productDomain === "booking_creation")
2073
+ score += 3;
2074
+ if (productDomain === "payments" || productDomain === "payments_webhooks")
2075
+ score += 3;
2076
+ if (productDomain === "auth_oauth")
2077
+ score += 3;
2078
+ if (productDomain === "webhooks")
2079
+ score += 2;
2080
+ if (gravity >= 85)
2081
+ score += 2;
2082
+ if (heat >= 70)
2083
+ score += 2;
2084
+ if (maxNesting >= 4)
2085
+ score += 1;
2086
+ if (hasLongFunctions)
2087
+ score += 1;
2088
+ if (swallowedCatches >= 1)
2089
+ score += 1;
2090
+ if (runtimeEntrypoints.length >= 2)
2091
+ score += 2;
2092
+ if (score >= 10)
2093
+ return 5;
2094
+ if (score >= 7)
2095
+ return 4;
2096
+ if (score >= 4)
2097
+ return 3;
2098
+ if (score >= 2)
2099
+ return 2;
2100
+ return 1;
2101
+ }
2102
+ function applyCorrections(file) {
2103
+ if (file.writeIntents.includes("handle_payment_webhook")) {
2104
+ if (!file.sideEffectProfile.includes("payment_mutation"))
2105
+ file.sideEffectProfile.push("payment_mutation");
2106
+ if (!file.sideEffectProfile.includes("webhook_ingress"))
2107
+ file.sideEffectProfile.push("webhook_ingress");
2108
+ file.sideEffectProfile = file.sideEffectProfile.filter((s) => s !== "none_detected");
2109
+ }
2110
+ if (file.sideEffectProfile.includes("payment_mutation") || file.sideEffectProfile.includes("booking_mutation")) {
2111
+ if (file.canonicalSeverity < 4)
2112
+ file.canonicalSeverity = 4;
2113
+ }
2114
+ if (file.canonicalSeverity === 5)
2115
+ file.canonicalLoadBearing = true;
2116
+ }
2117
+ function inferObservableOutputs(frameworkRole, productDomain, sideEffectProfile) {
2118
+ const outputs = [];
2119
+ const ENTRYPOINT_ROLES2 = /* @__PURE__ */ new Set(["app_route_page", "app_route_handler", "pages_route", "pages_api_route", "trpc_api_route"]);
2120
+ if (sideEffectProfile.includes("redirect"))
2121
+ outputs.push("redirect_url");
2122
+ if (ENTRYPOINT_ROLES2.has(frameworkRole))
2123
+ outputs.push("http_status");
2124
+ if (frameworkRole === "app_route_handler" || frameworkRole === "pages_api_route") {
2125
+ outputs.push("json_response_shape");
2126
+ }
2127
+ if (productDomain === "booking_creation" || productDomain === "booking_management")
2128
+ outputs.push("booking_uid");
2129
+ if (productDomain === "payments" || productDomain === "payments_webhooks")
2130
+ outputs.push("payment_status");
2131
+ if (productDomain === "auth_oauth")
2132
+ outputs.push("auth_token");
2133
+ if (sideEffectProfile.includes("webhook_delivery") || sideEffectProfile.includes("webhook_ingress")) {
2134
+ outputs.push("webhook_payload");
2135
+ }
2136
+ if (sideEffectProfile.includes("calendar_mutation"))
2137
+ outputs.push("calendar_event_id");
2138
+ if (sideEffectProfile.includes("email_send"))
2139
+ outputs.push("email_payload");
2140
+ if (sideEffectProfile.includes("analytics_event"))
2141
+ outputs.push("sdk_event_name");
2142
+ if (frameworkRole === "hook" || frameworkRole === "store")
2143
+ outputs.push("ui_state_transition");
2144
+ return [...new Set(outputs)];
2145
+ }
2146
+ function inferPatchRisk(productDomain, riskTypes, sideEffectProfile, importedByCount, loadBearingScore) {
2147
+ if (loadBearingScore >= 12 || productDomain === "booking_creation" && riskTypes.includes("mutation_orchestration")) {
2148
+ return {
2149
+ level: "critical",
2150
+ reason: `${productDomain} domain with ${riskTypes.join(", ")} \u2014 any patch risks breaking live booking, payment, or auth flows.`
2151
+ };
1393
2152
  }
1394
- const pillars = [];
1395
- for (const [name, files] of keywordGroups) {
1396
- const sorted = [...files].sort((a, b) => b.gravity - a.gravity);
1397
- pillars.push({
1398
- name,
1399
- description: `${name} subsystem: ${files.length} file${files.length > 1 ? "s" : ""} centered on ${basename(sorted[0].relativePath)}.`,
1400
- memberFiles: sorted.map((f) => f.relativePath)
2153
+ if (loadBearingScore >= 8 || sideEffectProfile.includes("payment_mutation") || sideEffectProfile.includes("auth_token_mutation")) {
2154
+ const external = sideEffectProfile.filter((s) => ["payment_mutation", "auth_token_mutation", "database_write", "webhook_delivery"].includes(s));
2155
+ return {
2156
+ level: "high",
2157
+ reason: `${productDomain} writes to external state (${external.join(", ") || "database"}). Changes require integration testing.`
2158
+ };
2159
+ }
2160
+ if (loadBearingScore >= 5 || importedByCount >= 5) {
2161
+ return { level: "medium", reason: `Imported by ${importedByCount} files. Interface changes will cascade.` };
2162
+ }
2163
+ return { level: "low", reason: "Locally contained \u2014 limited blast radius." };
2164
+ }
2165
+ function inferSafePatchStrategy(riskTypes, sideEffectProfile) {
2166
+ if (riskTypes.includes("mutation_orchestration")) {
2167
+ 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.";
2168
+ }
2169
+ if (riskTypes.includes("registry_bottleneck")) {
2170
+ return "Add new entries without removing existing keys. Treat the registry map as append-only until all consumers are verified.";
2171
+ }
2172
+ if (riskTypes.includes("registry_consumer")) {
2173
+ return "Verify the registry contract (Components.tsx) before patching. Changes to field types must be reflected in both the registry and all rendering paths.";
2174
+ }
2175
+ if (riskTypes.includes("route_handler_write_path")) {
2176
+ return "Add integration tests covering success and failure paths before modifying. Verify HTTP status codes and response shapes are preserved.";
2177
+ }
2178
+ if (riskTypes.includes("god_component") || riskTypes.includes("god_hook")) {
2179
+ return "Extract sub-concerns into separate modules first. Only refactor the extraction points after tests confirm equivalence.";
2180
+ }
2181
+ if (sideEffectProfile.includes("database_write")) {
2182
+ return "Wrap changes in a transaction or use a feature flag. Run against a staging database before production.";
2183
+ }
2184
+ return "Review importedBy before patching. Run affected integration tests.";
2185
+ }
2186
+ function inferDoNotTouch(sideEffectProfile, productDomain) {
2187
+ const items = [];
2188
+ if (sideEffectProfile.includes("payment_mutation"))
2189
+ items.push("payment flow branch");
2190
+ if (sideEffectProfile.includes("auth_token_mutation"))
2191
+ items.push("token issuance / refresh branch");
2192
+ if (sideEffectProfile.includes("webhook_delivery") || sideEffectProfile.includes("webhook_ingress")) {
2193
+ items.push("webhook payload shape");
2194
+ }
2195
+ if (sideEffectProfile.includes("redirect"))
2196
+ items.push("redirect URL strings");
2197
+ if (sideEffectProfile.includes("analytics_event"))
2198
+ items.push("SDK event names");
2199
+ if (sideEffectProfile.includes("booking_mutation")) {
2200
+ items.push("booking success response shape", "recurring booking branch");
2201
+ }
2202
+ if (productDomain === "auth_oauth")
2203
+ items.push("OAuth callback URLs", "token scopes");
2204
+ return items;
2205
+ }
2206
+ function inferTestProbes(writeIntents, observableOutputs) {
2207
+ const probes = [];
2208
+ if (writeIntents.includes("create_booking")) {
2209
+ probes.push({
2210
+ name: "standard booking success",
2211
+ scenario: "create a standard booking and assert success redirect and booking uid",
2212
+ expectedObservable: ["booking_uid", "redirect_url", "sdk_event_name"].filter((o) => observableOutputs.includes(o))
1401
2213
  });
1402
2214
  }
1403
- if (unlabeled.length > 0) {
1404
- const communityGroups = /* @__PURE__ */ new Map();
1405
- for (const a of unlabeled) {
1406
- const c = communities.get(a.relativePath);
1407
- if (c === void 0)
1408
- continue;
1409
- if (!communityGroups.has(c))
1410
- communityGroups.set(c, []);
1411
- communityGroups.get(c).push(a);
1412
- }
1413
- const remainingSlots = Math.max(0, 6 - pillars.length);
1414
- 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);
1415
- for (const g of sorted) {
1416
- const top = [...g.files].sort((a, b) => b.gravity - a.gravity);
1417
- const name = pillarNameFromCluster(top);
1418
- const existing = pillars.find((p) => p.name === name);
1419
- if (existing) {
1420
- existing.memberFiles.push(...top.map((f) => f.relativePath));
1421
- existing.description = `${name} subsystem: ${existing.memberFiles.length} files centered on ${basename(existing.memberFiles[0])}.`;
1422
- } else {
1423
- pillars.push({
1424
- name,
1425
- description: `${g.files.length} files centered on ${basename(top[0].relativePath)}.`,
1426
- memberFiles: top.map((f) => f.relativePath)
1427
- });
1428
- }
1429
- }
2215
+ if (writeIntents.includes("reschedule_booking")) {
2216
+ probes.push({
2217
+ name: "reschedule booking",
2218
+ scenario: "reschedule an existing booking and assert reschedule event path",
2219
+ expectedObservable: ["booking_uid", "redirect_url"].filter((o) => observableOutputs.includes(o))
2220
+ });
1430
2221
  }
1431
- pillars.sort((a, b) => {
1432
- const gravA = real.filter((f) => a.memberFiles.includes(f.relativePath)).reduce((s, f) => s + f.gravity, 0);
1433
- const gravB = real.filter((f) => b.memberFiles.includes(f.relativePath)).reduce((s, f) => s + f.gravity, 0);
1434
- return gravB - gravA;
1435
- });
1436
- if (pillars.length === 0 && real.length > 0) {
1437
- pillars.push({ name: "Core", description: "Primary application code.", memberFiles: real.slice(0, 20).map((f) => f.relativePath) });
2222
+ if (writeIntents.includes("create_recurring_booking")) {
2223
+ probes.push({
2224
+ name: "recurring booking",
2225
+ scenario: "create recurring booking and assert recurring success behavior",
2226
+ expectedObservable: ["booking_uid", "redirect_url"].filter((o) => observableOutputs.includes(o))
2227
+ });
1438
2228
  }
1439
- const finalPillars = [];
1440
- for (const p of pillars) {
1441
- if (p.memberFiles.length > 15) {
1442
- const groups = /* @__PURE__ */ new Map();
1443
- for (const f of p.memberFiles) {
1444
- let bucket = "Core";
1445
- if (f.includes("app/") || f.includes("pages/") || f.includes("routes/"))
1446
- bucket = "Routing";
1447
- else if (f.includes("components/") || f.includes("ui/"))
1448
- bucket = "Components";
1449
- else if (f.includes("hooks/") || f.includes("lib/") || f.includes("utils/"))
1450
- bucket = "Logic";
1451
- const d = basename(dirname(f));
1452
- const key = `${p.name} (${bucket} - ${d})`;
1453
- if (!groups.has(key))
1454
- groups.set(key, []);
1455
- groups.get(key).push(f);
1456
- }
1457
- for (const [key, files] of groups) {
1458
- if (files.length > 0) {
1459
- finalPillars.push({
1460
- name: key,
1461
- description: `Subdivided from ${p.name}`,
1462
- memberFiles: files
1463
- });
1464
- }
1465
- }
1466
- } else {
1467
- finalPillars.push(p);
1468
- }
2229
+ if (writeIntents.includes("handle_payment_webhook")) {
2230
+ probes.push({
2231
+ name: "payment webhook ingestion",
2232
+ scenario: "send a valid payment webhook and assert booking/payment state updated",
2233
+ expectedObservable: ["payment_status", "booking_uid", "http_status"].filter((o) => observableOutputs.includes(o))
2234
+ });
1469
2235
  }
1470
- const seen = /* @__PURE__ */ new Set();
1471
- for (const p of finalPillars) {
1472
- let n = p.name, i = 2;
1473
- while (seen.has(n)) {
1474
- n = `${p.name} ${i++}`;
1475
- }
1476
- p.name = n;
1477
- seen.add(n);
2236
+ if (writeIntents.includes("issue_auth_token")) {
2237
+ probes.push({
2238
+ name: "token issuance",
2239
+ scenario: "complete OAuth flow and assert access token issued with correct scopes",
2240
+ expectedObservable: ["auth_token", "http_status"].filter((o) => observableOutputs.includes(o))
2241
+ });
1478
2242
  }
1479
- return finalPillars;
2243
+ return probes;
1480
2244
  }
1481
- function pillarNameFromCluster(files) {
1482
- const hintCounts = /* @__PURE__ */ new Map();
1483
- for (const f of files) {
1484
- if (f.pillarHint && !f.pillarHint.startsWith("community-")) {
1485
- hintCounts.set(f.pillarHint, (hintCounts.get(f.pillarHint) || 0) + 1);
1486
- }
2245
+ function deriveConfidence(fanIn, gravity) {
2246
+ if (fanIn >= 10 && gravity >= 40)
2247
+ return "high";
2248
+ if (fanIn >= 5 || gravity >= 25)
2249
+ return "medium";
2250
+ return "low";
2251
+ }
2252
+ async function runScoring(projectRoot, cr) {
2253
+ const dir = join7(projectRoot, ".vibe-splainer");
2254
+ await mkdir6(dir, { recursive: true });
2255
+ const persisted = {};
2256
+ const severityBreakdowns = {};
2257
+ for (const f of cr.classified) {
2258
+ const severity = computeSeverity(f.sideEffectProfile, f.productDomain, f.gravity, f.heat, f.heatSignals.maxNesting, f.heatSignals.longFunctions > 0, f.heatSignals.swallowedCatches, f.runtimeEntrypoints);
2259
+ const isLoadBearing = f.loadBearingScore >= 5;
2260
+ const pf = {
2261
+ relativePath: f.rel,
2262
+ language: f.lang,
2263
+ isRealSource: f.isRealSource,
2264
+ demoteReason: f.demoteReason,
2265
+ gravity: Math.round(f.gravity),
2266
+ heat: Math.round(f.heat),
2267
+ gravitySignals: f.gravitySignals,
2268
+ heatSignals: f.heatSignals,
2269
+ smells: f.smells,
2270
+ pillarHint: f.pillarHint,
2271
+ importedBy: f.importedBy,
2272
+ imports: f.imports,
2273
+ importsUnresolved: f.importsUnresolved,
2274
+ frameworkRole: f.frameworkRole,
2275
+ productDomain: f.productDomain,
2276
+ sideEffectProfile: f.sideEffectProfile,
2277
+ hotSpans: f.hotSpans,
2278
+ riskTypes: f.riskTypes,
2279
+ writeIntents: f.writeIntents,
2280
+ canonicalSeverity: severity,
2281
+ canonicalLoadBearing: isLoadBearing
2282
+ };
2283
+ applyCorrections(pf);
2284
+ persisted[f.rel] = pf;
2285
+ severityBreakdowns[f.rel] = `severity=${pf.canonicalSeverity} loadBearing=${pf.canonicalLoadBearing} effects=${pf.sideEffectProfile.join(",")} domain=${pf.productDomain}`;
2286
+ }
2287
+ const stage09 = Object.fromEntries(Object.entries(persisted).filter(([, pf]) => pf.isRealSource).map(([rel, pf]) => [rel, { canonicalSeverity: pf.canonicalSeverity, canonicalLoadBearing: pf.canonicalLoadBearing, scoreBreakdown: severityBreakdowns[rel] }]));
2288
+ await writeFile7(join7(dir, "stage-09-severity.json"), JSON.stringify(stage09, null, 2), "utf8");
2289
+ const store = { files: persisted };
2290
+ const importedByMapForDelta = /* @__PURE__ */ new Map();
2291
+ for (const [rel, pf] of Object.entries(persisted)) {
2292
+ importedByMapForDelta.set(rel, new Set(pf.importedBy));
2293
+ }
2294
+ const metaForDelta = new Map(Object.entries(persisted).map(([rel, pf]) => [rel, { frameworkRole: pf.frameworkRole, productDomain: pf.productDomain }]));
2295
+ const deltaTargets = Object.values(persisted).filter((pf) => pf.isRealSource).sort((a, b) => b.gravity - a.gravity).map((pf) => {
2296
+ const runtimeEntrypoints = findRuntimeEntrypoints(pf.relativePath, importedByMapForDelta, metaForDelta);
2297
+ const entrypointTraceStatus = deriveEntrypointTraceStatus(pf.productDomain, runtimeEntrypoints, pf.importsUnresolved);
2298
+ const smellMaxSeverity = pf.smells.length > 0 ? Math.max(...pf.smells.map((s) => s.severity)) : 0;
2299
+ const loadBearingScore = computeLoadBearingScore(pf.gravity, pf.heat, pf.importedBy.length, pf.sideEffectProfile, pf.productDomain, smellMaxSeverity, runtimeEntrypoints);
2300
+ const observableOutputs = inferObservableOutputs(pf.frameworkRole, pf.productDomain, pf.sideEffectProfile);
2301
+ const patchRisk = inferPatchRisk(pf.productDomain, pf.riskTypes, pf.sideEffectProfile, pf.importedBy.length, loadBearingScore);
2302
+ const confidence = deriveConfidence(pf.gravitySignals.fanIn, pf.gravity);
2303
+ const fileHashInput = pf.hotSpans.map((h) => h.snippet).join("");
2304
+ const fileHash = createHash("sha256").update(fileHashInput || pf.relativePath).digest("hex").slice(0, 12);
2305
+ const rawEvidence = pf.hotSpans.map((span) => ({
2306
+ file: pf.relativePath,
2307
+ startLine: span.startLine,
2308
+ endLine: span.endLine,
2309
+ rawSourceExcerpt: span.snippet,
2310
+ evidenceHash: createHash("sha256").update(span.snippet).digest("hex").slice(0, 12)
2311
+ }));
2312
+ return {
2313
+ path: pf.relativePath,
2314
+ frameworkRole: pf.frameworkRole,
2315
+ productDomain: pf.productDomain,
2316
+ gravity: Math.round(pf.gravity),
2317
+ heat: Math.round(pf.heat),
2318
+ severity: pf.canonicalSeverity,
2319
+ confidence,
2320
+ isLoadBearing: pf.canonicalLoadBearing || loadBearingScore >= 5,
2321
+ loadBearingScore,
2322
+ riskTypes: pf.riskTypes,
2323
+ sideEffectProfile: pf.sideEffectProfile,
2324
+ blastRadius: pf.importedBy,
2325
+ runtimeEntrypoints,
2326
+ entrypointTraceStatus,
2327
+ blockedImports: pf.importsUnresolved,
2328
+ observableOutputs,
2329
+ writeIntents: pf.writeIntents,
2330
+ patchRisk,
2331
+ safePatchStrategy: inferSafePatchStrategy(pf.riskTypes, pf.sideEffectProfile),
2332
+ doNotTouch: inferDoNotTouch(pf.sideEffectProfile, pf.productDomain),
2333
+ testProbes: inferTestProbes(pf.writeIntents, observableOutputs),
2334
+ rawEvidence,
2335
+ analysisAnnotation: `${pf.frameworkRole} in ${pf.productDomain} domain. fanIn=${pf.gravitySignals.fanIn} cyclomatic=${pf.gravitySignals.cyclomatic} loc=${pf.gravitySignals.loc}`,
2336
+ hashes: { fileHash, evidenceHash: rawEvidence.map((e) => e.evidenceHash).join("-") }
2337
+ };
2338
+ });
2339
+ const dest = join7(dir, "delta_targets.json");
2340
+ const tmp = dest + ".tmp";
2341
+ await writeFile7(tmp, JSON.stringify(deltaTargets, null, 2), "utf8");
2342
+ const { rename } = await import("fs/promises");
2343
+ await rename(tmp, dest);
2344
+ const validationReport = buildValidationReport(store, deltaTargets);
2345
+ await writeFile7(join7(dir, "validation_report.json"), JSON.stringify(validationReport, null, 2), "utf8");
2346
+ for (const e of validationReport.errors) {
2347
+ console.error(`[vibe-splain] VALIDATION ERROR [${e.rule}] ${e.file}: ${e.detail}`);
1487
2348
  }
1488
- if (hintCounts.size > 0) {
1489
- const best = [...hintCounts.entries()].sort((a, b) => b[1] - a[1])[0];
1490
- if (best[1] >= files.length * 0.4)
1491
- return best[0];
2349
+ for (const w of validationReport.warnings) {
2350
+ console.error(`[vibe-splain] VALIDATION WARN [${w.rule}] ${w.file}: ${w.detail}`);
1492
2351
  }
1493
- const dirs = files.map((f) => dirname(f.relativePath)).filter((d) => d && d !== ".");
1494
- if (dirs.length) {
1495
- const segCounts = /* @__PURE__ */ new Map();
1496
- for (const d of dirs) {
1497
- const segments = d.split(sep).filter((s) => !MEANINGLESS_SEGMENTS.has(s.toLowerCase()));
1498
- const meaningful = segments.pop();
1499
- if (meaningful)
1500
- segCounts.set(meaningful, (segCounts.get(meaningful) || 0) + 1);
2352
+ return { store, deltaTargets, validationReport };
2353
+ }
2354
+ function buildValidationReport(store, deltaTargets) {
2355
+ const errors = [];
2356
+ const warnings = [];
2357
+ let passCount = 0;
2358
+ const deltaByPath = new Map(deltaTargets.map((d) => [d.path, d]));
2359
+ for (const [, pf] of Object.entries(store.files)) {
2360
+ if (!pf.isRealSource)
2361
+ continue;
2362
+ const delta = deltaByPath.get(pf.relativePath);
2363
+ if (pf.canonicalSeverity === 5 && !pf.canonicalLoadBearing) {
2364
+ errors.push({
2365
+ file: pf.relativePath,
2366
+ rule: "severity_5_not_load_bearing",
2367
+ detail: "severity=5 but canonicalLoadBearing=false \u2014 post-correction invariant violated",
2368
+ expected: "canonicalLoadBearing=true",
2369
+ actual: "canonicalLoadBearing=false"
2370
+ });
2371
+ continue;
1501
2372
  }
1502
- const top = [...segCounts.entries()].sort((a, b) => b[1] - a[1])[0];
1503
- if (top)
1504
- return titleCase(top[0]);
2373
+ if (pf.writeIntents.includes("handle_payment_webhook") && pf.sideEffectProfile.includes("none_detected")) {
2374
+ errors.push({
2375
+ file: pf.relativePath,
2376
+ rule: "payment_webhook_no_effects",
2377
+ detail: "writeIntents includes handle_payment_webhook but sideEffectProfile is none_detected",
2378
+ expected: "payment_mutation + webhook_ingress",
2379
+ actual: "none_detected"
2380
+ });
2381
+ continue;
2382
+ }
2383
+ if (pf.productDomain === "booking_creation" && delta?.entrypointTraceStatus === "no_runtime_entrypoint_found" && pf.importsUnresolved.length === 0) {
2384
+ errors.push({
2385
+ file: pf.relativePath,
2386
+ rule: "booking_creation_no_entrypoint_no_blockers",
2387
+ detail: "booking_creation domain with no entrypoint found and no blocked imports \u2014 classification may be wrong"
2388
+ });
2389
+ continue;
2390
+ }
2391
+ if (delta && delta.severity !== pf.canonicalSeverity) {
2392
+ errors.push({
2393
+ file: pf.relativePath,
2394
+ rule: "severity_mismatch_delta",
2395
+ detail: "DeltaTarget severity does not match canonicalSeverity",
2396
+ expected: String(pf.canonicalSeverity),
2397
+ actual: String(delta.severity)
2398
+ });
2399
+ continue;
2400
+ }
2401
+ if (pf.canonicalSeverity >= 4 && (delta?.rawEvidence.length ?? 0) === 0 && pf.hotSpans.length === 0) {
2402
+ errors.push({
2403
+ file: pf.relativePath,
2404
+ rule: "high_severity_no_evidence",
2405
+ detail: `severity=${pf.canonicalSeverity} but rawEvidence is empty`
2406
+ });
2407
+ continue;
2408
+ }
2409
+ if (pf.canonicalSeverity >= 4 && (delta?.runtimeEntrypoints.length ?? 0) === 0) {
2410
+ warnings.push({
2411
+ file: pf.relativePath,
2412
+ rule: "high_severity_no_entrypoints",
2413
+ detail: `severity=${pf.canonicalSeverity} but no runtime entrypoints found \u2014 check alias resolution`
2414
+ });
2415
+ }
2416
+ if (delta?.entrypointTraceStatus === "partial_wrong_surface") {
2417
+ const foundPaths = delta.runtimeEntrypoints.map((e) => e.path).join(", ");
2418
+ warnings.push({
2419
+ file: pf.relativePath,
2420
+ rule: "partial_wrong_surface",
2421
+ detail: `Entrypoints found but domain surface mismatch for ${pf.productDomain}. Found: ${foundPaths}`
2422
+ });
2423
+ }
2424
+ passCount++;
1505
2425
  }
1506
- const topFile = basename(files[0].relativePath, extname(files[0].relativePath));
1507
- return titleCase(topFile);
2426
+ return {
2427
+ timestamp: (/* @__PURE__ */ new Date()).toISOString(),
2428
+ passed: errors.length === 0,
2429
+ errors,
2430
+ warnings,
2431
+ summary: { errorCount: errors.length, warningCount: warnings.length, passCount }
2432
+ };
1508
2433
  }
1509
- function titleCase(s) {
1510
- return s.replace(/[-_]/g, " ").replace(/\b\w/g, (c) => c.toUpperCase());
2434
+
2435
+ // ../brain/dist/pipeline/orchestrator.js
2436
+ async function runPipeline(projectRoot) {
2437
+ const inv = await runInventory(projectRoot);
2438
+ const res = await runResolution(projectRoot, inv);
2439
+ const cr = await runClassification(projectRoot, inv, res);
2440
+ const scoring = await runScoring(projectRoot, cr);
2441
+ await writeGraph(projectRoot, res.graph);
2442
+ await writeAnalysis(projectRoot, scoring.store);
2443
+ const files = cr.classified.filter((f) => f.isRealSource).sort((a, b) => b.gravity - a.gravity).map((f) => ({
2444
+ path: f.abs,
2445
+ relativePath: f.rel,
2446
+ language: f.lang,
2447
+ isRealSource: f.isRealSource,
2448
+ demoteReason: f.demoteReason,
2449
+ gravity: Math.round(f.gravity),
2450
+ heat: Math.round(f.heat),
2451
+ gravitySignals: f.gravitySignals,
2452
+ heatSignals: f.heatSignals,
2453
+ smells: f.smells,
2454
+ pillarHint: f.pillarHint,
2455
+ frameworkRole: f.frameworkRole,
2456
+ productDomain: f.productDomain,
2457
+ sideEffectProfile: f.sideEffectProfile
2458
+ }));
2459
+ 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) => ({
2460
+ path: f.abs,
2461
+ relativePath: f.rel,
2462
+ language: f.lang,
2463
+ isRealSource: f.isRealSource,
2464
+ demoteReason: f.demoteReason,
2465
+ gravity: Math.round(f.gravity),
2466
+ heat: Math.round(f.heat),
2467
+ gravitySignals: f.gravitySignals,
2468
+ heatSignals: f.heatSignals,
2469
+ smells: f.smells,
2470
+ pillarHint: f.pillarHint,
2471
+ frameworkRole: f.frameworkRole,
2472
+ productDomain: f.productDomain,
2473
+ sideEffectProfile: f.sideEffectProfile
2474
+ }));
2475
+ const uiUrl = `file://${join8(projectRoot, ".vibe-splainer", "ui", "index.html")}`;
2476
+ return {
2477
+ projectRoot,
2478
+ totalFilesScanned: cr.classified.length,
2479
+ realSourceCount: files.length,
2480
+ files,
2481
+ map: cr.map,
2482
+ wildCandidates,
2483
+ uiUrl,
2484
+ graph: res.graph,
2485
+ validation: {
2486
+ passed: scoring.validationReport.passed,
2487
+ errors: scoring.validationReport.summary.errorCount,
2488
+ warnings: scoring.validationReport.summary.warningCount,
2489
+ reportPath: ".vibe-splainer/validation_report.json"
2490
+ }
2491
+ };
2492
+ }
2493
+
2494
+ // ../brain/dist/scanner.js
2495
+ async function initParser2() {
2496
+ return initParser();
2497
+ }
2498
+ async function scanProject(projectRoot) {
2499
+ return runPipeline(projectRoot);
1511
2500
  }
1512
2501
  async function getFileAnalysis(absPath) {
1513
- const ext = extname(absPath);
2502
+ const ext = extname4(absPath);
1514
2503
  const lang = EXT_LANG[ext];
1515
2504
  if (!lang)
1516
2505
  return null;
1517
2506
  let source;
1518
2507
  try {
1519
- source = await readFile4(absPath, "utf8");
2508
+ source = await readFile6(absPath, "utf8");
1520
2509
  } catch {
1521
2510
  return null;
1522
2511
  }
@@ -1535,20 +2524,19 @@ async function getFileAnalysis(absPath) {
1535
2524
  reason: `${s.kind}: ${s.note}`
1536
2525
  };
1537
2526
  });
1538
- const heatSignals = {
1539
- todos: ast.smells.filter((s) => s.kind === "todo").length,
1540
- suppressions: ast.smells.filter((s) => s.kind === "suppression").length,
1541
- swallowedCatches: ast.swallowedCatches,
1542
- maxNesting: ast.maxNesting,
1543
- longFunctions: ast.longFunctions,
1544
- magicNumbers: ast.magicNumbers
1545
- };
1546
2527
  return {
1547
2528
  language: lang,
1548
2529
  signature: ast.signature,
1549
2530
  hotSpans: ast.hotSpans,
1550
2531
  smellSpans,
1551
- heatSignals,
2532
+ heatSignals: {
2533
+ todos: ast.smells.filter((s) => s.kind === "todo").length,
2534
+ suppressions: ast.smells.filter((s) => s.kind === "suppression").length,
2535
+ swallowedCatches: ast.swallowedCatches,
2536
+ maxNesting: ast.maxNesting,
2537
+ longFunctions: ast.longFunctions,
2538
+ magicNumbers: ast.magicNumbers
2539
+ },
1552
2540
  loc: ast.loc,
1553
2541
  cyclomatic: ast.cyclomatic
1554
2542
  };
@@ -1556,16 +2544,16 @@ async function getFileAnalysis(absPath) {
1556
2544
 
1557
2545
  // ../brain/dist/dossier.js
1558
2546
  import { Mutex } from "async-mutex";
1559
- import { join as join5, dirname as dirname2 } from "path";
2547
+ import { join as join9, dirname as dirname3 } from "path";
1560
2548
  import { fileURLToPath as fileURLToPath2 } from "url";
1561
- import { readFile as readFile5, writeFile as writeFile4, mkdir as mkdir3 } from "fs/promises";
1562
- import { existsSync as existsSync3, cpSync } from "fs";
1563
- var __dirname2 = dirname2(fileURLToPath2(import.meta.url));
2549
+ import { readFile as readFile7, writeFile as writeFile8, mkdir as mkdir7 } from "fs/promises";
2550
+ import { existsSync as existsSync4, cpSync } from "fs";
2551
+ var __dirname2 = dirname3(fileURLToPath2(import.meta.url));
1564
2552
  var dossierMutex = new Mutex();
1565
2553
  async function readDossier(projectRoot) {
1566
- const dossierPath = join5(projectRoot, ".vibe-splainer", "dossier.json");
2554
+ const dossierPath = join9(projectRoot, ".vibe-splainer", "dossier.json");
1567
2555
  try {
1568
- const raw = await readFile5(dossierPath, "utf8");
2556
+ const raw = await readFile7(dossierPath, "utf8");
1569
2557
  return JSON.parse(raw);
1570
2558
  } catch {
1571
2559
  return null;
@@ -1577,33 +2565,33 @@ async function writeDossier(projectRoot, dossier) {
1577
2565
  p.decisions = p.decisions.filter((c) => !(c.severity === 1 && c.category === "Convention"));
1578
2566
  p.cardCount = p.decisions.length;
1579
2567
  }
1580
- const dir = join5(projectRoot, ".vibe-splainer");
1581
- await mkdir3(dir, { recursive: true });
1582
- const dossierPath = join5(dir, "dossier.json");
2568
+ const dir = join9(projectRoot, ".vibe-splainer");
2569
+ await mkdir7(dir, { recursive: true });
2570
+ const dossierPath = join9(dir, "dossier.json");
1583
2571
  const tmp = dossierPath + ".tmp";
1584
- await writeFile4(tmp, JSON.stringify(dossier, null, 2), "utf8");
2572
+ await writeFile8(tmp, JSON.stringify(dossier, null, 2), "utf8");
1585
2573
  const { rename } = await import("fs/promises");
1586
2574
  await rename(tmp, dossierPath);
1587
2575
  await regenerateUI(projectRoot, dossier);
1588
2576
  });
1589
2577
  }
1590
2578
  async function regenerateUI(projectRoot, dossier) {
1591
- const uiDir = join5(projectRoot, ".vibe-splainer", "ui");
1592
- await mkdir3(uiDir, { recursive: true });
1593
- let templateDir = join5(__dirname2, "ui");
1594
- if (!existsSync3(templateDir)) {
1595
- templateDir = join5(__dirname2, "../../cli/dist/ui");
2579
+ const uiDir = join9(projectRoot, ".vibe-splainer", "ui");
2580
+ await mkdir7(uiDir, { recursive: true });
2581
+ let templateDir = join9(__dirname2, "ui");
2582
+ if (!existsSync4(templateDir)) {
2583
+ templateDir = join9(__dirname2, "../../cli/dist/ui");
1596
2584
  }
1597
- if (!existsSync3(templateDir)) {
2585
+ if (!existsSync4(templateDir)) {
1598
2586
  console.error("[vibe-splain] UI template not found at", templateDir, "- skipping UI regeneration");
1599
2587
  return;
1600
2588
  }
1601
2589
  cpSync(templateDir, uiDir, { recursive: true });
1602
- let html = await readFile5(join5(templateDir, "index.html"), "utf8");
2590
+ let html = await readFile7(join9(templateDir, "index.html"), "utf8");
1603
2591
  const injection = `<script>window.__VIBE_DOSSIER__ = ${JSON.stringify(dossier)};</script>`;
1604
2592
  html = html.replace("<!-- VIBE_DOSSIER_INJECTION_POINT -->", injection);
1605
- await writeFile4(join5(uiDir, "index.html"), html, "utf8");
1606
- console.error("[vibe-splain] UI regenerated at", join5(uiDir, "index.html"));
2593
+ await writeFile8(join9(uiDir, "index.html"), html, "utf8");
2594
+ console.error("[vibe-splain] UI regenerated at", join9(uiDir, "index.html"));
1607
2595
  }
1608
2596
  function validateMermaidNodeCount(diagram) {
1609
2597
  if (!diagram)
@@ -1622,9 +2610,9 @@ function validateMermaidNodeCount(diagram) {
1622
2610
 
1623
2611
  // ../brain/dist/watcher.js
1624
2612
  import chokidar from "chokidar";
1625
- import { createHash } from "crypto";
1626
- import { readFile as readFile6 } from "fs/promises";
1627
- import { join as join6 } from "path";
2613
+ import { createHash as createHash2 } from "crypto";
2614
+ import { readFile as readFile8 } from "fs/promises";
2615
+ import { join as join10 } from "path";
1628
2616
  function startWatcher(projectRoot, watchedPaths) {
1629
2617
  const watcher = chokidar.watch(watchedPaths.length > 0 ? watchedPaths : projectRoot, {
1630
2618
  ignoreInitial: true,
@@ -1636,14 +2624,14 @@ function startWatcher(projectRoot, watchedPaths) {
1636
2624
  const dossier = await readDossier(projectRoot);
1637
2625
  if (!dossier)
1638
2626
  return;
1639
- const content = await readFile6(filepath, "utf8");
1640
- const newHash = createHash("sha256").update(content).digest("hex");
2627
+ const content = await readFile8(filepath, "utf8");
2628
+ const newHash = createHash2("sha256").update(content).digest("hex");
1641
2629
  let mutated = false;
1642
2630
  for (const pillar of dossier.pillars) {
1643
2631
  for (const card of pillar.decisions) {
1644
2632
  if (!card.primaryFile)
1645
2633
  continue;
1646
- const absMatch = filepath === join6(projectRoot, card.primaryFile) || filepath.endsWith("/" + card.primaryFile);
2634
+ const absMatch = filepath === join10(projectRoot, card.primaryFile) || filepath.endsWith("/" + card.primaryFile);
1647
2635
  if (absMatch && card.lastScannedHash !== newHash) {
1648
2636
  card.status = "stale";
1649
2637
  const rel = card.primaryFile;
@@ -1702,7 +2690,22 @@ async function handleScanProject(args) {
1702
2690
  await writeDossier(projectRoot, dossier);
1703
2691
  startWatcher(projectRoot, result.files.map((f) => f.path));
1704
2692
  console.error(`[vibe-splain] Scan complete. ${result.totalFilesScanned} files, ${result.realSourceCount} real-source, ${result.wildCandidates.length} wild candidates.`);
2693
+ const validation = result.validation ?? { passed: true, errors: 0, warnings: 0, reportPath: ".vibe-splainer/validation_report.json" };
1705
2694
  return {
2695
+ ok: true,
2696
+ validation: {
2697
+ passed: validation.passed,
2698
+ errors: validation.errors,
2699
+ warnings: validation.warnings,
2700
+ reportPath: validation.reportPath
2701
+ },
2702
+ artifacts: {
2703
+ analysis: ".vibe-splainer/analysis.json",
2704
+ deltaTargets: ".vibe-splainer/delta_targets.json",
2705
+ dossier: ".vibe-splainer/dossier.json",
2706
+ graph: ".vibe-splainer/graph.json",
2707
+ html: ".vibe-splainer/ui/index.html"
2708
+ },
1706
2709
  projectRoot: result.projectRoot,
1707
2710
  totalFilesScanned: result.totalFilesScanned,
1708
2711
  realSourceCount: result.realSourceCount,
@@ -1798,8 +2801,8 @@ async function handleSetProjectBrief(args) {
1798
2801
  }
1799
2802
 
1800
2803
  // dist/mcp/tools/get_file_context.js
1801
- import { readFile as readFile7 } from "fs/promises";
1802
- import { join as join7, relative as relative2, isAbsolute } from "path";
2804
+ import { readFile as readFile9 } from "fs/promises";
2805
+ import { join as join11, relative as relative3, isAbsolute } from "path";
1803
2806
  var getFileContextTool = {
1804
2807
  name: "get_file_context",
1805
2808
  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.",
@@ -1819,8 +2822,8 @@ async function handleGetFileContext(args) {
1819
2822
  const full = args.full === true;
1820
2823
  if (!projectRoot || !filePath)
1821
2824
  throw new Error("projectRoot and filePath are required");
1822
- const fullPath = isAbsolute(filePath) ? filePath : join7(projectRoot, filePath);
1823
- const relPath = relative2(projectRoot, fullPath);
2825
+ const fullPath = isAbsolute(filePath) ? filePath : join11(projectRoot, filePath);
2826
+ const relPath = relative3(projectRoot, fullPath);
1824
2827
  const evidence = await getFileAnalysis(fullPath);
1825
2828
  if (!evidence) {
1826
2829
  throw new Error(`Could not analyze ${relPath} (unsupported language or parse failure).`);
@@ -1844,16 +2847,16 @@ async function handleGetFileContext(args) {
1844
2847
  smellSpans: evidence.smellSpans
1845
2848
  };
1846
2849
  if (full) {
1847
- result.source = await readFile7(fullPath, "utf8");
2850
+ result.source = await readFile9(fullPath, "utf8");
1848
2851
  }
1849
2852
  return result;
1850
2853
  }
1851
2854
 
1852
2855
  // dist/mcp/tools/write_decision_card.js
1853
2856
  import { v4 as uuidv4 } from "uuid";
1854
- import { createHash as createHash2 } from "crypto";
1855
- import { readFile as readFile8 } from "fs/promises";
1856
- import { join as join8 } from "path";
2857
+ import { createHash as createHash3 } from "crypto";
2858
+ import { readFile as readFile10 } from "fs/promises";
2859
+ import { join as join12 } from "path";
1857
2860
  var CATEGORIES = ["Bottleneck", "Hack", "Smart-Move", "Risk", "Convention", "Dead-Weight"];
1858
2861
  function normalizeSnippet(s) {
1859
2862
  let out = (s ?? "").replace(/\r\n/g, "\n");
@@ -1945,10 +2948,10 @@ async function handleWriteDecisionCard(args) {
1945
2948
  const heat = persisted ? Math.round(persisted.heat) : void 0;
1946
2949
  let primaryContent = "";
1947
2950
  try {
1948
- primaryContent = await readFile8(join8(projectRoot, primaryFile), "utf8");
2951
+ primaryContent = await readFile10(join12(projectRoot, primaryFile), "utf8");
1949
2952
  } catch {
1950
2953
  }
1951
- const hash = createHash2("sha256").update(primaryContent).digest("hex");
2954
+ const hash = createHash3("sha256").update(primaryContent).digest("hex");
1952
2955
  const card = {
1953
2956
  id: uuidv4(),
1954
2957
  pillar,
@@ -2189,7 +3192,7 @@ var TOOL_HANDLERS = {
2189
3192
  mark_stale: handleMarkStale
2190
3193
  };
2191
3194
  async function startMCPServer() {
2192
- await initParser();
3195
+ await initParser2();
2193
3196
  console.error("[vibe-splain] Tree-Sitter parser initialized");
2194
3197
  const server = new Server({ name: "vibe-splain", version: "2.0.0" }, { capabilities: { tools: {}, prompts: {} } });
2195
3198
  server.setRequestHandler(ListPromptsRequestSchema, async () => ({
@@ -2314,7 +3317,7 @@ async function serveCommand() {
2314
3317
 
2315
3318
  // dist/index.js
2316
3319
  var program = new Command();
2317
- program.name("vibe-splain").description("Architectural dossier engine for vibe-coded projects").version("2.3.1");
3320
+ program.name("vibe-splain").description("Architectural dossier engine for vibe-coded projects").version("2.5.0");
2318
3321
  program.command("install").description("Patch coding agent MCP config files to register vibe-splain").action(installCommand);
2319
3322
  program.command("serve").description("Start the MCP server (called by the coding agent, not by you)").action(serveCommand);
2320
3323
  program.parse();