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 +1696 -693
- package/dist/mcp/tools/scan_project.js +15 -0
- package/package.json +1 -1
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
|
|
91
|
-
import {
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
import {
|
|
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/
|
|
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
|
|
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, "
|
|
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
|
|
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 (
|
|
256
|
-
return
|
|
191
|
+
if (_parser)
|
|
192
|
+
return _parser;
|
|
257
193
|
await Parser.init();
|
|
258
|
-
|
|
194
|
+
_parser = new Parser();
|
|
259
195
|
const ts = await getLanguage("typescript");
|
|
260
196
|
if (ts)
|
|
261
|
-
|
|
262
|
-
return
|
|
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
|
-
|
|
417
|
-
|
|
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
|
-
|
|
584
|
-
|
|
585
|
-
|
|
586
|
-
|
|
587
|
-
|
|
588
|
-
if (
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
592
|
-
|
|
593
|
-
|
|
594
|
-
|
|
595
|
-
|
|
596
|
-
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
|
|
610
|
-
|
|
611
|
-
|
|
612
|
-
|
|
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
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
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 (
|
|
629
|
-
|
|
630
|
-
|
|
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
|
-
|
|
633
|
-
|
|
634
|
-
|
|
635
|
-
|
|
636
|
-
|
|
637
|
-
|
|
638
|
-
|
|
639
|
-
|
|
640
|
-
|
|
641
|
-
|
|
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
|
-
|
|
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
|
|
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
|
-
|
|
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
|
|
819
|
-
const
|
|
820
|
-
const
|
|
821
|
-
const
|
|
822
|
-
|
|
823
|
-
|
|
824
|
-
|
|
825
|
-
|
|
826
|
-
|
|
827
|
-
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
}
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
|
|
840
|
-
|
|
841
|
-
|
|
842
|
-
|
|
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
|
|
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
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
|
|
934
|
-
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
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
|
-
|
|
940
|
-
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
944
|
-
|
|
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
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
|
|
953
|
-
|
|
954
|
-
|
|
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
|
-
|
|
957
|
-
|
|
958
|
-
}
|
|
959
|
-
walk2(root);
|
|
960
|
-
return out;
|
|
1199
|
+
} catch {
|
|
1200
|
+
continue;
|
|
1201
|
+
}
|
|
961
1202
|
}
|
|
962
|
-
|
|
963
|
-
|
|
964
|
-
|
|
965
|
-
|
|
966
|
-
|
|
967
|
-
|
|
968
|
-
|
|
969
|
-
|
|
970
|
-
|
|
971
|
-
|
|
972
|
-
|
|
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
|
-
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
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
|
-
|
|
981
|
-
|
|
982
|
-
|
|
983
|
-
|
|
984
|
-
|
|
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
|
-
|
|
987
|
-
|
|
988
|
-
|
|
989
|
-
|
|
990
|
-
|
|
991
|
-
|
|
992
|
-
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
996
|
-
|
|
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
|
-
|
|
1006
|
-
|
|
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
|
-
|
|
1009
|
-
return
|
|
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
|
|
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
|
-
|
|
1086
|
-
|
|
1087
|
-
|
|
1088
|
-
|
|
1089
|
-
const
|
|
1090
|
-
|
|
1091
|
-
|
|
1092
|
-
|
|
1093
|
-
|
|
1094
|
-
|
|
1095
|
-
|
|
1096
|
-
|
|
1097
|
-
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1104
|
-
|
|
1105
|
-
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1116
|
-
const
|
|
1117
|
-
|
|
1118
|
-
|
|
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 (
|
|
1136
|
-
|
|
1137
|
-
|
|
1138
|
-
|
|
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
|
-
|
|
1156
|
-
|
|
1157
|
-
|
|
1158
|
-
|
|
1159
|
-
const
|
|
1160
|
-
const
|
|
1161
|
-
|
|
1162
|
-
|
|
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
|
-
|
|
1734
|
+
const topFile = basename2(files[0].rel, extname3(files[0].rel));
|
|
1735
|
+
return titleCase(topFile);
|
|
1169
1736
|
}
|
|
1170
|
-
|
|
1171
|
-
|
|
1172
|
-
|
|
1173
|
-
|
|
1174
|
-
|
|
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
|
-
|
|
1186
|
-
|
|
1187
|
-
const
|
|
1188
|
-
|
|
1189
|
-
const
|
|
1190
|
-
|
|
1191
|
-
|
|
1192
|
-
|
|
1193
|
-
|
|
1194
|
-
|
|
1195
|
-
|
|
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
|
|
1198
|
-
const
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
1202
|
-
|
|
1203
|
-
|
|
1204
|
-
|
|
1205
|
-
|
|
1206
|
-
|
|
1207
|
-
|
|
1208
|
-
|
|
1209
|
-
|
|
1210
|
-
|
|
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
|
-
|
|
1213
|
-
|
|
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
|
-
|
|
1224
|
-
|
|
1225
|
-
|
|
1226
|
-
|
|
1227
|
-
|
|
1228
|
-
|
|
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
|
-
|
|
1231
|
-
|
|
1232
|
-
|
|
1233
|
-
|
|
1234
|
-
const
|
|
1235
|
-
|
|
1236
|
-
|
|
1237
|
-
|
|
1238
|
-
|
|
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(
|
|
1281
|
-
const tDir = target.split(
|
|
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
|
|
1290
|
-
const
|
|
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
|
|
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
|
|
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
|
|
1323
|
-
|
|
1324
|
-
|
|
1325
|
-
|
|
1326
|
-
|
|
1327
|
-
|
|
1328
|
-
|
|
1329
|
-
|
|
1330
|
-
|
|
1331
|
-
|
|
1332
|
-
|
|
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:
|
|
1348
|
-
imports
|
|
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
|
|
1352
|
-
|
|
1353
|
-
const
|
|
1354
|
-
|
|
1355
|
-
const
|
|
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:
|
|
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
|
-
|
|
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
|
-
|
|
1383
|
-
|
|
1384
|
-
|
|
1385
|
-
|
|
1386
|
-
|
|
1387
|
-
|
|
1388
|
-
|
|
1389
|
-
|
|
1390
|
-
|
|
1391
|
-
|
|
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
|
-
|
|
1395
|
-
|
|
1396
|
-
|
|
1397
|
-
|
|
1398
|
-
|
|
1399
|
-
|
|
1400
|
-
|
|
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 (
|
|
1404
|
-
|
|
1405
|
-
|
|
1406
|
-
|
|
1407
|
-
|
|
1408
|
-
|
|
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
|
-
|
|
1432
|
-
|
|
1433
|
-
|
|
1434
|
-
|
|
1435
|
-
|
|
1436
|
-
|
|
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
|
-
|
|
1440
|
-
|
|
1441
|
-
|
|
1442
|
-
|
|
1443
|
-
|
|
1444
|
-
|
|
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
|
-
|
|
1471
|
-
|
|
1472
|
-
|
|
1473
|
-
|
|
1474
|
-
|
|
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
|
|
2243
|
+
return probes;
|
|
1480
2244
|
}
|
|
1481
|
-
function
|
|
1482
|
-
|
|
1483
|
-
|
|
1484
|
-
|
|
1485
|
-
|
|
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
|
-
|
|
1489
|
-
|
|
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
|
-
|
|
1494
|
-
|
|
1495
|
-
|
|
1496
|
-
|
|
1497
|
-
|
|
1498
|
-
|
|
1499
|
-
|
|
1500
|
-
|
|
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
|
-
|
|
1503
|
-
|
|
1504
|
-
|
|
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
|
-
|
|
1507
|
-
|
|
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
|
-
|
|
1510
|
-
|
|
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 =
|
|
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
|
|
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
|
|
2547
|
+
import { join as join9, dirname as dirname3 } from "path";
|
|
1560
2548
|
import { fileURLToPath as fileURLToPath2 } from "url";
|
|
1561
|
-
import { readFile as
|
|
1562
|
-
import { existsSync as
|
|
1563
|
-
var __dirname2 =
|
|
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 =
|
|
2554
|
+
const dossierPath = join9(projectRoot, ".vibe-splainer", "dossier.json");
|
|
1567
2555
|
try {
|
|
1568
|
-
const raw = await
|
|
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 =
|
|
1581
|
-
await
|
|
1582
|
-
const dossierPath =
|
|
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
|
|
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 =
|
|
1592
|
-
await
|
|
1593
|
-
let templateDir =
|
|
1594
|
-
if (!
|
|
1595
|
-
templateDir =
|
|
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 (!
|
|
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
|
|
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
|
|
1606
|
-
console.error("[vibe-splain] UI regenerated at",
|
|
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
|
|
1627
|
-
import { join as
|
|
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
|
|
1640
|
-
const newHash =
|
|
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 ===
|
|
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
|
|
1802
|
-
import { join as
|
|
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 :
|
|
1823
|
-
const relPath =
|
|
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
|
|
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
|
|
1855
|
-
import { readFile as
|
|
1856
|
-
import { join as
|
|
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
|
|
2951
|
+
primaryContent = await readFile10(join12(projectRoot, primaryFile), "utf8");
|
|
1949
2952
|
} catch {
|
|
1950
2953
|
}
|
|
1951
|
-
const hash =
|
|
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
|
|
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.
|
|
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();
|